Files
guarddog-nexus/guarddog_nexus/main.py
Marker689 c1258dde99 refactor: FastAPI best practices — return types, Pydantic schemas, middleware, code dedup
- Все 18 роутов получили return type annotations
- Создан schemas.py с Pydantic-моделями (ScanOut, PackageOut, FindingOut, ...)
- API-роуты: response_model на list/detail/export/stats
- 404 через HTTPException(404) вместо {'detail':'Not found'} (200)
- RequestLoggingMiddleware: method, path, status, duration_ms
- Глобальный exception handler: ловит необработанные исключения → 500
- _parse_flagged(): вынесен дублирующийся string→bool
- parse_package_path(): общий для web.py и api_packages.py
- selectinload: вынесены в top-level imports
- harvester: makedirs/mkdtemp/rmtree обёрнуты в asyncio.to_thread()
2026-05-10 12:53:33 +03:00

120 lines
3.4 KiB
Python

"""GuardDog Nexus — FastAPI application entry point."""
import os
import time
from contextlib import asynccontextmanager
import uvicorn
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from starlette.middleware.base import BaseHTTPMiddleware
from guarddog_nexus.config import config
from guarddog_nexus.constants import (
APP_DESCRIPTION,
APP_NAME,
APP_PACKAGE,
APP_VERSION,
STATIC_MOUNT_PATH,
)
from guarddog_nexus.db.engine import init_db
from guarddog_nexus.i18n import DEFAULT_LANG, LANGUAGES
from guarddog_nexus.logging_setup import log
from guarddog_nexus.routes import api_findings, api_packages, api_scans
from guarddog_nexus.routes.metrics import router as metrics_router
from guarddog_nexus.routes.web import router as web_router
from guarddog_nexus.routes.webhooks import router as webhook_router
STATIC_DIR = os.path.join(os.path.dirname(__file__), "web", "static")
class LangMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# Cookie takes priority, then query param, then default
cookie_lang = request.cookies.get("lang")
query_lang = request.query_params.get("lang")
if query_lang and query_lang in LANGUAGES and query_lang != cookie_lang:
lang = query_lang
elif cookie_lang and cookie_lang in LANGUAGES:
lang = cookie_lang
else:
lang = DEFAULT_LANG
request.state.lang = lang
response = await call_next(request)
if query_lang and query_lang in LANGUAGES:
response.set_cookie("lang", query_lang, max_age=365 * 24 * 3600, httponly=True)
return response
@asynccontextmanager
async def lifespan(app: FastAPI):
await init_db()
log.info("%s started on %s:%s", APP_NAME, config.host, config.port)
yield
log.info("%s shutting down", APP_NAME)
class RequestLoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
start = time.monotonic()
response = await call_next(request)
duration = (time.monotonic() - start) * 1000
log.info(
"%s %s %s %.1fms",
request.method,
request.url.path,
response.status_code,
duration,
)
return response
app = FastAPI(
title=APP_NAME,
version=APP_VERSION,
description=APP_DESCRIPTION,
lifespan=lifespan,
)
app.add_middleware(LangMiddleware)
app.add_middleware(RequestLoggingMiddleware)
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
log.exception("Unhandled exception on %s %s", request.method, request.url.path)
return JSONResponse(status_code=500, content={"detail": "Internal server error"})
app.include_router(webhook_router)
app.include_router(metrics_router)
app.include_router(api_scans.router)
app.include_router(api_packages.router)
app.include_router(api_findings.router)
app.include_router(web_router)
if os.path.isdir(STATIC_DIR):
app.mount(STATIC_MOUNT_PATH, StaticFiles(directory=STATIC_DIR), name="static")
@app.get("/health")
async def health() -> dict:
return {"status": "ok", "version": APP_VERSION}
def main():
uvicorn.run(
f"{APP_PACKAGE}.main:app",
host=config.host,
port=config.port,
log_level=config.log_level.lower(),
reload=False,
)
if __name__ == "__main__":
main()