Files
Solo_bot/api/v1/routes/base_crud.py
T

207 lines
8.3 KiB
Python

from typing import Any
from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query
from sqlalchemy import (
inspect as sa_inspect,
select,
)
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from sqlalchemy.orm.attributes import InstrumentedAttribute
from api.depends import get_session, verify_admin_token
from database.access.resolution import resolve_user_optional
from database.models import Admin
from handlers.texts import get_site_gift_link, get_telegram_gift_link
def _apply_user_relationship_loader(model: type, stmt):
if model.__name__ in ("ManualBan", "BlockedUser", "TemporaryData"):
return stmt.options(selectinload(model.user))
return stmt
def cast_identifier_type(field: InstrumentedAttribute, value: int | str):
column_type = type(field.property.columns[0].type).__name__
if column_type in ("Integer", "BigInteger"):
return int(value)
return value
def normalize_outgoing_object(obj: object) -> None:
if hasattr(obj, "vless") and obj.vless is None:
obj.vless = False
cls_name = type(obj).__name__
if cls_name == "Gift":
gift_id = getattr(obj, "gift_id", None)
if gift_id:
obj.telegram_gift_link = get_telegram_gift_link(gift_id)
obj.site_gift_link = get_site_gift_link(gift_id)
if cls_name in ("ManualBan", "BlockedUser", "TemporaryData"):
insp = sa_inspect(obj)
stored = getattr(obj, "tg_id", None)
if "user" in insp.unloaded:
obj.tg_id = stored
return
rel = getattr(obj, "user", None)
rel_tg = getattr(rel, "tg_id", None) if rel is not None else None
obj.tg_id = stored if stored is not None else rel_tg
def to_schema(schema_response: type, obj: object):
normalize_outgoing_object(obj)
return schema_response.model_validate(obj, from_attributes=True)
def generate_crud_router(
*,
model: type,
schema_response: type,
schema_create: type,
schema_update: type,
identifier_field: str = "tg_id",
parameter_name: str = "tg_id",
extra_get_by_email: bool = False,
telegram_path_to_user_id: bool = False,
enabled_methods: list[str] = ("get_all", "get_one", "get_by_email", "create", "update", "delete"),
) -> APIRouter:
router = APIRouter()
async def _path_filter(session: AsyncSession, value: int | str):
if telegram_path_to_user_id:
u = await resolve_user_optional(session, int(value))
if u is None:
return None
return model.user_id, u.id
field = getattr(model, identifier_field)
return field, cast_identifier_type(field, value)
if "get_all" in enabled_methods:
@router.get("/", response_model=list[schema_response])
async def get_all(
admin: Admin = Depends(verify_admin_token),
session: AsyncSession = Depends(get_session),
):
result = await session.execute(_apply_user_relationship_loader(model, select(model)))
items = result.scalars().all()
for item in items:
normalize_outgoing_object(item)
return [schema_response.model_validate(item, from_attributes=True) for item in items]
if "get_by_email" in enabled_methods and extra_get_by_email:
@router.get("/by_email", response_model=schema_response)
async def get_by_email(
email: str = Query(...),
admin: Admin = Depends(verify_admin_token),
session: AsyncSession = Depends(get_session),
):
result = await session.execute(select(model).where(model.email == email))
obj = result.scalar_one_or_none()
if not obj:
raise HTTPException(status_code=404, detail="Not found by email")
return to_schema(schema_response, obj)
if "get_one" in enabled_methods:
@router.get(f"/{{{parameter_name}}}", response_model=schema_response)
async def get_one(
value: int | str = Path(..., alias=parameter_name),
admin: Admin = Depends(verify_admin_token),
session: AsyncSession = Depends(get_session),
):
resolved = await _path_filter(session, value)
if resolved is None:
raise HTTPException(status_code=404, detail=f"{model.__name__} not found")
field, casted = resolved
result = await session.execute(_apply_user_relationship_loader(model, select(model).where(field == casted)))
obj = result.scalar_one_or_none()
if not obj:
raise HTTPException(status_code=404, detail=f"{model.__name__} not found")
return to_schema(schema_response, obj)
if "get_all_by_field" in enabled_methods:
@router.get(f"/all/{{{parameter_name}}}", response_model=list[schema_response])
async def get_all_by_field(
value: int | str = Path(..., alias=parameter_name),
admin: Admin = Depends(verify_admin_token),
session: AsyncSession = Depends(get_session),
):
resolved = await _path_filter(session, value)
if resolved is None:
raise HTTPException(status_code=404, detail=f"{model.__name__} not found")
field, casted = resolved
result = await session.execute(_apply_user_relationship_loader(model, select(model).where(field == casted)))
objs = result.scalars().all()
if not objs:
raise HTTPException(status_code=404, detail=f"{model.__name__} not found")
for obj in objs:
normalize_outgoing_object(obj)
return [schema_response.model_validate(obj, from_attributes=True) for obj in objs]
if "create" in enabled_methods:
@router.post("/", response_model=schema_response)
async def create(
payload: Any = Body(...),
admin: Admin = Depends(verify_admin_token),
session: AsyncSession = Depends(get_session),
):
validated = schema_create.model_validate(payload)
data = validated.model_dump(exclude_unset=True)
if "days" in data and data["days"] == 0:
data["days"] = None
obj = model(**data)
session.add(obj)
await session.refresh(obj)
return to_schema(schema_response, obj)
if "update" in enabled_methods:
@router.patch(f"/{{{parameter_name}}}", response_model=schema_response)
async def update(
payload: Any = Body(...),
value: int | str = Path(..., alias=parameter_name),
admin: Admin = Depends(verify_admin_token),
session: AsyncSession = Depends(get_session),
):
resolved = await _path_filter(session, value)
if resolved is None:
raise HTTPException(status_code=404, detail=f"{model.__name__} not found")
field, casted = resolved
result = await session.execute(select(model).where(field == casted))
obj = result.scalar_one_or_none()
if not obj:
raise HTTPException(status_code=404, detail=f"{model.__name__} not found")
validated = schema_update.model_validate(payload)
for k, v in validated.model_dump(exclude_unset=True).items():
setattr(obj, k, v)
await session.refresh(obj)
return to_schema(schema_response, obj)
if "delete" in enabled_methods:
@router.delete(f"/{{{parameter_name}}}", response_model=dict)
async def delete(
value: int | str = Path(..., alias=parameter_name),
admin: Admin = Depends(verify_admin_token),
session: AsyncSession = Depends(get_session),
):
resolved = await _path_filter(session, value)
if resolved is None:
raise HTTPException(status_code=404, detail=f"{model.__name__} not found")
field, casted = resolved
result = await session.execute(select(model).where(field == casted))
obj = result.scalar_one_or_none()
if not obj:
raise HTTPException(status_code=404, detail=f"{model.__name__} not found")
await session.delete(obj)
return {"detail": f"{model.__name__} deleted"}
return router