9 Commits

Author SHA1 Message Date
37ec05bf3b Merge branch 'k8s-integration' into develop 2023-09-20 22:14:56 +02:00
3434802b31 🎨 Reorganize back-end code 2023-09-20 22:08:32 +02:00
bdbc72ab39 🐛 Front: Fix URL used to fetch transport mode representation 2023-09-10 12:25:38 +02:00
4cc8f60076 🐛 Front: Use the public API server to fetch data 2023-09-10 12:17:48 +02:00
cf5c4c6224 🔒️ Fix CORS allowed origins and methods 2023-09-10 12:07:20 +02:00
f69aee1c9c 🔒️ Remove driver and password from configuration file
Password will be provided by vault using an env variable.
2023-09-10 12:04:25 +02:00
8c493f8fab ♻️ Remove pg_trgm creation from the db session init
The pg_trgm extension will be created during db init, by the db-updated image.
2023-09-10 11:46:24 +02:00
4fce832db5 ♻️ Rename docker file building api image 2023-09-10 11:45:08 +02:00
bfc669cd11 ♻️ Use pydantic-settings to handle config file 2023-09-09 23:35:18 +02:00
32 changed files with 113 additions and 108 deletions

View File

@@ -9,7 +9,7 @@ ENV POETRY_NO_INTERACTION=1 \
WORKDIR /app WORKDIR /app
COPY ./pyproject.toml /app COPY pyproject.toml /app
RUN poetry install --only=main --no-root && \ RUN poetry install --only=main --no-root && \
rm -rf ${POETRY_CACHE_DIR} rm -rf ${POETRY_CACHE_DIR}
@@ -29,10 +29,6 @@ env VIRTUAL_ENV=/app/.venv \
COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV} COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
COPY backend /app/backend COPY api /app/api
COPY dependencies.py /app
COPY config.sample.yaml /app
COPY routers/ /app/routers
COPY main.py /app
CMD ["python", "./main.py"] CMD ["python", "./api/main.py"]

View File

@@ -34,10 +34,7 @@ env VIRTUAL_ENV=/app/.venv \
COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV} COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
COPY backend /app/backend COPY api /app/api
COPY dependencies.py /app
COPY config.sample.yaml /app
COPY config.local.yaml /app
COPY db_updater /app/db_updater COPY db_updater /app/db_updater
CMD ["python", "-m", "db_updater.fill_db"] CMD ["python", "-m", "db_updater.fill_db"]

View File

@@ -10,9 +10,7 @@ db:
name: carrramba-encore-rate name: carrramba-encore-rate
host: postgres host: postgres
port: 5432 port: 5432
driver: postgresql+psycopg
user: cer user: cer
password: cer_password
cache: cache:
enable: true enable: true

View File

@@ -14,7 +14,7 @@ from sqlalchemy.ext.asyncio import (
) )
from .base_class import Base from .base_class import Base
from ..settings import DatabaseSettings from settings import DatabaseSettings
logger = getLogger(__name__) logger = getLogger(__name__)
@@ -61,9 +61,6 @@ class Database:
while not ret: while not ret:
try: try:
async with self._async_engine.begin() as session: async with self._async_engine.begin() as session:
await session.execute(
text("CREATE EXTENSION IF NOT EXISTS pg_trgm;")
)
if clear_static_data: if clear_static_data:
await session.run_sync(Base.metadata.drop_all) await session.run_sync(Base.metadata.drop_all)
await session.run_sync(Base.metadata.create_all) await session.run_sync(Base.metadata.create_all)

View File

@@ -4,9 +4,9 @@ from fastapi_cache.backends.redis import RedisBackend
from redis import asyncio as aioredis from redis import asyncio as aioredis
from yaml import safe_load from yaml import safe_load
from backend.db import db from db import db
from backend.idfm_interface.idfm_interface import IdfmInterface from idfm_interface.idfm_interface import IdfmInterface
from backend.settings import CacheSettings, Settings from settings import CacheSettings, Settings
CONFIG_PATH = environ.get("CONFIG_PATH", "./config.sample.yaml") CONFIG_PATH = environ.get("CONFIG_PATH", "./config.sample.yaml")

View File

@@ -7,9 +7,9 @@ from aiohttp import ClientSession
from msgspec import ValidationError from msgspec import ValidationError
from msgspec.json import Decoder from msgspec.json import Decoder
from ..db import Database
from ..models import Line, Stop, StopArea
from .idfm_types import Destinations as IdfmDestinations, IdfmResponse, IdfmState from .idfm_types import Destinations as IdfmDestinations, IdfmResponse, IdfmState
from db import Database
from models import Line, Stop, StopArea
class IdfmInterface: class IdfmInterface:

View File

@@ -12,7 +12,7 @@ from opentelemetry.sdk.resources import Resource, SERVICE_NAME
from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.sdk.trace.export import BatchSpanProcessor
from backend.db import db from db import db
from dependencies import idfm_interface, redis_backend, settings from dependencies import idfm_interface, redis_backend, settings
from routers import line, stop from routers import line, stop
@@ -34,13 +34,12 @@ app = FastAPI(lifespan=lifespan)
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["https://localhost:4443", "https://localhost:3000"], allow_origins=["http://carrramba.adrien.run", "https://carrramba.adrien.run"],
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["OPTIONS", "GET"],
allow_headers=["*"], allow_headers=["*"],
) )
# The cache-control header entry is not managed properly by fastapi-cache: # The cache-control header entry is not managed properly by fastapi-cache:
# For now, a request with a cache-control set to no-cache # For now, a request with a cache-control set to no-cache
# is interpreted as disabling the use of the server cache. # is interpreted as disabling the use of the server cache.

View File

@@ -14,8 +14,8 @@ from sqlalchemy import (
from sqlalchemy.orm import Mapped, mapped_column, relationship, selectinload from sqlalchemy.orm import Mapped, mapped_column, relationship, selectinload
from sqlalchemy.sql.expression import tuple_ from sqlalchemy.sql.expression import tuple_
from ..db import Base, db from db import Base, db
from ..idfm_interface.idfm_types import ( from idfm_interface.idfm_types import (
IdfmState, IdfmState,
IdfmLineState, IdfmLineState,
TransportMode, TransportMode,

View File

@@ -26,8 +26,8 @@ from sqlalchemy.orm import (
from sqlalchemy.schema import Index from sqlalchemy.schema import Index
from sqlalchemy_utils.types.ts_vector import TSVectorType from sqlalchemy_utils.types.ts_vector import TSVectorType
from ..db import Base, db from db import Base, db
from ..idfm_interface.idfm_types import TransportMode, IdfmState, StopAreaType from idfm_interface.idfm_types import TransportMode, IdfmState, StopAreaType
if TYPE_CHECKING: if TYPE_CHECKING:
from .line import Line from .line import Line

View File

@@ -1,7 +1,7 @@
from sqlalchemy import BigInteger, ForeignKey, String from sqlalchemy import BigInteger, ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from ..db import Base, db from db import Base, db
from .stop import _Stop from .stop import _Stop

View File

@@ -1,8 +1,8 @@
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from fastapi_cache.decorator import cache from fastapi_cache.decorator import cache
from backend.models import Line from models import Line
from backend.schemas import Line as LineSchema, TransportMode from schemas import Line as LineSchema, TransportMode
router = APIRouter(prefix="/line", tags=["line"]) router = APIRouter(prefix="/line", tags=["line"])

View File

@@ -5,9 +5,9 @@ from typing import Sequence
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from fastapi_cache.decorator import cache from fastapi_cache.decorator import cache
from backend.idfm_interface import Destinations as IdfmDestinations, TrainStatus from idfm_interface import Destinations as IdfmDestinations, TrainStatus
from backend.models import Stop, StopArea, StopShape from models import Stop, StopArea, StopShape
from backend.schemas import ( from schemas import (
NextPassage as NextPassageSchema, NextPassage as NextPassageSchema,
NextPassages as NextPassagesSchema, NextPassages as NextPassagesSchema,
Stop as StopSchema, Stop as StopSchema,

View File

@@ -2,7 +2,7 @@ from enum import StrEnum
from pydantic import BaseModel from pydantic import BaseModel
from ..idfm_interface import ( from idfm_interface import (
IdfmLineState, IdfmLineState,
IdfmState, IdfmState,
TransportMode as IdfmTransportMode, TransportMode as IdfmTransportMode,

View File

@@ -1,6 +1,6 @@
from pydantic import BaseModel from pydantic import BaseModel
from ..idfm_interface.idfm_types import TrainStatus from idfm_interface.idfm_types import TrainStatus
class NextPassage(BaseModel): class NextPassage(BaseModel):

View File

@@ -1,6 +1,6 @@
from pydantic import BaseModel from pydantic import BaseModel
from ..idfm_interface import StopAreaType from idfm_interface import StopAreaType
class Stop(BaseModel): class Stop(BaseModel):

74
backend/api/settings.py Normal file
View File

@@ -0,0 +1,74 @@
from __future__ import annotations
from typing import Annotated
from pydantic import BaseModel, SecretStr
from pydantic.functional_validators import model_validator
from pydantic_settings import (
BaseSettings,
PydanticBaseSettingsSource,
SettingsConfigDict,
)
class HttpSettings(BaseModel):
host: str = "127.0.0.1"
port: int = 8080
cert: str | None = None
class DatabaseSettings(BaseModel):
name: str
host: str
port: int
driver: str = "postgresql+psycopg"
user: str
password: Annotated[SecretStr, check_user_password]
class CacheSettings(BaseModel):
enable: bool = False
host: str = "127.0.0.1"
port: int = 6379
user: str | None = None
password: Annotated[SecretStr | None, check_user_password] = None
@model_validator(mode="after")
def check_user_password(self) -> DatabaseSettings | CacheSettings:
if self.user is not None and self.password is None:
raise ValueError("user is set, password shall be set too.")
if self.password is not None and self.user is None:
raise ValueError("password is set, user shall be set too.")
return self
class TracingSettings(BaseModel):
enable: bool = False
class Settings(BaseSettings):
app_name: str
idfm_api_key: SecretStr
clear_static_data: bool
http: HttpSettings
db: DatabaseSettings
cache: CacheSettings
tracing: TracingSettings
model_config = SettingsConfigDict(env_prefix="CER__", env_nested_delimiter="__")
@classmethod
def settings_customise_sources(
cls,
settings_cls: type[BaseSettings],
init_settings: PydanticBaseSettingsSource,
env_settings: PydanticBaseSettingsSource,
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource,
) -> tuple[PydanticBaseSettingsSource, ...]:
return env_settings, init_settings, file_secret_settings

View File

@@ -1,59 +0,0 @@
from typing import Any
from pydantic import BaseModel, BaseSettings, Field, root_validator, SecretStr
class HttpSettings(BaseModel):
host: str = "127.0.0.1"
port: int = 8080
cert: str | None = None
def check_user_password(cls, values: dict[str, Any]) -> dict[str, Any]:
user = values.get("user")
password = values.get("password")
if user is not None and password is None:
raise ValueError("user is set, password shall be set too.")
if password is not None and user is None:
raise ValueError("password is set, user shall be set too.")
return values
class DatabaseSettings(BaseModel):
name: str = "carrramba-encore-rate"
host: str = "127.0.0.1"
port: int = 5432
driver: str = "postgresql+psycopg"
user: str | None = None
password: SecretStr | None = None
_user_password_validation = root_validator(allow_reuse=True)(check_user_password)
class CacheSettings(BaseModel):
enable: bool = False
host: str = "127.0.0.1"
port: int = 6379
user: str | None = None
password: SecretStr | None = None
_user_password_validation = root_validator(allow_reuse=True)(check_user_password)
class TracingSettings(BaseModel):
enable: bool = False
class Settings(BaseSettings):
app_name: str
idfm_api_key: SecretStr = Field(..., env="IDFM_API_KEY")
clear_static_data: bool = Field(False, env="CLEAR_STATIC_DATA")
http: HttpSettings = HttpSettings()
db: DatabaseSettings = DatabaseSettings()
cache: CacheSettings = CacheSettings()
tracing: TracingSettings = TracingSettings()

View File

@@ -16,9 +16,9 @@ from shapefile import Reader as ShapeFileReader, ShapeRecord # type: ignore
from tqdm import tqdm from tqdm import tqdm
from yaml import safe_load from yaml import safe_load
from backend.db import Base, db, Database from api.db import Base, db, Database
from backend.models import ConnectionArea, Line, LinePicto, Stop, StopArea, StopShape from api.models import ConnectionArea, Line, LinePicto, Stop, StopArea, StopShape
from backend.idfm_interface.idfm_types import ( from api.idfm_interface.idfm_types import (
ConnectionArea as IdfmConnectionArea, ConnectionArea as IdfmConnectionArea,
IdfmLineState, IdfmLineState,
Line as IdfmLine, Line as IdfmLine,
@@ -31,8 +31,8 @@ from backend.idfm_interface.idfm_types import (
StopLineAsso as IdfmStopLineAsso, StopLineAsso as IdfmStopLineAsso,
TransportMode, TransportMode,
) )
from backend.idfm_interface.ratp_types import Picto as RatpPicto from api.idfm_interface.ratp_types import Picto as RatpPicto
from backend.settings import Settings from api.settings import Settings
CONFIG_PATH = environ.get("CONFIG_PATH", "./config.sample.yaml") CONFIG_PATH = environ.get("CONFIG_PATH", "./config.sample.yaml")

View File

@@ -9,7 +9,7 @@ readme = "README.md"
python = "^3.11" python = "^3.11"
aiohttp = "^3.8.3" aiohttp = "^3.8.3"
aiofiles = "^22.1.0" aiofiles = "^22.1.0"
fastapi = "^0.95.0" fastapi = "^0.103.0"
uvicorn = "^0.20.0" uvicorn = "^0.20.0"
msgspec = "^0.12.0" msgspec = "^0.12.0"
opentelemetry-instrumentation-fastapi = "^0.38b0" opentelemetry-instrumentation-fastapi = "^0.38b0"
@@ -23,11 +23,12 @@ sqlalchemy = "^2.0.12"
psycopg = "^3.1.9" psycopg = "^3.1.9"
pyyaml = "^6.0" pyyaml = "^6.0"
fastapi-cache2 = {extras = ["redis"], version = "^0.2.1"} fastapi-cache2 = {extras = ["redis"], version = "^0.2.1"}
pydantic-settings = "^2.0.3"
[tool.poetry.group.db_updater.dependencies] [tool.poetry.group.db_updater.dependencies]
aiofiles = "^22.1.0" aiofiles = "^22.1.0"
aiohttp = "^3.8.3" aiohttp = "^3.8.3"
fastapi = "^0.95.0" fastapi = "^0.103.0"
msgspec = "^0.12.0" msgspec = "^0.12.0"
opentelemetry-instrumentation-fastapi = "^0.38b0" opentelemetry-instrumentation-fastapi = "^0.38b0"
opentelemetry-instrumentation-sqlalchemy = "^0.38b0" opentelemetry-instrumentation-sqlalchemy = "^0.38b0"
@@ -41,11 +42,14 @@ pyyaml = "^6.0"
sqlalchemy = "^2.0.12" sqlalchemy = "^2.0.12"
sqlalchemy-utils = "^0.41.1" sqlalchemy-utils = "^0.41.1"
tqdm = "^4.65.0" tqdm = "^4.65.0"
pydantic-settings = "^2.0.3"
[build-system] [build-system]
requires = ["poetry-core"] requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
pylsp-mypy = "^0.6.2" pylsp-mypy = "^0.6.2"
mccabe = "^0.7.0" mccabe = "^0.7.0"

View File

@@ -28,8 +28,7 @@ export interface BusinessDataStore {
export const BusinessDataContext = createContext<BusinessDataStore>(); export const BusinessDataContext = createContext<BusinessDataStore>();
export function BusinessDataProvider(props: { children: JSX.Element }) { export function BusinessDataProvider(props: { children: JSX.Element }) {
const [serverUrl] = createSignal<string>("https://carrramba.adrien.run/api");
const [serverUrl] = createSignal<string>("https://localhost:4443");
type Store = { type Store = {
lines: Lines; lines: Lines;

View File

@@ -26,7 +26,7 @@ export const TransportModeWeights: Record<string, number> = {
export function getTransportModeSrc(mode: string, color: boolean = true): string | undefined { export function getTransportModeSrc(mode: string, color: boolean = true): string | undefined {
let ret = undefined; let ret = undefined;
if (validTransportModes.includes(mode)) { if (validTransportModes.includes(mode)) {
return `/carrramba-encore-rate/public/symbole_${mode}_${color ? "" : "support_fonce_"}RVB.svg`; return `/symbole_${mode}_${color ? "" : "support_fonce_"}RVB.svg`;
} }
return ret; return ret;
} }