feat: Initialize frontend and backend structure with essential configurations
Some checks failed
Backend CI / lint-and-test (push) Failing after 2m15s
Frontend CI / lint-and-build (push) Successful in 1m1s

- Added TypeScript build info for frontend.
- Created Vite configuration for React application.
- Implemented pre-commit hook to run checks before commits.
- Set up PostgreSQL Dockerfile with PostGIS support and initialization scripts.
- Added database creation script for PostgreSQL with necessary extensions.
- Established Python project configuration with dependencies and development tools.
- Developed pre-commit script to enforce code quality checks for backend and frontend.
- Created PowerShell script to set up Git hooks path.
This commit is contained in:
2025-10-11 15:25:32 +02:00
commit fc1e874309
74 changed files with 9477 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
"""Repository abstractions for database access."""
from backend.app.repositories.stations import StationRepository
from backend.app.repositories.tracks import TrackRepository
from backend.app.repositories.trains import TrainRepository
__all__ = [
"StationRepository",
"TrackRepository",
"TrainRepository",
]

View File

@@ -0,0 +1,39 @@
from __future__ import annotations
import sqlalchemy as sa
from typing import Generic, Iterable, Optional, Sequence, Type, TypeVar
from sqlalchemy.orm import Session
from backend.app.db.models import Base
ModelT = TypeVar("ModelT", bound=Base)
class BaseRepository(Generic[ModelT]):
"""Provide common CRUD helpers for SQLAlchemy models."""
model: Type[ModelT]
def __init__(self, session: Session) -> None:
self.session = session
def get(self, identifier: object) -> Optional[ModelT]:
return self.session.get(self.model, identifier)
def list(self) -> Sequence[ModelT]:
statement = sa.select(self.model)
return list(self.session.scalars(statement))
def add(self, instance: ModelT) -> ModelT:
self.session.add(instance)
return instance
def add_all(self, instances: Iterable[ModelT]) -> None:
self.session.add_all(instances)
def delete(self, instance: ModelT) -> None:
self.session.delete(instance)
def flush(self) -> None:
self.session.flush()

View File

@@ -0,0 +1,37 @@
from __future__ import annotations
import sqlalchemy as sa
from sqlalchemy.orm import Session
from geoalchemy2.elements import WKTElement
from backend.app.db.models import Station
from backend.app.repositories.base import BaseRepository
from backend.app.models import StationCreate
class StationRepository(BaseRepository[Station]):
model = Station
def __init__(self, session: Session) -> None:
super().__init__(session)
def list_active(self) -> list[Station]:
statement = sa.select(self.model).where(self.model.is_active.is_(True))
return list(self.session.scalars(statement))
@staticmethod
def _point(latitude: float, longitude: float) -> WKTElement:
return WKTElement(f"POINT({longitude} {latitude})", srid=4326)
def create(self, data: StationCreate) -> Station:
station = Station(
name=data.name,
osm_id=data.osm_id,
code=data.code,
location=self._point(data.latitude, data.longitude),
elevation_m=data.elevation_m,
is_active=data.is_active,
)
self.session.add(station)
return station

View File

@@ -0,0 +1,50 @@
from __future__ import annotations
import sqlalchemy as sa
from geoalchemy2.elements import WKTElement
from uuid import UUID
from sqlalchemy.orm import Session
from backend.app.db.models import Track
from backend.app.repositories.base import BaseRepository
from backend.app.models import TrackCreate
class TrackRepository(BaseRepository[Track]):
model = Track
def __init__(self, session: Session) -> None:
super().__init__(session)
def list_all(self) -> list[Track]:
statement = sa.select(self.model)
return list(self.session.scalars(statement))
@staticmethod
def _ensure_uuid(value: UUID | str) -> UUID:
if isinstance(value, UUID):
return value
return UUID(str(value))
@staticmethod
def _line_string(coordinates: list[tuple[float, float]]) -> WKTElement:
if len(coordinates) < 2:
raise ValueError("Track geometry requires at least two coordinate pairs")
parts = [f"{lon} {lat}" for lat, lon in coordinates]
return WKTElement(f"LINESTRING({', '.join(parts)})", srid=4326)
def create(self, data: TrackCreate) -> Track:
coordinates = list(data.coordinates)
geometry = self._line_string(coordinates)
track = Track(
name=data.name,
start_station_id=self._ensure_uuid(data.start_station_id),
end_station_id=self._ensure_uuid(data.end_station_id),
length_meters=data.length_meters,
max_speed_kph=data.max_speed_kph,
is_bidirectional=data.is_bidirectional,
status=data.status,
track_geometry=geometry,
)
self.session.add(track)
return track

View File

@@ -0,0 +1,40 @@
from __future__ import annotations
import sqlalchemy as sa
from uuid import UUID
from sqlalchemy.orm import Session
from backend.app.db.models import Train
from backend.app.repositories.base import BaseRepository
from backend.app.models import TrainCreate
class TrainRepository(BaseRepository[Train]):
model = Train
def __init__(self, session: Session) -> None:
super().__init__(session)
def list_all(self) -> list[Train]:
statement = sa.select(self.model)
return list(self.session.scalars(statement))
@staticmethod
def _optional_uuid(value: UUID | str | None) -> UUID | None:
if value is None:
return None
if isinstance(value, UUID):
return value
return UUID(str(value))
def create(self, data: TrainCreate) -> Train:
train = Train(
designation=data.designation,
operator_id=self._optional_uuid(data.operator_id),
home_station_id=self._optional_uuid(data.home_station_id),
capacity=data.capacity,
max_speed_kph=data.max_speed_kph,
consist=data.consist,
)
self.session.add(train)
return train