Author SHA1 Message Date
Adrien bdbc72ab39 🐛 Front: Fix URL used to fetch transport mode representation 2023-09-10 12:25:38 +02:00
Adrien 4cc8f60076 🐛 Front: Use the public API server to fetch data 2023-09-10 12:17:48 +02:00
Adrien cf5c4c6224 🔒️ Fix CORS allowed origins and methods 2023-09-10 12:07:20 +02:00
Adrien 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
Adrien 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
Adrien 4fce832db5 ♻️ Rename docker file building api image 2023-09-10 11:45:08 +02:00
Adrien bfc669cd11 ♻️ Use pydantic-settings to handle config file 2023-09-09 23:35:18 +02:00
Adrien 4056b3a739 🐛 Error raised by frontend Map component if no stop found 2023-09-09 23:18:03 +02:00
Adrien f7f0fdb980 ️ Use of integer to store Line and Stop id
Update Line and Stop schemas.
2023-09-09 23:05:18 +02:00
Adrien 6c149e844b 💥 Remove /widget static endpoint
This endpoint shall be served by a dedicated static HTTP server.
2023-06-13 05:45:33 +02:00
Adrien f5529bba24 Merge branch 'remove-db-filling-from-backend' into develop 2023-06-13 05:44:00 +02:00
13 changed files with 74 additions and 59 deletions
-3
View File
@@ -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)
@@ -31,6 +31,10 @@ class IdfmInterface:
async def startup(self) -> None: async def startup(self) -> None:
... ...
@staticmethod
def _format_line_id(line_id: str) -> int:
return int(line_id[1:] if line_id[0] == "C" else line_id)
async def render_line_picto(self, line: Line) -> tuple[None | str, None | str]: async def render_line_picto(self, line: Line) -> tuple[None | str, None | str]:
line_picto_path = line_picto_format = None line_picto_path = line_picto_format = None
target = f"/tmp/{line.id}_repr" target = f"/tmp/{line.id}_repr"
@@ -81,7 +85,6 @@ class IdfmInterface:
if (stop := await Stop.get_by_id(stop_id)) is not None: if (stop := await Stop.get_by_id(stop_id)) is not None:
expected_stop_ids = {stop.id} expected_stop_ids = {stop.id}
elif (stop_area := await StopArea.get_by_id(stop_id)) is not None: elif (stop_area := await StopArea.get_by_id(stop_id)) is not None:
expected_stop_ids = {stop.id for stop in stop_area.stops} expected_stop_ids = {stop.id for stop in stop_area.stops}
else: else:
@@ -105,7 +108,8 @@ class IdfmInterface:
if ( if (
dst_names := journey.DestinationName dst_names := journey.DestinationName
) and monitored_stop_id in expected_stop_ids: ) and monitored_stop_id in expected_stop_ids:
line_id = journey.LineRef.value.split(":")[-2] raw_line_id = journey.LineRef.value.split(":")[-2]
line_id = IdfmInterface._format_line_id(raw_line_id)
destinations[line_id].add(dst_names[0].value) destinations[line_id].add(dst_names[0].value)
return destinations return destinations
+2 -2
View File
@@ -53,8 +53,8 @@ class Line(BaseModel):
transportMode: TransportMode transportMode: TransportMode
backColorHexa: str backColorHexa: str
foreColorHexa: str foreColorHexa: str
operatorId: str operatorId: int
accessibility: IdfmState accessibility: IdfmState
visualSignsAvailable: IdfmState visualSignsAvailable: IdfmState
audibleSignsAvailable: IdfmState audibleSignsAvailable: IdfmState
stopIds: list[str] stopIds: list[int]
+2 -2
View File
@@ -9,7 +9,7 @@ class Stop(BaseModel):
town: str town: str
epsg3857_x: float epsg3857_x: float
epsg3857_y: float epsg3857_y: float
lines: list[str] lines: list[int]
class StopArea(BaseModel): class StopArea(BaseModel):
@@ -17,7 +17,7 @@ class StopArea(BaseModel):
name: str name: str
town: str town: str
type: StopAreaType type: StopAreaType
lines: list[str] # SNCF lines are linked to stop areas and not stops. lines: list[int] # SNCF lines are linked to stop areas and not stops.
stops: list[Stop] stops: list[Stop]
+45 -30
View File
@@ -1,6 +1,14 @@
from typing import Any from __future__ import annotations
from pydantic import BaseModel, BaseSettings, Field, root_validator, SecretStr 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): class HttpSettings(BaseModel):
@@ -9,28 +17,13 @@ class HttpSettings(BaseModel):
cert: str | None = None 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): class DatabaseSettings(BaseModel):
name: str = "carrramba-encore-rate" name: str
host: str = "127.0.0.1" host: str
port: int = 5432 port: int
driver: str = "postgresql+psycopg" driver: str = "postgresql+psycopg"
user: str | None = None user: str
password: SecretStr | None = None password: Annotated[SecretStr, check_user_password]
_user_password_validation = root_validator(allow_reuse=True)(check_user_password)
class CacheSettings(BaseModel): class CacheSettings(BaseModel):
@@ -38,9 +31,18 @@ class CacheSettings(BaseModel):
host: str = "127.0.0.1" host: str = "127.0.0.1"
port: int = 6379 port: int = 6379
user: str | None = None user: str | None = None
password: SecretStr | None = None password: Annotated[SecretStr | None, check_user_password] = None
_user_password_validation = root_validator(allow_reuse=True)(check_user_password)
@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): class TracingSettings(BaseModel):
@@ -50,10 +52,23 @@ class TracingSettings(BaseModel):
class Settings(BaseSettings): class Settings(BaseSettings):
app_name: str app_name: str
idfm_api_key: SecretStr = Field(..., env="IDFM_API_KEY") idfm_api_key: SecretStr
clear_static_data: bool = Field(False, env="CLEAR_STATIC_DATA") clear_static_data: bool
http: HttpSettings = HttpSettings() http: HttpSettings
db: DatabaseSettings = DatabaseSettings() db: DatabaseSettings
cache: CacheSettings = CacheSettings() cache: CacheSettings
tracing: TracingSettings = TracingSettings() 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
-2
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
+2 -6
View File
@@ -4,7 +4,6 @@ import uvicorn
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi_cache import FastAPICache from fastapi_cache import FastAPICache
from opentelemetry import trace from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
@@ -35,14 +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=["*"],
) )
app.mount("/widget", StaticFiles(directory="../frontend/", html=True), name="widget")
# 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.
@@ -60,7 +57,6 @@ async def fastapi_cache_issue_144_workaround(request: Request, call_next):
return await call_next(request) return await call_next(request)
app.include_router(line.router) app.include_router(line.router)
app.include_router(stop.router) app.include_router(stop.router)
+6 -2
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"
+1 -2
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;
+2
View File
@@ -116,8 +116,10 @@ export const Map: ParentComponent<{}> = () => {
const foundStopIds = new Set(); const foundStopIds = new Set();
for (const foundStop of stops) { for (const foundStop of stops) {
foundStopIds.add(foundStop.id); foundStopIds.add(foundStop.id);
if (foundStop.stops !== undefined) {
foundStop.stops.forEach(s => foundStopIds.add(s.id)); foundStop.stops.forEach(s => foundStopIds.add(s.id));
} }
}
for (const [stopIdStr, feature] of Object.entries(displayedFeatures)) { for (const [stopIdStr, feature] of Object.entries(displayedFeatures)) {
const stopId = parseInt(stopIdStr); const stopId = parseInt(stopIdStr);
+6 -6
View File
@@ -8,7 +8,7 @@ export enum TrafficStatus {
export class Passage { export class Passage {
line: number; line: number;
operator: string; operator: number;
destinations: string[]; destinations: string[];
atStop: boolean; atStop: boolean;
aimedArrivalTs: number; aimedArrivalTs: number;
@@ -19,7 +19,7 @@ export class Passage {
arrivalStatus: string; arrivalStatus: string;
departStatus: string; departStatus: string;
constructor(line: number, operator: string, destinations: string[], atStop: boolean, aimedArrivalTs: number, constructor(line: number, operator: number, destinations: string[], atStop: boolean, aimedArrivalTs: number,
expectedArrivalTs: number, arrivalPlatformName: string, aimedDepartTs: number, expectedDepartTs: number, expectedArrivalTs: number, arrivalPlatformName: string, aimedDepartTs: number, expectedDepartTs: number,
arrivalStatus: string, departStatus: string) { arrivalStatus: string, departStatus: string) {
this.line = line; this.line = line;
@@ -45,9 +45,9 @@ export class Stop {
epsg3857_x: number; epsg3857_x: number;
epsg3857_y: number; epsg3857_y: number;
stops: Stop[]; stops: Stop[];
lines: string[]; lines: number[];
constructor(id: number, name: string, town: string, epsg3857_x: number, epsg3857_y: number, stops: Stop[], lines: string[]) { constructor(id: number, name: string, town: string, epsg3857_x: number, epsg3857_y: number, stops: Stop[], lines: number[]) {
this.id = id; this.id = id;
this.name = name; this.name = name;
this.town = town; this.town = town;
@@ -82,7 +82,7 @@ export class StopShape {
export type StopShapes = Record<number, StopShape>; export type StopShapes = Record<number, StopShape>;
export class Line { export class Line {
id: string; id: number;
shortName: string; shortName: string;
name: string; name: string;
status: string; // TODO: Use an enum status: string; // TODO: Use an enum
@@ -95,7 +95,7 @@ export class Line {
audibleSignsAvailable: string; // TODO: Use an enum audibleSignsAvailable: string; // TODO: Use an enum
stopIds: number[]; stopIds: number[];
constructor(id: string, shortName: string, name: string, status: string, transportMode: string, backColorHexa: string, constructor(id: number, shortName: string, name: string, status: string, transportMode: string, backColorHexa: string,
foreColorHexa: string, operatorId: number, accessibility: boolean, visualSignsAvailable: string, foreColorHexa: string, operatorId: number, accessibility: boolean, visualSignsAvailable: string,
audibleSignsAvailable: string, stopIds: number[]) { audibleSignsAvailable: string, stopIds: number[]) {
this.id = id; this.id = id;
+1 -1
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;
} }