feat: bootstrap FastAPI configuration
This commit is contained in:
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
36
app/config.py
Normal file
36
app/config.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from functools import lru_cache
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from pydantic import Field, PositiveInt, model_validator
|
||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
model_config = SettingsConfigDict(env_file=".env", extra="ignore", case_sensitive=False)
|
||||||
|
|
||||||
|
admin_password: str = Field(alias="ADMIN_PASSWORD", min_length=1)
|
||||||
|
secret_key: str = Field(alias="SECRET_KEY", min_length=16)
|
||||||
|
credential_encryption_key: str = Field(alias="CREDENTIAL_ENCRYPTION_KEY", min_length=1)
|
||||||
|
data_dir: Path = Field(default=Path("/data"), alias="DATA_DIR")
|
||||||
|
database_url: str | None = Field(default=None, alias="DATABASE_URL")
|
||||||
|
sync_interval_minutes: PositiveInt = Field(default=5, alias="SYNC_INTERVAL_MINUTES")
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def derive_paths(self) -> "Settings":
|
||||||
|
self.data_dir = self.data_dir.expanduser().resolve()
|
||||||
|
if self.database_url is None:
|
||||||
|
self.database_url = f"sqlite:///{self.data_dir / 'app.db'}"
|
||||||
|
return self
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tokens_dir(self) -> Path:
|
||||||
|
return self.data_dir / "tokens"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def activities_dir(self) -> Path:
|
||||||
|
return self.data_dir / "activities"
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache
|
||||||
|
def get_settings() -> Settings:
|
||||||
|
return Settings()
|
||||||
22
app/main.py
Normal file
22
app/main.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
from app.config import Settings, get_settings
|
||||||
|
|
||||||
|
|
||||||
|
def create_app(settings: Settings | None = None) -> FastAPI:
|
||||||
|
resolved = settings or get_settings()
|
||||||
|
resolved.data_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
resolved.tokens_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
resolved.activities_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
app = FastAPI(title="MyWhoosh Garmin Sync")
|
||||||
|
app.state.settings = resolved
|
||||||
|
|
||||||
|
@app.get("/healthz")
|
||||||
|
def healthz() -> dict[str, str]:
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
29
pyproject.toml
Normal file
29
pyproject.toml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=70", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "mywhoosh-garmin-sync"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"fastapi>=0.115,<1",
|
||||||
|
"uvicorn[standard]>=0.30,<1",
|
||||||
|
"sqlalchemy>=2.0,<3",
|
||||||
|
"pydantic-settings>=2.0,<3",
|
||||||
|
"cryptography>=43,<50",
|
||||||
|
"jinja2>=3.1,<4",
|
||||||
|
"python-multipart>=0.0.9,<1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
test = [
|
||||||
|
"pytest>=8,<9",
|
||||||
|
"httpx>=0.27,<1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.setuptools.packages]
|
||||||
|
find = { where = ["."], include = ["app*"] }
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
0
tests/conftest.py
Normal file
0
tests/conftest.py
Normal file
32
tests/test_config.py
Normal file
32
tests/test_config.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from app.config import Settings
|
||||||
|
|
||||||
|
|
||||||
|
def test_settings_build_default_data_paths(tmp_path: Path) -> None:
|
||||||
|
settings = Settings(
|
||||||
|
ADMIN_PASSWORD="admin-secret",
|
||||||
|
SECRET_KEY="session-secret-16chars",
|
||||||
|
CREDENTIAL_ENCRYPTION_KEY="ZmFrZS1rZXktZm9yLXRlc3RzLW11c3QtYmUtNDQtY2hhcnM=",
|
||||||
|
DATA_DIR=str(tmp_path),
|
||||||
|
SYNC_INTERVAL_MINUTES=5,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert settings.data_dir == tmp_path
|
||||||
|
assert settings.database_url == f"sqlite:///{tmp_path / 'app.db'}"
|
||||||
|
assert settings.tokens_dir == tmp_path / "tokens"
|
||||||
|
assert settings.activities_dir == tmp_path / "activities"
|
||||||
|
|
||||||
|
|
||||||
|
def test_sync_interval_must_be_positive(tmp_path: Path) -> None:
|
||||||
|
try:
|
||||||
|
Settings(
|
||||||
|
ADMIN_PASSWORD="admin-secret",
|
||||||
|
SECRET_KEY="session-secret-16chars",
|
||||||
|
CREDENTIAL_ENCRYPTION_KEY="ZmFrZS1rZXktZm9yLXRlc3RzLW11c3QtYmUtNDQtY2hhcnM=",
|
||||||
|
DATA_DIR=str(tmp_path),
|
||||||
|
SYNC_INTERVAL_MINUTES=0,
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
return
|
||||||
|
raise AssertionError("Expected validation failure for non-positive interval")
|
||||||
Reference in New Issue
Block a user