mirror of
https://github.com/remnawave/python-sdk.git
synced 2026-05-13 12:16:42 +00:00
fix: Обновить версии API и добавить новые модели и исключения для управления трафиком узлов и внешними отрядами
This commit is contained in:
parent
591ff5d120
commit
91ac8ef33e
11 changed files with 424 additions and 62 deletions
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
...
|
||||
|
|
@ -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"
|
||||
|
|
@ -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",
|
||||
]
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue