Migrate location recorder and refine db config
This commit is contained in:
@@ -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
@@ -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()
|
||||
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
"""SQLAlchemy models package."""
|
||||
|
||||
from app.models.location import Location
|
||||
|
||||
__all__ = ["Location"]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user