feat(*): initial project from templates

This commit is contained in:
hh committed 2026-10-15 14:07:04 +02:00
commit 09ca301d51
70 files changed
+2184

No files matched your search

View File
Whitespace-only changes.
+17
View File
@@ -0,0 +1,17 @@
import uvicorn
from utils.env import env
def main() -> None:
uvicorn.run(
"api.app:app",
host=env.api.host,
port=env.api.port,
workers=env.api.workers,
forwarded_allow_ips="*",
)
if __name__ == "__main__":
main()
+45
View File
@@ -0,0 +1,45 @@
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from api import routers
from utils.env import env
from utils.logging import setup_logging
IMMUTABLE_HEADERS = {"Cache-Control": "public, max-age=31536000, immutable"}
NO_CACHE_HEADERS = {"Cache-Control": "no-cache"}
@asynccontextmanager
async def lifespan(_app: FastAPI) -> AsyncGenerator[None]:
setup_logging()
yield
app = FastAPI(
title="spotigrab API",
lifespan=lifespan,
openapi_url="/openapi.json" if env.api.docs else None,
)
app.add_middleware(
CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]
)
app.include_router(routers.router)
spa_dir = Path(env.api.static_dir).resolve()
if spa_dir.is_dir():
@app.get("/{spa_path:path}")
async def serve_spa(spa_path: str) -> FileResponse:
candidate = (spa_dir / spa_path).resolve()
if spa_path and candidate.is_relative_to(spa_dir) and candidate.is_file():
immutable = spa_path.startswith("_app/immutable/")
headers = IMMUTABLE_HEADERS if immutable else NO_CACHE_HEADERS
return FileResponse(candidate, headers=headers)
return FileResponse(spa_dir / "index.html", headers=NO_CACHE_HEADERS)
Whitespace-only changes.
View File
Whitespace-only changes.
+6
View File
@@ -0,0 +1,6 @@
from fastapi import APIRouter
from . import api
router = APIRouter()
router.include_router(api.router, prefix="/api")
+6
View File
@@ -0,0 +1,6 @@
from fastapi import APIRouter
from . import health
router = APIRouter()
router.include_router(health.router, prefix="/health")
+9
View File
@@ -0,0 +1,9 @@
from fastapi import APIRouter
router = APIRouter(tags=["health"])
@router.get("")
@router.get("/")
async def health() -> dict[str, bool]:
return {"ok": True}
View File
Whitespace-only changes.
+4
View File
@@ -0,0 +1,4 @@
from .env import env
from .logging import logger
__all__ = ["env", "logger"]
+37
View File
@@ -0,0 +1,37 @@
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class Section(BaseSettings):
model_config = SettingsConfigDict(extra="ignore")
class LogSettings(Section):
level: str = "INFO"
level_external: str = "WARNING"
show_time: bool = False
show_locals: bool = False
console_width: int = 150
class ApiSettings(Section):
host: str = "0.0.0.0" # noqa: S104
port: int = 8080
workers: int = 1
docs: bool = False
static_dir: str = "static"
class Settings(BaseSettings):
log: LogSettings = Field(default_factory=LogSettings)
api: ApiSettings = Field(default_factory=ApiSettings)
model_config = SettingsConfigDict(
case_sensitive=False,
env_file=("../.env", ".env"),
env_nested_delimiter="__",
extra="ignore",
)
env = Settings()
+6
View File
@@ -0,0 +1,6 @@
import asyncio
from importlib import import_module
async def preload(module: str) -> None:
await asyncio.to_thread(import_module, module)
+33
View File
@@ -0,0 +1,33 @@
import logging
from rich.console import Console
from rich.logging import RichHandler
from rich.traceback import install
from .env import env
console = Console(width=env.log.console_width, color_system="auto", force_terminal=True)
def setup_logging() -> None:
logging.basicConfig(
level=env.log.level_external,
format="",
datefmt=None,
handlers=[
RichHandler(
console=console,
markup=True,
rich_tracebacks=True,
enable_link_path=False,
tracebacks_show_locals=env.log.show_locals,
omit_repeated_times=False,
show_time=env.log.show_time,
)
],
)
install(console=console, show_locals=env.log.show_locals)
logger = logging.getLogger("spotigrab")
logger.setLevel(env.log.level)