/
ivanstrike
/
tasker
Обзор
Документация
Войти
/
ivanstrike
/
tasker
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
python_Sklyarenko/Lab1/app/main.py
181 строка
6 KB
ivanstrike
Lab3
13 дек 2025, 11:15
13 дек 2025, 11:15
15c0fa5
Код
Авторство
О чём код?
# app/main.py from __future__ import annotations import time from collections import defaultdict from fastapi import FastAPI, Request, Response from fastapi.middleware.cors import CORSMiddleware from fastapi.openapi.utils import get_openapi from starlette.middleware.base import BaseHTTPMiddleware from .database import Base, engine from . import models # noqa: F401 # чтобы таблицы создались from .auth import router as auth_router from .routers.v1.projects import router as projects_v1 from .routers.v1.tasks import router as tasks_v1 from .routers.v2.tasks import router as tasks_v2 # --------------------------- # Инициализация БД # --------------------------- Base.metadata.create_all(bind=engine) # --------------------------- # Приложение # --------------------------- app = FastAPI( title="Task API", version="2025.10", description="Учебный REST API: JWT, идемпотентность, rate limiting и версионность (v1/v2).", openapi_tags=[ {"name": "auth", "description": "Регистрация и логин (JWT)"}, {"name": "projects", "description": "CRUD для проектов (v1)"}, {"name": "tasks", "description": "CRUD для задач (v1/v2)"}, ], ) # --------------------------- # CORS (демо-настройки) # --------------------------- app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # --------------------------- # Rate limiting (простой in-memory) # По умолчанию считаем по IP клиента. # В проде лучше Redis/SlowAPI. # --------------------------- RATE_LIMIT = 60 # запросов WINDOW_SECONDS = 60 # в окне 60 сек _rate_store = defaultdict(lambda: {"count": 0, "ts": 0.0}) class RateLimitMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): key = request.client.host or "anonymous" now = time.time() rec = _rate_store[key] # Сброс окна if now - rec["ts"] >= WINDOW_SECONDS: rec["ts"] = now rec["count"] = 0 # Превышение — 429 и Retry-After if rec["count"] >= RATE_LIMIT: retry_after = int(max(0, WINDOW_SECONDS - (now - rec["ts"]))) return Response( content='{"detail":"Too Many Requests"}', status_code=429, media_type="application/json", headers={ "X-Limit-Remaining": "0", "Retry-After": str(retry_after), }, ) rec["count"] += 1 response = await call_next(request) remaining = max(0, RATE_LIMIT - rec["count"]) # Служебные заголовки в каждый ответ response.headers["X-Limit-Remaining"] = str(remaining) # 0 — можно сразу, иначе — сколько ждать до следующего окна response.headers["Retry-After"] = "0" return response app.add_middleware(RateLimitMiddleware) # --------------------------- # Роутеры # --------------------------- app.include_router(auth_router) app.include_router(projects_v1) app.include_router(tasks_v1) app.include_router(tasks_v2) # --------------------------- # Health / Root # --------------------------- @app.get("/ping", tags=["root"]) def ping(): return {"status": "ok"} @app.get("/", tags=["root"]) def root(): return {"message": "Task API up", "versions": ["/api/v1", "/api/v2"]} # --------------------------- # OpenAPI: схема Bearer (JWT) # Глобально требуем токен, кроме /auth/register и /auth/login. # --------------------------- def custom_openapi(): if app.openapi_schema: return app.openapi_schema openapi_schema = get_openapi( title=app.title, version=app.version, description=app.description, routes=app.routes, tags=app.openapi_tags, ) components = openapi_schema.setdefault("components", {}) security_schemes = components.setdefault("securitySchemes", {}) security_schemes["BearerAuth"] = { "type": "http", "scheme": "bearer", "bearerFormat": "JWT", } # Требуем авторизацию по умолчанию openapi_schema["security"] = [{"BearerAuth": []}] # Оставляем публичными register/login paths = openapi_schema.get("paths", {}) for public_path in ("/api/v1/auth/register", "/api/v1/auth/login"): if public_path in paths and "post" in paths[public_path]: paths[public_path]["post"]["security"] = [] # (опционально) документируем 429 и служебные заголовки for path_item in paths.values(): for method in list(path_item.keys()): if method.lower() in {"get", "post", "put", "patch", "delete"}: responses = path_item[method].setdefault("responses", {}) responses.setdefault( "429", { "description": "Too Many Requests", "headers": { "X-Limit-Remaining": {"schema": {"type": "string"}}, "Retry-After": {"schema": {"type": "string"}}, }, }, ) app.openapi_schema = openapi_schema return app.openapi_schema app.openapi = custom_openapi # --- Injected in optimization step --- try: from .routers.v3.tasks import router as tasks_v3 from .routers.internal import router as internal_router app.include_router(tasks_v3) app.include_router(internal_router) except Exception as e: # Non-fatal if v3 not available pass try: from .routers.v3.projects import router as projects_v3 app.include_router(projects_v3) except Exception: pass