fix: Обновить версии API и добавить новые модели и исключения для управления трафиком узлов и внешними отрядами

This commit is contained in:
Artem 2025-11-12 00:01:39 +01:00
parent 591ff5d120
commit 91ac8ef33e
No known key found for this signature in database
GPG key ID: 833485276B7902CE
11 changed files with 424 additions and 62 deletions

View file

@ -63,7 +63,8 @@ pip install git+https://github.com/remnawave/python-sdk.git@development
| Contract Version | Remnawave Panel Version |
| ---------------- | ----------------------- |
| 2.2.13 | >=2.2.0 |
| 2.2.6 | >=2.2.6 |
| 2.2.3 | >=2.2.13 |
| 2.1.19 | >=2.1.19, <2.2.0 |
| 2.1.18 | >=2.1.18 |
| 2.1.17 | >=2.1.16, <=2.1.17 |

View file

@ -1,7 +1,7 @@
[project]
name = "remnawave"
version = "2.2.3.post2"
description = "A Python SDK for interacting with the Remnawave API v2.2.3."
version = "2.2.6"
description = "A Python SDK for interacting with the Remnawave API v2.2.6."
authors = [
{name = "Artem",email = "dev@forestsnet.com"}
]

View file

@ -18,6 +18,8 @@ from remnawave.models import (
UpdateNodeRequestDto,
UpdateNodeResponseDto,
RestartAllNodesRequestBodyDto,
ResetNodeTrafficRequestDto,
ResetNodeTrafficResponseDto
)
from remnawave.rapid import BaseController, delete, get, patch, post
@ -100,4 +102,12 @@ class NodesController(BaseController):
body: Annotated[ReorderNodeRequestDto, PydanticBody()],
) -> ReorderNodeResponseDto:
"""Reorder Nodes"""
...
@post("/nodes/actions/reset-traffic", response_class=ResetNodeTrafficResponseDto)
async def reset_traffic_all_nodes(
self,
body: Annotated[ResetNodeTrafficRequestDto, PydanticBody()],
) -> ResetNodeTrafficResponseDto:
"""Reset Traffic All Nodes"""
...

View file

@ -78,3 +78,156 @@ class ErrorCode(StrEnum):
SUBSCRIPTION_SETTINGS_NOT_FOUND = "A071"
GET_SUBSCRIPTION_SETTINGS_ERROR = "A072"
UPDATE_SUBSCRIPTION_SETTINGS_ERROR = "A073"
CREATE_INBOUND_ERROR = "A074"
DELETE_INBOUND_ERROR = "A075"
GET_INBOUND_ERROR = "A076"
INBOUND_NOT_FOUND = "A077"
INBOUND_TAG_ALREADY_EXISTS = "A078"
CREATE_HOST_BULK_ACTION_ERROR = "A079"
DELETE_HOST_BULK_ACTION_ERROR = "A080"
UPDATE_HOST_BULK_ACTION_ERROR = "A081"
BULK_ACTION_NOT_FOUND = "A082"
GET_USERS_STATS_ERROR = "A083"
RESET_USERS_TRAFFIC_BULK_ERROR = "A084"
UPDATE_USERS_BULK_ERROR = "A085"
DELETE_USERS_BULK_ERROR = "A086"
GET_USERS_BULK_ERROR = "A087"
CREATE_TEMPLATE_ERROR = "A088"
TEMPLATE_NOT_FOUND = "A089"
UPDATE_TEMPLATE_ERROR = "A090"
DELETE_TEMPLATE_ERROR = "A091"
TEMPLATE_NAME_ALREADY_EXISTS = "A092"
GET_TEMPLATE_ERROR = "A093"
GET_ALL_TEMPLATES_ERROR = "A094"
GENERATE_CONFIG_ERROR = "A095"
INVALID_TEMPLATE_TYPE = "A096"
CREATE_EXTERNAL_SQUAD_ERROR = "A097"
EXTERNAL_SQUAD_NOT_FOUND = "A098"
UPDATE_EXTERNAL_SQUAD_ERROR = "A099"
DELETE_EXTERNAL_SQUAD_ERROR = "A100"
EXTERNAL_SQUAD_NAME_ALREADY_EXISTS = "A101"
ADD_USERS_TO_EXTERNAL_SQUAD_ERROR = "A102"
REMOVE_USERS_FROM_EXTERNAL_SQUAD_ERROR = "A103"
GET_EXTERNAL_SQUAD_ERROR = "A104"
GET_ALL_EXTERNAL_SQUADS_ERROR = "A105"
CREATE_INTERNAL_SQUAD_ERROR = "A106"
INTERNAL_SQUAD_NOT_FOUND = "A107"
UPDATE_INTERNAL_SQUAD_ERROR = "A108"
DELETE_INTERNAL_SQUAD_ERROR = "A109"
INTERNAL_SQUAD_NAME_ALREADY_EXISTS = "A110"
GET_INTERNAL_SQUAD_ERROR = "A111"
GET_ALL_INTERNAL_SQUADS_ERROR = "A112"
CREATE_WEBHOOK_ERROR = "A113"
WEBHOOK_NOT_FOUND = "A114"
UPDATE_WEBHOOK_ERROR = "A115"
DELETE_WEBHOOK_ERROR = "A116"
WEBHOOK_URL_ALREADY_EXISTS = "A117"
GET_WEBHOOK_ERROR = "A118"
GET_ALL_WEBHOOKS_ERROR = "A119"
WEBHOOK_DELIVERY_ERROR = "A120"
CREATE_PASSKEY_ERROR = "A121"
PASSKEY_NOT_FOUND = "A122"
DELETE_PASSKEY_ERROR = "A123"
GET_PASSKEY_ERROR = "A124"
GET_ALL_PASSKEYS_ERROR = "A125"
PASSKEY_ALREADY_EXISTS = "A126"
CREATE_SNIPPET_ERROR = "A127"
SNIPPET_NOT_FOUND = "A128"
UPDATE_SNIPPET_ERROR = "A129"
DELETE_SNIPPET_ERROR = "A130"
SNIPPET_NAME_ALREADY_EXISTS = "A131"
GET_SNIPPET_ERROR = "A132"
GET_ALL_SNIPPETS_ERROR = "A133"
HWID_RESET_ERROR = "A134"
HWID_NOT_FOUND = "A135"
GET_HWID_ERROR = "A136"
GET_ALL_HWIDS_ERROR = "A137"
DELETE_HWID_ERROR = "A138"
BANDWIDTH_STATS_ERROR = "A139"
GET_NODES_USAGE_STATS_ERROR = "A140"
SUBSCRIPTION_REQUEST_ERROR = "A141"
SUBSCRIPTION_REQUEST_NOT_FOUND = "A142"
GET_SUBSCRIPTION_REQUEST_ERROR = "A143"
GET_ALL_SUBSCRIPTION_REQUESTS_ERROR = "A144"
APPROVE_SUBSCRIPTION_REQUEST_ERROR = "A145"
REJECT_SUBSCRIPTION_REQUEST_ERROR = "A146"
CREATE_SUBSCRIPTION_REQUEST_HISTORY_ERROR = "A147"
GET_SUBSCRIPTION_REQUEST_HISTORY_ERROR = "A148"
KEYGEN_ERROR = "A149"
GENERATE_KEYS_ERROR = "A150"
INVALID_KEY_TYPE = "A151"
SYSTEM_STATS_ERROR = "A152"
SYSTEM_HEALTH_ERROR = "A153"
NODES_METRICS_ERROR = "A154"
X25519_KEYGEN_ERROR = "A155"
HAPP_CRYPTO_ERROR = "A156"
SRR_MATCHER_ERROR = "A157"
GET_REMNAWAVE_SETTINGS_ERROR = "A158"
UPDATE_REMNAWAVE_SETTINGS_ERROR = "A159"
OAUTH_ERROR = "A160"
PASSKEY_SETTINGS_ERROR = "A161"
TELEGRAM_AUTH_ERROR = "A162"
BRANDING_SETTINGS_ERROR = "A163"
CONFIG_PROFILE_ERROR = "A164"
CONFIG_PROFILE_NOT_FOUND = "A165"
CREATE_CONFIG_PROFILE_ERROR = "A166"
UPDATE_CONFIG_PROFILE_ERROR = "A167"
DELETE_CONFIG_PROFILE_ERROR = "A168"
GET_CONFIG_PROFILE_ERROR = "A169"
GET_ALL_CONFIG_PROFILES_ERROR = "A170"
XRAY_CONFIG_ERROR = "A171"
XRAY_CONFIG_VALIDATION_ERROR = "A172"
INFRA_BILLING_ERROR = "A173"
INFRA_BILLING_NOT_FOUND = "A174"
GET_INFRA_BILLING_ERROR = "A175"
UPDATE_INFRA_BILLING_ERROR = "A176"
CALCULATE_BILLING_ERROR = "A177"
BILLING_PERIOD_ERROR = "A178"
# Добавляем новые коды из failed тестов
CREATE_SUBSCRIPTION_TEMPLATE_ERROR = "A179"
SUBSCRIPTION_TEMPLATE_NOT_FOUND = "A180"
UPDATE_SUBSCRIPTION_TEMPLATE_ERROR = "A181"
DELETE_SUBSCRIPTION_TEMPLATE_ERROR = "A182"
GET_SUBSCRIPTION_TEMPLATE_ERROR = "A183"
# Валидационные ошибки
VALIDATION_ERROR = "V001"
INVALID_UUID_FORMAT = "V002"
INVALID_EMAIL_FORMAT = "V003"
INVALID_DATE_FORMAT = "V004"
REQUIRED_FIELD_MISSING = "V005"
FIELD_TOO_LONG = "V006"
FIELD_TOO_SHORT = "V007"
INVALID_ENUM_VALUE = "V008"
INVALID_REGEX_PATTERN = "V009"
NUMERIC_VALIDATION_ERROR = "V010"
# Сетевые ошибки
NETWORK_ERROR = "N003"
TIMEOUT_ERROR = "N004"
CONNECTION_ERROR = "N005"
DNS_ERROR = "N006"
SSL_ERROR = "N007"
# Ошибки аутентификации и авторизации
INVALID_TOKEN = "AUTH001"
TOKEN_EXPIRED = "AUTH002"
INVALID_CREDENTIALS = "AUTH003"
TWO_FACTOR_REQUIRED = "AUTH004"
ACCOUNT_LOCKED = "AUTH005"
PASSWORD_COMPLEXITY_ERROR = "AUTH006"
# Ошибки бизнес-логики
TRAFFIC_LIMIT_EXCEEDED = "BL001"
USER_LIMIT_EXCEEDED = "BL002"
SUBSCRIPTION_EXPIRED = "BL003"
FEATURE_NOT_AVAILABLE = "BL004"
QUOTA_EXCEEDED = "BL005"
RESOURCE_LOCKED = "BL006"
# Общие коды
UNKNOWN = "UNKNOWN"
NOT_IMPLEMENTED = "NOT_IMPLEMENTED"
MAINTENANCE_MODE = "MAINTENANCE"
RATE_LIMIT_EXCEEDED = "RATE_LIMIT"

View file

@ -1,23 +1,39 @@
from .handler import handle_api_error
from .general import (
ConflictError,
ApiErrorResponse,
BadRequestError,
NotFoundError,
ForbiddenError,
UnauthorizedError,
ServerError,
ApiError,
ApiErrorResponse,
AuthenticationError,
BadRequestError,
BusinessLogicError,
ConflictError,
FeatureNotAvailableError,
ForbiddenError,
MaintenanceError,
NetworkError,
NotFoundError,
QuotaExceededError,
RateLimitError,
ServerError,
UnauthorizedError,
ValidationError,
)
from .handler import handle_api_error
__all__ = [
"handle_api_error",
"ApiError",
"ApiErrorResponse",
"NotFoundError",
"AuthenticationError",
"BadRequestError",
"ForbiddenError",
"UnauthorizedError",
"BusinessLogicError",
"ConflictError",
"FeatureNotAvailableError",
"ForbiddenError",
"MaintenanceError",
"NetworkError",
"NotFoundError",
"QuotaExceededError",
"RateLimitError",
"ServerError",
]
"UnauthorizedError",
"ValidationError",
"handle_api_error",
]

View file

@ -1,14 +1,13 @@
from datetime import datetime
from typing import Any, List, Optional
from pydantic import AliasChoices, BaseModel, Field
from remnawave.enums import ErrorCode
from typing import Any, List, Optional
class ApiErrorResponse(BaseModel):
"""Standard API error response model"""
timestamp: Optional[datetime] = Field(None, description="Время возникновения ошибки")
path: Optional[str] = Field(None, description="Путь запроса")
message: str = Field(..., description="Сообщение об ошибке")
@ -23,6 +22,8 @@ class ApiErrorResponse(BaseModel):
class ApiError(Exception):
"""Base API error exception"""
def __init__(self, status_code: int, error: ApiErrorResponse):
self.status_code = status_code
self.error = error
@ -30,38 +31,93 @@ class ApiError(Exception):
f"API Error {error.code}: {error.message} (HTTP {status_code})"
)
@property
def code(self) -> Optional[str]:
"""Get error code"""
return self.error.code
@property
def message(self) -> str:
"""Get error message"""
return self.error.message
@property
def timestamp(self) -> Optional[datetime]:
"""Get error timestamp"""
return self.error.timestamp
@property
def path(self) -> Optional[str]:
"""Get request path"""
return self.error.path
class BadRequestError(ApiError):
"""Ошибки клиента (400)"""
pass
class UnauthorizedError(ApiError):
"""Ошибка авторизации (401)"""
pass
class ForbiddenError(ApiError):
"""Доступ запрещен (403)"""
pass
class NotFoundError(ApiError):
"""Ресурс не найден (404)"""
pass
class ConflictError(ApiError):
"""Конфликт (409)"""
pass
class ValidationError(ApiError):
"""Ошибка валидации данных (422)"""
pass
class ServerError(ApiError):
"""Серверная ошибка (500)"""
"""Серверная ошибка (500+)"""
pass
# Новые специализированные исключения
class NetworkError(ApiError):
"""Сетевые ошибки"""
pass
class AuthenticationError(ApiError):
"""Ошибки аутентификации"""
pass
class BusinessLogicError(ApiError):
"""Ошибки бизнес-логики"""
pass
class RateLimitError(BadRequestError):
"""Превышен лимит запросов"""
pass
class MaintenanceError(ServerError):
"""Режим обслуживания"""
pass
class QuotaExceededError(BusinessLogicError):
"""Превышена квота"""
pass
class FeatureNotAvailableError(BusinessLogicError):
"""Функция недоступна"""
pass

View file

@ -1,4 +1,5 @@
from datetime import datetime
from typing import Dict, Type
import httpx
@ -12,11 +13,15 @@ from .general import (
NotFoundError,
ServerError,
UnauthorizedError,
ValidationError,
NetworkError,
AuthenticationError,
BusinessLogicError,
)
ERRORS: dict[str, dict] = {
ERRORS: Dict[str, Type[ApiError]] = {
ErrorCode.INTERNAL_SERVER_ERROR: ServerError,
ErrorCode.LOGIN_ERROR: ServerError,
ErrorCode.LOGIN_ERROR: AuthenticationError,
ErrorCode.UNAUTHORIZED: UnauthorizedError,
ErrorCode.FORBIDDEN_ROLE_ERROR: ForbiddenError,
ErrorCode.CREATE_API_TOKEN_ERROR: ServerError,
@ -33,9 +38,9 @@ ERRORS: dict[str, dict] = {
ErrorCode.CREATE_MANY_INBOUNDS_ERROR: ServerError,
ErrorCode.FIND_ALL_INBOUNDS_ERROR: ServerError,
ErrorCode.CREATE_USER_ERROR: ServerError,
ErrorCode.USER_USERNAME_ALREADY_EXISTS: BadRequestError,
ErrorCode.USER_SHORT_UUID_ALREADY_EXISTS: BadRequestError,
ErrorCode.USER_SUBSCRIPTION_UUID_ALREADY_EXISTS: BadRequestError,
ErrorCode.USER_USERNAME_ALREADY_EXISTS: ConflictError,
ErrorCode.USER_SHORT_UUID_ALREADY_EXISTS: ConflictError,
ErrorCode.USER_SUBSCRIPTION_UUID_ALREADY_EXISTS: ConflictError,
ErrorCode.CREATE_USER_WITH_INBOUNDS_ERROR: ServerError,
ErrorCode.CANT_GET_CREATED_USER_WITH_INBOUNDS: ServerError,
ErrorCode.GET_ALL_USERS_ERROR: ServerError,
@ -43,12 +48,12 @@ ERRORS: dict[str, dict] = {
ErrorCode.GET_USER_BY_ERROR: ServerError,
ErrorCode.REVOKE_USER_SUBSCRIPTION_ERROR: ServerError,
ErrorCode.DISABLE_USER_ERROR: ServerError,
ErrorCode.USER_ALREADY_DISABLED: BadRequestError,
ErrorCode.USER_ALREADY_ENABLED: BadRequestError,
ErrorCode.USER_ALREADY_DISABLED: ConflictError,
ErrorCode.USER_ALREADY_ENABLED: ConflictError,
ErrorCode.ENABLE_USER_ERROR: ServerError,
ErrorCode.CREATE_NODE_ERROR: ServerError,
ErrorCode.NODE_NAME_ALREADY_EXISTS: BadRequestError,
ErrorCode.NODE_ADDRESS_ALREADY_EXISTS: BadRequestError,
ErrorCode.NODE_NAME_ALREADY_EXISTS: ConflictError,
ErrorCode.NODE_ADDRESS_ALREADY_EXISTS: ConflictError,
ErrorCode.NODE_ERROR_WITH_MSG: ServerError,
ErrorCode.NODE_ERROR_500_WITH_MSG: ServerError,
ErrorCode.RESTART_NODE_ERROR: ServerError,
@ -61,7 +66,7 @@ ERRORS: dict[str, dict] = {
ErrorCode.GET_ONE_NODE_ERROR: ServerError,
ErrorCode.DELETE_NODE_ERROR: ServerError,
ErrorCode.CREATE_HOST_ERROR: ServerError,
ErrorCode.HOST_REMARK_ALREADY_EXISTS: BadRequestError,
ErrorCode.HOST_REMARK_ALREADY_EXISTS: ConflictError,
ErrorCode.HOST_NOT_FOUND: NotFoundError,
ErrorCode.DELETE_HOST_ERROR: ServerError,
ErrorCode.GET_USER_STATS_ERROR: ServerError,
@ -77,7 +82,7 @@ ERRORS: dict[str, dict] = {
ErrorCode.GET_ALL_INBOUNDS_ERROR: ServerError,
ErrorCode.BULK_DELETE_USERS_BY_STATUS_ERROR: ServerError,
ErrorCode.UPDATE_INBOUND_ERROR: ServerError,
ErrorCode.CONFIG_VALIDATION_ERROR: ServerError,
ErrorCode.CONFIG_VALIDATION_ERROR: ValidationError,
ErrorCode.USERS_NOT_FOUND: NotFoundError,
ErrorCode.GET_USER_BY_UNIQUE_FIELDS_NOT_FOUND: NotFoundError,
ErrorCode.UPDATE_EXCEEDED_TRAFFIC_USERS_ERROR: ServerError,
@ -85,15 +90,106 @@ ERRORS: dict[str, dict] = {
ErrorCode.CREATE_ADMIN_ERROR: ServerError,
ErrorCode.GET_AUTH_STATUS_ERROR: ServerError,
ErrorCode.FORBIDDEN_ONE: ForbiddenError,
ErrorCode.FORBIDDEN_TWO: ForbiddenError,
ErrorCode.DISABLE_NODE_ERROR: ServerError,
ErrorCode.GET_ONE_HOST_ERROR: ServerError,
ErrorCode.SUBSCRIPTION_SETTINGS_NOT_FOUND: NotFoundError,
ErrorCode.GET_SUBSCRIPTION_SETTINGS_ERROR: ServerError,
ErrorCode.UPDATE_SUBSCRIPTION_SETTINGS_ERROR: ServerError,
ErrorCode.CREATE_SUBSCRIPTION_TEMPLATE_ERROR: ServerError,
ErrorCode.SUBSCRIPTION_TEMPLATE_NOT_FOUND: NotFoundError,
ErrorCode.UPDATE_SUBSCRIPTION_TEMPLATE_ERROR: ServerError,
ErrorCode.DELETE_SUBSCRIPTION_TEMPLATE_ERROR: ServerError,
ErrorCode.GET_SUBSCRIPTION_TEMPLATE_ERROR: ServerError,
ErrorCode.CREATE_INBOUND_ERROR: ServerError,
ErrorCode.DELETE_INBOUND_ERROR: ServerError,
ErrorCode.GET_INBOUND_ERROR: ServerError,
ErrorCode.INBOUND_NOT_FOUND: NotFoundError,
ErrorCode.INBOUND_TAG_ALREADY_EXISTS: ConflictError,
ErrorCode.CREATE_EXTERNAL_SQUAD_ERROR: ServerError,
ErrorCode.EXTERNAL_SQUAD_NOT_FOUND: NotFoundError,
ErrorCode.UPDATE_EXTERNAL_SQUAD_ERROR: ServerError,
ErrorCode.DELETE_EXTERNAL_SQUAD_ERROR: ServerError,
ErrorCode.EXTERNAL_SQUAD_NAME_ALREADY_EXISTS: ConflictError,
ErrorCode.ADD_USERS_TO_EXTERNAL_SQUAD_ERROR: ServerError,
ErrorCode.REMOVE_USERS_FROM_EXTERNAL_SQUAD_ERROR: ServerError,
ErrorCode.GET_EXTERNAL_SQUAD_ERROR: ServerError,
ErrorCode.GET_ALL_EXTERNAL_SQUADS_ERROR: ServerError,
ErrorCode.CREATE_INTERNAL_SQUAD_ERROR: ServerError,
ErrorCode.INTERNAL_SQUAD_NOT_FOUND: NotFoundError,
ErrorCode.UPDATE_INTERNAL_SQUAD_ERROR: ServerError,
ErrorCode.DELETE_INTERNAL_SQUAD_ERROR: ServerError,
ErrorCode.INTERNAL_SQUAD_NAME_ALREADY_EXISTS: ConflictError,
ErrorCode.GET_INTERNAL_SQUAD_ERROR: ServerError,
ErrorCode.GET_ALL_INTERNAL_SQUADS_ERROR: ServerError,
ErrorCode.CREATE_SNIPPET_ERROR: ServerError,
ErrorCode.SNIPPET_NOT_FOUND: NotFoundError,
ErrorCode.UPDATE_SNIPPET_ERROR: ServerError,
ErrorCode.DELETE_SNIPPET_ERROR: ServerError,
ErrorCode.SNIPPET_NAME_ALREADY_EXISTS: ConflictError,
ErrorCode.GET_SNIPPET_ERROR: ServerError,
ErrorCode.GET_ALL_SNIPPETS_ERROR: ServerError,
# Валидационные ошибки
ErrorCode.VALIDATION_ERROR: ValidationError,
ErrorCode.INVALID_UUID_FORMAT: ValidationError,
ErrorCode.INVALID_EMAIL_FORMAT: ValidationError,
ErrorCode.INVALID_DATE_FORMAT: ValidationError,
ErrorCode.REQUIRED_FIELD_MISSING: ValidationError,
ErrorCode.FIELD_TOO_LONG: ValidationError,
ErrorCode.FIELD_TOO_SHORT: ValidationError,
ErrorCode.INVALID_ENUM_VALUE: ValidationError,
ErrorCode.INVALID_REGEX_PATTERN: ValidationError,
ErrorCode.NUMERIC_VALIDATION_ERROR: ValidationError,
# Сетевые ошибки
ErrorCode.NETWORK_ERROR: NetworkError,
ErrorCode.TIMEOUT_ERROR: NetworkError,
ErrorCode.CONNECTION_ERROR: NetworkError,
ErrorCode.DNS_ERROR: NetworkError,
ErrorCode.SSL_ERROR: NetworkError,
# Ошибки аутентификации
ErrorCode.INVALID_TOKEN: AuthenticationError,
ErrorCode.TOKEN_EXPIRED: AuthenticationError,
ErrorCode.INVALID_CREDENTIALS: AuthenticationError,
ErrorCode.TWO_FACTOR_REQUIRED: AuthenticationError,
ErrorCode.ACCOUNT_LOCKED: AuthenticationError,
ErrorCode.PASSWORD_COMPLEXITY_ERROR: ValidationError,
# Бизнес-логика
ErrorCode.TRAFFIC_LIMIT_EXCEEDED: BusinessLogicError,
ErrorCode.USER_LIMIT_EXCEEDED: BusinessLogicError,
ErrorCode.SUBSCRIPTION_EXPIRED: BusinessLogicError,
ErrorCode.FEATURE_NOT_AVAILABLE: BusinessLogicError,
ErrorCode.QUOTA_EXCEEDED: BusinessLogicError,
ErrorCode.RESOURCE_LOCKED: ConflictError,
# Системные ошибки
ErrorCode.SYSTEM_STATS_ERROR: ServerError,
ErrorCode.SYSTEM_HEALTH_ERROR: ServerError,
ErrorCode.NODES_METRICS_ERROR: ServerError,
ErrorCode.X25519_KEYGEN_ERROR: ServerError,
ErrorCode.HAPP_CRYPTO_ERROR: ServerError,
ErrorCode.SRR_MATCHER_ERROR: ServerError,
# Настройки Remnawave
ErrorCode.GET_REMNAWAVE_SETTINGS_ERROR: ServerError,
ErrorCode.UPDATE_REMNAWAVE_SETTINGS_ERROR: ServerError,
ErrorCode.OAUTH_ERROR: AuthenticationError,
ErrorCode.PASSKEY_SETTINGS_ERROR: ServerError,
ErrorCode.TELEGRAM_AUTH_ERROR: AuthenticationError,
ErrorCode.BRANDING_SETTINGS_ERROR: ServerError,
}
def handle_api_error(response: httpx.Response) -> None:
"""Handle API error responses and raise appropriate exceptions"""
if response.status_code >= 400:
try:
error_data = response.json()
@ -103,7 +199,7 @@ def handle_api_error(response: httpx.Response) -> None:
if error_response.timestamp is None:
error_response.timestamp = datetime.now()
if error_response.path is None:
error_response.path = response.request.url.path
error_response.path = str(response.request.url.path)
if error_response.code is None:
# Use status_code or default to UNKNOWN
if error_response.status_code:
@ -111,32 +207,46 @@ def handle_api_error(response: httpx.Response) -> None:
else:
error_response.code = "UNKNOWN"
# Map error code to exception class
if error_response.code in ERRORS:
exception_class = ERRORS[error_response.code]
else:
if response.status_code == 400:
exception_class = BadRequestError
elif response.status_code == 401:
exception_class = UnauthorizedError
elif response.status_code == 403:
exception_class = ForbiddenError
elif response.status_code == 404:
exception_class = NotFoundError
elif response.status_code == 409:
exception_class = ConflictError
elif response.status_code >= 500:
exception_class = ServerError
else:
exception_class = ApiError
# Fallback based on HTTP status code
exception_class = _get_exception_by_status_code(response.status_code)
raise exception_class(response.status_code, error_response)
except ValueError:
# JSON parsing failed, create generic error
raise ApiError(
response.status_code,
ApiErrorResponse(
timestamp=datetime.now(),
path=response.request.url.path,
message="Unknown error " + response.text,
path=str(response.request.url.path),
message=f"Unknown error: {response.text}",
code="UNKNOWN",
status_code=response.status_code,
),
)
def _get_exception_by_status_code(status_code: int) -> Type[ApiError]:
"""Get exception class based on HTTP status code"""
if status_code == 400:
return BadRequestError
elif status_code == 401:
return UnauthorizedError
elif status_code == 403:
return ForbiddenError
elif status_code == 404:
return NotFoundError
elif status_code == 409:
return ConflictError
elif status_code == 422:
return ValidationError
elif status_code == 429:
return BadRequestError # Rate limit
elif status_code >= 500:
return ServerError
else:
return ApiError

View file

@ -172,6 +172,8 @@ from .nodes import (
UpdateNodeResponseDto,
RestartAllNodesRequestDto, # Legacy alias,
RestartAllNodesRequestBodyDto,
ResetNodeTrafficRequestDto,
ResetNodeTrafficResponseDto
)
from .nodes_usage_history import (
GetNodeUserUsageByRangeResponseDto,
@ -384,6 +386,8 @@ __all__ = [
"NodeConfigProfileRequestDto",
"RestartAllNodesRequestDto", # Legacy alias
"RestartAllNodesRequestBodyDto",
"ResetNodeTrafficRequestDto",
"ResetNodeTrafficResponseDto",
# Hosts models
"CreateHostRequestDto",
"CreateHostResponseDto",

View file

@ -1,6 +1,6 @@
from datetime import datetime
from enum import StrEnum
from typing import List, Optional
from typing import Dict, List, Optional
from uuid import UUID
from pydantic import BaseModel, Field
@ -41,22 +41,30 @@ class ExternalSquadSubscriptionSettingsDto(BaseModel):
randomize_hosts: bool = Field(alias="randomizeHosts")
# НОВЫЕ МОДЕЛИ
class ExternalSquadHostOverridesDto(BaseModel):
"""External squad host overrides"""
server_description: Optional[str] = Field(None, alias="serverDescription", max_length=30)
vless_route_id: Optional[int] = Field(None, alias="vlessRouteId", ge=0, le=65535)
class ExternalSquadDto(BaseModel):
"""External squad data model"""
uuid: UUID
name: str
info: ExternalSquadInfoDto
templates: List[ExternalSquadTemplateDto]
subscription_settings: Optional[ExternalSquadSubscriptionSettingsDto] = Field(alias="subscriptionSettings")
subscription_settings: Optional[ExternalSquadSubscriptionSettingsDto] = Field(None, alias="subscriptionSettings")
host_overrides: Optional[ExternalSquadHostOverridesDto] = Field(None, alias="hostOverrides")
response_headers: Optional[Dict[str, str]] = Field(None, alias="responseHeaders")
created_at: datetime = Field(alias="createdAt")
updated_at: datetime = Field(alias="updatedAt")
# Request/Response models
class GetExternalSquadsResponseDto(BaseModel):
class GetExternalSquadsResponseDto(ExternalSquadDto):
"""Response with all external squads"""
total: float
external_squads: List[ExternalSquadDto] = Field(alias="externalSquads")
pass
class GetExternalSquadByUuidResponseDto(ExternalSquadDto):
@ -80,6 +88,8 @@ class UpdateExternalSquadRequestDto(BaseModel):
name: Optional[str] = Field(None, min_length=2, max_length=30, pattern=r"^[A-Za-z0-9_\s-]+$")
templates: Optional[List[ExternalSquadTemplateDto]] = None
subscription_settings: Optional[ExternalSquadSubscriptionSettingsDto] = Field(None, serialization_alias="subscriptionSettings")
host_overrides: Optional[ExternalSquadHostOverridesDto] = Field(None, serialization_alias="hostOverrides")
response_headers: Optional[Dict[str, str]] = Field(None, serialization_alias="responseHeaders")
class UpdateExternalSquadResponseDto(ExternalSquadDto):

View file

@ -1,5 +1,5 @@
from datetime import datetime
from typing import Annotated, List, Optional
from typing import Annotated, List, Optional, Union
from uuid import UUID
from pydantic import BaseModel, Field, StringConstraints, RootModel
@ -208,7 +208,12 @@ class DeleteNodeResponseDto(BaseModel):
class RestartAllNodesRequestBodyDto(BaseModel):
force_restart: bool = Field(default=False, alias="forceRestart")
class ResetNodeTrafficRequestDto(BaseModel):
uuid: Union[str, UUID] = Field(alias="uuid")
class ResetNodeTrafficResponseDto(RestartEventResponse):
pass
# Для обратной совместимости
RestartAllNodesRequestDto = RestartAllNodesRequestBodyDto

View file

@ -134,9 +134,6 @@ class UserResponseDto(BaseModel):
external_squad_uuid: UUID | None = Field(None, alias="externalSquadUuid")
created_at: datetime = Field(alias="createdAt")
updated_at: datetime = Field(alias="updatedAt")
model_config = {"alias_generator": to_camel, "populate_by_name": True}
class EmailUserResponseDto(RootModel[list[UserResponseDto]]):
def __iter__(self):