Migrate location recorder and refine db config

This commit is contained in:
2026-04-19 21:39:23 +02:00
parent 31390882ef
commit 32cc6847fd
19 changed files with 507 additions and 31 deletions
+28
View File
@@ -0,0 +1,28 @@
import json
from fastapi import APIRouter, Depends, Request
from fastapi.responses import PlainTextResponse, Response
from pydantic import ValidationError
from sqlalchemy.orm import Session
from app.dependencies import get_db
from app.schemas.location import LocationRecordRequest
from app.services.location import record_location
router = APIRouter(tags=["location"])
@router.post("/location/record")
async def create_location_record(request: Request, db: Session = Depends(get_db)) -> Response:
try:
raw_payload = await request.body()
data = json.loads(raw_payload)
payload = LocationRecordRequest.model_validate(data)
except json.JSONDecodeError as exc:
return PlainTextResponse(str(exc), status_code=400)
except ValidationError as exc:
return PlainTextResponse(str(exc), status_code=400)
record_location(db, payload)
return Response(status_code=200)
+17 -8
View File
@@ -12,7 +12,8 @@ class Settings(BaseSettings):
app_host: str = "0.0.0.0"
app_port: int = 8000
database_url: str = "sqlite:///./data/app.db"
location_database_url: str = "sqlite:///./data/locationRecorder.db"
poo_database_url: str = "sqlite:///./data/pooRecorder.db"
ticktick_client_id: str = ""
ticktick_client_secret: str = ""
@@ -34,17 +35,25 @@ class Settings(BaseSettings):
def is_development(self) -> bool:
return self.app_env.lower() == "development"
@staticmethod
def _sqlite_path_from_url(database_url: str) -> Path | None:
prefix = "sqlite:///"
if not database_url.startswith(prefix):
return None
raw_path = database_url[len(prefix) :]
return Path(raw_path)
@computed_field
@property
def sqlite_path(self) -> Path | None:
prefix = "sqlite:///"
if not self.database_url.startswith(prefix):
return None
raw_path = self.database_url[len(prefix) :]
return Path(raw_path)
def location_sqlite_path(self) -> Path | None:
return self._sqlite_path_from_url(self.location_database_url)
@computed_field
@property
def poo_sqlite_path(self) -> Path | None:
return self._sqlite_path_from_url(self.poo_database_url)
@lru_cache
def get_settings() -> Settings:
return Settings()
+2 -3
View File
@@ -13,10 +13,10 @@ class Base(DeclarativeBase):
settings = get_settings()
connect_args: dict[str, object] = {}
if settings.database_url.startswith("sqlite"):
if settings.location_database_url.startswith("sqlite"):
connect_args["check_same_thread"] = False
engine = create_engine(settings.database_url, connect_args=connect_args)
engine = create_engine(settings.location_database_url, connect_args=connect_args)
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, class_=Session)
@@ -26,4 +26,3 @@ def get_db_session() -> Generator[Session, None, None]:
yield session
finally:
session.close()
+6 -3
View File
@@ -4,14 +4,17 @@ from pathlib import Path
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from app import models # noqa: F401
from app.api.routes import pages, status
from app.api.routes.location import router as location_router
from app.config import get_settings
def ensure_runtime_dirs() -> None:
settings = get_settings()
if settings.sqlite_path is not None:
settings.sqlite_path.parent.mkdir(parents=True, exist_ok=True)
for path in (settings.location_sqlite_path, settings.poo_sqlite_path):
if path is not None:
path.parent.mkdir(parents=True, exist_ok=True)
@asynccontextmanager
@@ -38,8 +41,8 @@ def create_app() -> FastAPI:
app.include_router(status.router)
app.include_router(pages.router)
app.include_router(location_router)
return app
app = create_app()
+3
View File
@@ -1,2 +1,5 @@
"""SQLAlchemy models package."""
from app.models.location import Location
__all__ = ["Location"]
+15
View File
@@ -0,0 +1,15 @@
from sqlalchemy import Float, String
from sqlalchemy.orm import Mapped, mapped_column
from app.db import Base
class Location(Base):
__tablename__ = "location"
person: Mapped[str] = mapped_column(String, primary_key=True)
datetime: Mapped[str] = mapped_column(String, primary_key=True)
latitude: Mapped[float] = mapped_column(Float, nullable=False)
longitude: Mapped[float] = mapped_column(Float, nullable=False)
altitude: Mapped[float | None] = mapped_column(Float, nullable=True)
+11
View File
@@ -0,0 +1,11 @@
from pydantic import BaseModel, ConfigDict
class LocationRecordRequest(BaseModel):
person: str
latitude: str
longitude: str
altitude: str = ""
model_config = ConfigDict(extra="forbid")
+36
View File
@@ -0,0 +1,36 @@
from datetime import datetime, timezone
from sqlalchemy import insert
from sqlalchemy.orm import Session
from app.models.location import Location
from app.schemas.location import LocationRecordRequest
def _parse_float_compat(value: str) -> float:
try:
return float(value)
except (TypeError, ValueError):
return 0.0
def _utc_now_rfc3339() -> str:
now = datetime.now(timezone.utc).replace(microsecond=0)
return now.isoformat().replace("+00:00", "Z")
def record_location(session: Session, payload: LocationRecordRequest) -> None:
stmt = (
insert(Location)
.prefix_with("OR IGNORE")
.values(
person=payload.person,
datetime=_utc_now_rfc3339(),
latitude=_parse_float_compat(payload.latitude),
longitude=_parse_float_compat(payload.longitude),
altitude=_parse_float_compat(payload.altitude),
)
)
session.execute(stmt)
session.commit()