Handle optional RemnaWave dependencies in web API

This commit is contained in:
Egor
2025-09-28 06:03:11 +03:00
parent 042dc900ef
commit 8fdc70fdc3
2 changed files with 73 additions and 31 deletions

View File

@@ -1,13 +1,11 @@
from __future__ import annotations
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, TYPE_CHECKING
from fastapi import APIRouter, Depends, HTTPException, Query, Security, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.services.remnawave_service import RemnaWaveConfigurationError, RemnaWaveService
from ..dependencies import get_db_session, require_api_token
from ..schemas.remnawave import (
RemnaWaveConnectionStatus,
@@ -31,11 +29,41 @@ from ..schemas.remnawave import (
RemnaWaveUserTrafficResponse,
)
try: # pragma: no cover - импорт может не работать без optional-зависимостей
from app.services.remnawave_service import ( # type: ignore
RemnaWaveConfigurationError,
RemnaWaveService,
)
except Exception: # pragma: no cover - при ошибке импорта скрываем функционал
RemnaWaveConfigurationError = None # type: ignore[assignment]
RemnaWaveService = None # type: ignore[assignment]
if TYPE_CHECKING: # pragma: no cover - только для типов в IDE
from app.services.remnawave_service import RemnaWaveService as RemnaWaveServiceType
else:
RemnaWaveServiceType = Any
router = APIRouter()
def _ensure_service_configured(service: RemnaWaveService) -> None:
def _get_service() -> "RemnaWaveServiceType":
if RemnaWaveService is None: # pragma: no cover - зависимость не доступна
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="RemnaWave сервис недоступен",
)
return RemnaWaveService()
def _ensure_service_configured(service: "RemnaWaveServiceType") -> None:
if RemnaWaveService is None: # pragma: no cover - зависимость не доступна
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="RemnaWave сервис недоступен",
)
if not service.is_configured:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
@@ -74,7 +102,7 @@ def _parse_last_updated(value: Any) -> Optional[datetime]:
async def get_remnawave_status(
_: Any = Security(require_api_token),
) -> RemnaWaveStatusResponse:
service = RemnaWaveService()
service = _get_service()
connection_info: Optional[RemnaWaveConnectionStatus] = None
connection_result = await service.test_api_connection()
@@ -93,7 +121,7 @@ async def get_remnawave_status(
async def get_system_statistics(
_: Any = Security(require_api_token),
) -> RemnaWaveSystemStatsResponse:
service = RemnaWaveService()
service = _get_service()
_ensure_service_configured(service)
stats = await service.get_system_statistics()
@@ -108,7 +136,7 @@ async def get_system_statistics(
async def list_nodes(
_: Any = Security(require_api_token),
) -> RemnaWaveNodeListResponse:
service = RemnaWaveService()
service = _get_service()
_ensure_service_configured(service)
nodes = await service.get_all_nodes()
@@ -120,7 +148,7 @@ async def list_nodes(
async def get_nodes_realtime_usage(
_: Any = Security(require_api_token),
) -> List[Dict[str, Any]]:
service = RemnaWaveService()
service = _get_service()
_ensure_service_configured(service)
return await service.get_nodes_realtime_usage()
@@ -130,7 +158,7 @@ async def get_node_details(
node_uuid: str,
_: Any = Security(require_api_token),
) -> RemnaWaveNode:
service = RemnaWaveService()
service = _get_service()
_ensure_service_configured(service)
node = await service.get_node_details(node_uuid)
@@ -144,7 +172,7 @@ async def get_node_statistics(
node_uuid: str,
_: Any = Security(require_api_token),
) -> RemnaWaveNodeStatisticsResponse:
service = RemnaWaveService()
service = _get_service()
_ensure_service_configured(service)
stats = await service.get_node_statistics(node_uuid)
@@ -171,7 +199,7 @@ async def get_node_usage_range(
end: Optional[datetime] = Query(default=None),
_: Any = Security(require_api_token),
) -> RemnaWaveNodeUsageResponse:
service = RemnaWaveService()
service = _get_service()
_ensure_service_configured(service)
end_dt = end or datetime.utcnow()
@@ -190,7 +218,7 @@ async def manage_node(
payload: RemnaWaveNodeActionRequest,
_: Any = Security(require_api_token),
) -> RemnaWaveNodeActionResponse:
service = RemnaWaveService()
service = _get_service()
_ensure_service_configured(service)
success = await service.manage_node(node_uuid, payload.action)
@@ -212,7 +240,7 @@ async def manage_node(
async def restart_all_nodes(
_: Any = Security(require_api_token),
) -> RemnaWaveNodeActionResponse:
service = RemnaWaveService()
service = _get_service()
_ensure_service_configured(service)
success = await service.restart_all_nodes()
@@ -224,7 +252,7 @@ async def restart_all_nodes(
async def list_squads(
_: Any = Security(require_api_token),
) -> RemnaWaveSquadListResponse:
service = RemnaWaveService()
service = _get_service()
_ensure_service_configured(service)
squads = await service.get_all_squads()
@@ -237,7 +265,7 @@ async def get_squad_details(
squad_uuid: str,
_: Any = Security(require_api_token),
) -> RemnaWaveSquad:
service = RemnaWaveService()
service = _get_service()
_ensure_service_configured(service)
squad = await service.get_squad_details(squad_uuid)
@@ -251,7 +279,7 @@ async def create_squad(
payload: RemnaWaveSquadCreateRequest,
_: Any = Security(require_api_token),
) -> RemnaWaveOperationResponse:
service = RemnaWaveService()
service = _get_service()
_ensure_service_configured(service)
success = await service.create_squad(payload.name, payload.inbound_uuids)
@@ -265,7 +293,7 @@ async def update_squad(
payload: RemnaWaveSquadUpdateRequest,
_: Any = Security(require_api_token),
) -> RemnaWaveOperationResponse:
service = RemnaWaveService()
service = _get_service()
_ensure_service_configured(service)
success = False
@@ -288,7 +316,7 @@ async def squad_actions(
payload: RemnaWaveSquadActionRequest,
_: Any = Security(require_api_token),
) -> RemnaWaveOperationResponse:
service = RemnaWaveService()
service = _get_service()
_ensure_service_configured(service)
action = payload.action
@@ -322,7 +350,7 @@ async def squad_actions(
async def list_inbounds(
_: Any = Security(require_api_token),
) -> RemnaWaveInboundsResponse:
service = RemnaWaveService()
service = _get_service()
_ensure_service_configured(service)
inbounds = await service.get_all_inbounds()
@@ -334,7 +362,7 @@ async def get_user_traffic(
telegram_id: int,
_: Any = Security(require_api_token),
) -> RemnaWaveUserTrafficResponse:
service = RemnaWaveService()
service = _get_service()
_ensure_service_configured(service)
stats = await service.get_user_traffic_stats(telegram_id)
@@ -350,15 +378,17 @@ async def sync_from_panel(
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> RemnaWaveGenericSyncResponse:
service = RemnaWaveService()
service = _get_service()
_ensure_service_configured(service)
try:
stats = await service.sync_users_from_panel(db, payload.mode)
detail = "Синхронизация из панели выполнена"
return RemnaWaveGenericSyncResponse(success=True, detail=detail, data=stats)
except RemnaWaveConfigurationError as exc:
raise HTTPException(status.HTTP_503_SERVICE_UNAVAILABLE, str(exc)) from exc
except Exception as exc: # pragma: no cover - точный тип зависит от импорта
if RemnaWaveConfigurationError and isinstance(exc, RemnaWaveConfigurationError):
raise HTTPException(status.HTTP_503_SERVICE_UNAVAILABLE, str(exc)) from exc
raise
@router.post("/sync/to-panel", response_model=RemnaWaveGenericSyncResponse)
@@ -366,7 +396,7 @@ async def sync_to_panel(
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> RemnaWaveGenericSyncResponse:
service = RemnaWaveService()
service = _get_service()
_ensure_service_configured(service)
stats = await service.sync_users_to_panel(db)
@@ -379,7 +409,7 @@ async def validate_and_fix_subscriptions(
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> RemnaWaveGenericSyncResponse:
service = RemnaWaveService()
service = _get_service()
_ensure_service_configured(service)
stats = await service.validate_and_fix_subscriptions(db)
@@ -392,7 +422,7 @@ async def cleanup_orphaned_subscriptions(
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> RemnaWaveGenericSyncResponse:
service = RemnaWaveService()
service = _get_service()
_ensure_service_configured(service)
stats = await service.cleanup_orphaned_subscriptions(db)
@@ -405,7 +435,7 @@ async def sync_subscription_statuses(
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> RemnaWaveGenericSyncResponse:
service = RemnaWaveService()
service = _get_service()
_ensure_service_configured(service)
stats = await service.sync_subscription_statuses(db)
@@ -418,7 +448,7 @@ async def get_sync_recommendations(
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> RemnaWaveGenericSyncResponse:
service = RemnaWaveService()
service = _get_service()
_ensure_service_configured(service)
data = await service.get_sync_recommendations(db)

View File

@@ -137,13 +137,25 @@ curl -X POST "http://127.0.0.1:8080/tokens" \
| `GET` | `/remnawave/status` | Проверка конфигурации и доступности RemnaWave API. |
| `GET` | `/remnawave/system` | Агрегированная статистика по пользователям, нодам и трафику. |
| `GET` | `/remnawave/nodes` | Список нод и их текущее состояние. |
| `GET` | `/remnawave/nodes/realtime` | Текущая загрузка нод (realtime-метрики RemnaWave). |
| `GET` | `/remnawave/nodes/{uuid}` | Детальная информация по конкретной ноде. |
| `GET` | `/remnawave/nodes/{uuid}/statistics` | Агрегированная статистика и история нагрузок по ноде. |
| `GET` | `/remnawave/nodes/{uuid}/usage` | История использования ноды пользователями за выбранный период. |
| `POST` | `/remnawave/nodes/{uuid}/actions` | Включение, отключение или перезапуск ноды. |
| `GET` | `/remnawave/squads` | Управление сквадами и их инбаундами. |
| `POST` | `/remnawave/nodes/restart` | Массовый перезапуск всех нод в RemnaWave. |
| `GET` | `/remnawave/squads` | Список внутренних сквадов с составом и статистикой. |
| `GET` | `/remnawave/squads/{uuid}` | Детали выбранного сквада. |
| `POST` | `/remnawave/squads` | Создание нового сквада и привязка inbounds. |
| `PATCH` | `/remnawave/squads/{uuid}` | Обновление имени или состава inbounds сквада. |
| `POST` | `/remnawave/squads/{uuid}/actions` | Массовые операции: добавить/удалить всех, переименовать, обновить inbounds, удалить. |
| `GET` | `/remnawave/inbounds` | Список доступных inbounds в панели RemnaWave. |
| `GET` | `/remnawave/users/{telegram_id}/traffic` | Использование трафика конкретного пользователя RemnaWave. |
| `POST` | `/remnawave/sync/from-panel` | Синхронизация пользователей и подписок из панели в бота. |
| `POST` | `/remnawave/sync/to-panel` | Обратная синхронизация данных бота в панель. |
| `POST` | `/remnawave/sync/subscriptions/validate` | Проверка и восстановление подписок в RemnaWave. |
> Остальные операции (история трафика, рекомендации по синхронизации, очистка подписок и т. д.) также доступны в этом разделе Swagger UI.
| `POST` | `/remnawave/sync/subscriptions/cleanup` | Очистка «осиротевших» подписок и пользователей в RemnaWave. |
| `POST` | `/remnawave/sync/subscriptions/statuses` | Приведение статусов подписок в боте и панели к единому виду. |
| `GET` | `/remnawave/sync/recommendations` | Рекомендации по синхронизации: что добавить, обновить или удалить. |
> Все списковые эндпоинты поддерживают пагинацию (`limit`, `offset`) и фильтры, описанные в OpenAPI спецификации. Если `WEB_API_DOCS_ENABLED=true`, документация доступна по `/docs`. В ответах `/settings` поле `choices` всегда массив: пустой список означает отсутствие предопределённых значений.