Skip to content

Commit c0dbfb6

Browse files
committed
feat: собрать базовый каркас FastAPI приложения
Что сделано: - настроены конфигурация приложения, база данных и Redis-клиент - добавлен bootstrap FastAPI и жизненный цикл приложения - подключен health-роут для проверки состояния API
1 parent 3e8ca2c commit c0dbfb6

8 files changed

Lines changed: 154 additions & 0 deletions

File tree

app/__init__.py

Whitespace-only changes.

app/config.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from pydantic_settings import BaseSettings, SettingsConfigDict
2+
3+
4+
class Settings(BaseSettings):
5+
DATABASE_URL: str
6+
SECRET_KEY: str
7+
ALGORITHM: str = "HS256"
8+
ACCESS_TOKEN_EXPIRE_MINUTES: int = 10
9+
REFRESH_TOKEN_EXPIRE_DAYS: int = 7
10+
REDIS_URL: str = "redis://localhost:6379/0"
11+
# Argon2 tuning (can be overridden via .env)
12+
ARGON_TIME_COST: int = 3
13+
ARGON_MEMORY_COST: int = 65536 # KiB (≈64 MiB)
14+
ARGON_PARALLELISM: int = 2
15+
ARGON_HASH_LEN: int = 32
16+
ARGON_SALT_LEN: int = 16
17+
ARGON_MAX_PASSWORD_LEN: int = 1024 # basic DoS guard
18+
19+
model_config = SettingsConfigDict(
20+
env_file=".env",
21+
env_file_encoding="utf-8",
22+
extra="ignore",
23+
)
24+
25+
26+
settings = Settings()

app/database.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from collections.abc import AsyncGenerator
2+
3+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
4+
from sqlalchemy.orm import DeclarativeBase
5+
6+
from app.config import settings
7+
8+
engine = create_async_engine(
9+
settings.DATABASE_URL,
10+
echo=False,
11+
)
12+
13+
AsyncSessionLocal = async_sessionmaker(
14+
engine,
15+
expire_on_commit=False,
16+
)
17+
18+
19+
class Base(DeclarativeBase):
20+
pass
21+
22+
23+
async def get_db() -> AsyncGenerator[AsyncSession]:
24+
async with AsyncSessionLocal() as session:
25+
yield session

app/logging_config.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import logging
2+
3+
import structlog
4+
5+
6+
def setup_logging() -> None:
7+
"""Настраивает structlog для всего приложения. Вызывается один раз при старте."""
8+
9+
logging.basicConfig(
10+
level=logging.INFO,
11+
format="%(message)s",
12+
)
13+
14+
structlog.configure(
15+
processors=[
16+
structlog.stdlib.add_log_level,
17+
structlog.processors.TimeStamper(fmt="iso"),
18+
structlog.processors.JSONRenderer(),
19+
],
20+
wrapper_class=structlog.stdlib.BoundLogger,
21+
context_class=dict,
22+
logger_factory=structlog.stdlib.LoggerFactory(),
23+
)

app/main.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from contextlib import asynccontextmanager
2+
from typing import Awaitable, cast
3+
4+
from fastapi import FastAPI
5+
6+
from app.logging_config import setup_logging
7+
from app.middleware import LoggingMiddleware, MetricsMiddleware, RequestIDMiddleware
8+
from app.redis import redis_client
9+
from app.routes.auth import router as auth_router
10+
from app.routes.health import router as health_router
11+
from app.routes.metrics import router as metrics_router
12+
from app.routes.tags import router as tags_router
13+
from app.routes.tasks import router as tasks_router
14+
15+
16+
@asynccontextmanager
17+
async def lifespan(app: FastAPI):
18+
await cast(Awaitable[bool], redis_client.ping())
19+
try:
20+
yield
21+
finally:
22+
await redis_client.aclose()
23+
24+
25+
setup_logging()
26+
27+
app = FastAPI(
28+
title="Task Manager API",
29+
description="API для управления задачами с авторизацией",
30+
version="0.1.0",
31+
lifespan=lifespan,
32+
)
33+
34+
# middleware регистрируются в обратном порядке (луковица).
35+
# RequestID должен сработать первым, поэтому добавляем его последним.
36+
app.add_middleware(MetricsMiddleware)
37+
app.add_middleware(LoggingMiddleware)
38+
app.add_middleware(RequestIDMiddleware)
39+
40+
app.include_router(metrics_router)
41+
app.include_router(auth_router, prefix="/api/v1")
42+
app.include_router(health_router, prefix="/api/v1")
43+
app.include_router(tasks_router, prefix="/api/v1")
44+
app.include_router(tags_router, prefix="/api/v1")

app/redis.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from redis.asyncio import Redis
2+
3+
from app.config import settings
4+
5+
redis_client = Redis.from_url(
6+
settings.REDIS_URL,
7+
encoding="utf-8",
8+
decode_responses=True,
9+
)
10+
11+
12+
async def get_redis() -> Redis:
13+
return redis_client

app/routes/__init__.py

Whitespace-only changes.

app/routes/health.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from typing import Awaitable, cast
2+
3+
from fastapi import APIRouter, Depends
4+
from redis.asyncio import Redis
5+
from sqlalchemy import text
6+
from sqlalchemy.ext.asyncio import AsyncSession
7+
8+
from app.database import get_db
9+
from app.redis import get_redis
10+
11+
router = APIRouter(prefix="/health", tags=["health"])
12+
13+
14+
@router.get("/redis")
15+
async def redis_health(redis: Redis = Depends(get_redis)) -> dict[str, str]:
16+
ok = await cast(Awaitable[bool], redis.ping())
17+
return {"redis": "ok" if ok else "down"}
18+
19+
20+
@router.get("/db")
21+
async def db_health(session: AsyncSession = Depends(get_db)) -> dict[str, str]:
22+
await session.execute(text("SELECT 1"))
23+
return {"db": "ok"}

0 commit comments

Comments
 (0)