from __future__ import annotations from typing import Any, Optional from fastapi import APIRouter, Depends, HTTPException, Query, Security, status from sqlalchemy.ext.asyncio import AsyncSession from app.services.system_settings_service import ( ReadOnlySettingError, bot_configuration_service, ) from ..dependencies import get_db_session, require_api_token from ..schemas.config import ( SettingCategoryRef, SettingCategorySummary, SettingChoice, SettingDefinition, SettingUpdateRequest, ) router = APIRouter() def _coerce_value(key: str, value: Any) -> Any: definition = bot_configuration_service.get_definition(key) if value is None: if definition.is_optional: return None raise HTTPException(status.HTTP_400_BAD_REQUEST, "Value is required") python_type = definition.python_type try: if python_type is bool: if isinstance(value, bool): normalized = value elif isinstance(value, str): lowered = value.strip().lower() if lowered in {"true", "1", "yes", "on", "да"}: normalized = True elif lowered in {"false", "0", "no", "off", "нет"}: normalized = False else: raise ValueError("invalid bool") else: raise ValueError("invalid bool") elif python_type is int: normalized = int(value) elif python_type is float: normalized = float(value) else: normalized = str(value) except ValueError: raise HTTPException(status.HTTP_400_BAD_REQUEST, "Invalid value type") from None choices = bot_configuration_service.get_choice_options(key) if choices: allowed_values = {option.value for option in choices} if normalized not in allowed_values: readable = ", ".join(bot_configuration_service.format_value(opt.value) for opt in choices) raise HTTPException( status.HTTP_400_BAD_REQUEST, detail=f"Value must be one of: {readable}", ) return normalized def _serialize_definition(definition, include_choices: bool = True) -> SettingDefinition: current = bot_configuration_service.get_current_value(definition.key) original = bot_configuration_service.get_original_value(definition.key) has_override = bot_configuration_service.has_override(definition.key) choices: list[SettingChoice] = [] if include_choices: choices = [ SettingChoice( value=option.value, label=option.label, description=option.description, ) for option in bot_configuration_service.get_choice_options(definition.key) ] return SettingDefinition( key=definition.key, name=definition.display_name, category=SettingCategoryRef( key=definition.category_key, label=definition.category_label, ), type=definition.type_label, is_optional=definition.is_optional, current=current, original=original, has_override=has_override, read_only=bot_configuration_service.is_read_only(definition.key), choices=choices, ) @router.get("/categories", response_model=list[SettingCategorySummary]) async def list_categories( _: object = Security(require_api_token), ) -> list[SettingCategorySummary]: categories = bot_configuration_service.get_categories() return [ SettingCategorySummary(key=key, label=label, items=count) for key, label, count in categories ] @router.get("", response_model=list[SettingDefinition]) async def list_settings( _: object = Security(require_api_token), category: Optional[str] = Query(default=None, alias="category_key"), ) -> list[SettingDefinition]: items: list[SettingDefinition] = [] if category: definitions = bot_configuration_service.get_settings_for_category(category) items.extend(_serialize_definition(defn) for defn in definitions) return items for category_key, _, _ in bot_configuration_service.get_categories(): definitions = bot_configuration_service.get_settings_for_category(category_key) items.extend(_serialize_definition(defn) for defn in definitions) return items @router.get("/{key}", response_model=SettingDefinition) async def get_setting( key: str, _: object = Security(require_api_token), ) -> SettingDefinition: try: definition = bot_configuration_service.get_definition(key) except KeyError as error: # pragma: no cover - защита от некорректного ключа raise HTTPException(status.HTTP_404_NOT_FOUND, "Setting not found") from error return _serialize_definition(definition) @router.put("/{key}", response_model=SettingDefinition) async def update_setting( key: str, payload: SettingUpdateRequest, _: object = Security(require_api_token), db: AsyncSession = Depends(get_db_session), ) -> SettingDefinition: try: definition = bot_configuration_service.get_definition(key) except KeyError as error: raise HTTPException(status.HTTP_404_NOT_FOUND, "Setting not found") from error value = _coerce_value(key, payload.value) try: await bot_configuration_service.set_value(db, key, value) except ReadOnlySettingError as error: raise HTTPException(status.HTTP_403_FORBIDDEN, str(error)) from error await db.commit() return _serialize_definition(definition) @router.delete("/{key}", response_model=SettingDefinition) async def reset_setting( key: str, _: object = Security(require_api_token), db: AsyncSession = Depends(get_db_session), ) -> SettingDefinition: try: definition = bot_configuration_service.get_definition(key) except KeyError as error: raise HTTPException(status.HTTP_404_NOT_FOUND, "Setting not found") from error try: await bot_configuration_service.reset_value(db, key) except ReadOnlySettingError as error: raise HTTPException(status.HTTP_403_FORBIDDEN, str(error)) from error await db.commit() return _serialize_definition(definition)