feat: обновить модели для поддержки плавающих значений и улучшить валидацию

This commit is contained in:
Artem 2026-03-11 13:26:01 +01:00
parent 0ed1ce92c0
commit a880da073f
No known key found for this signature in database
GPG key ID: 833485276B7902CE
16 changed files with 150 additions and 79 deletions

View file

@ -34,10 +34,10 @@ class SubscriptionController(BaseController):
"""None"""
...
@get("/sub/outline/{short_uuid}/{type}/{encoded_tag}", response_class=str)
@get("/sub/outline/{shortUuid}/{type}/{encodedTag}", response_class=str)
async def get_subscription_with_type(
self,
short_uuid: Annotated[str, Path(description="Short UUID of the user")],
short_uuid: Annotated[str, Path(description="Short UUID of the user", alias="shortUuid")],
type: Annotated[
str,
Path(
@ -47,7 +47,8 @@ class SubscriptionController(BaseController):
encoded_tag: Annotated[
str,
Path(
description="Base64 encoded tag for Outline config. This paramter is optional. It is required only when type=ss."
description="Base64 encoded tag for Outline config. This paramter is optional. It is required only when type=ss.",
alias="encodedTag",
),
] = "VGVzdGVy",
) -> str:

View file

@ -1,6 +1,6 @@
from typing import Annotated, Any, Dict, Optional
from pydantic import BaseModel, Field, StringConstraints
from pydantic import BaseModel, Field, StringConstraints, field_validator
from remnawave.enums.auth import OAuth2Provider
@ -57,6 +57,17 @@ class RegisterRequestDto(BaseModel):
username: str
password: Annotated[str, StringConstraints(min_length=24)]
@field_validator("password")
@classmethod
def validate_password_complexity(cls, v: str) -> str:
if not any(c.isupper() for c in v):
raise ValueError("Password must contain at least one uppercase letter")
if not any(c.islower() for c in v):
raise ValueError("Password must contain at least one lowercase letter")
if not any(c.isdigit() for c in v):
raise ValueError("Password must contain at least one digit")
return v
class TelegramCallbackRequestDto(BaseModel):
id: int

View file

@ -1,8 +1,8 @@
from datetime import datetime
from typing import Any, Dict, List, Optional
from typing import Annotated, Any, Dict, List, Optional
from uuid import UUID
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, StringConstraints
class InboundDto(BaseModel):
@ -32,7 +32,7 @@ class ConfigProfileDto(BaseModel):
class CreateConfigProfileRequestDto(BaseModel):
name: str
name: Annotated[str, StringConstraints(min_length=2, max_length=30, pattern=r"^[A-Za-z0-9_\s-]+$")]
config: Dict[str, Any]
@ -42,7 +42,7 @@ class CreateConfigProfileResponseDto(ConfigProfileDto):
class UpdateConfigProfileRequestDto(BaseModel):
uuid: UUID
name: Optional[str] = Field(None, pattern=r"^[A-Za-z0-9_-]+$")
name: Optional[Annotated[str, StringConstraints(min_length=2, max_length=30, pattern=r"^[A-Za-z0-9_\s-]+$")]] = None
config: Optional[Dict[str, Any]] = None
@ -51,7 +51,7 @@ class UpdateConfigProfileResponseDto(ConfigProfileDto):
class GetAllConfigProfilesResponsePaginated(BaseModel):
total: int
total: float
config_profiles: List[ConfigProfileDto] = Field(alias="configProfiles")

View file

@ -58,6 +58,7 @@ class ExternalSquadDto(BaseModel):
response_headers: Optional[Dict[str, str]] = Field(None, alias="responseHeaders")
hwid_settings: Optional[HwidSettingsDto] = Field(None, alias="hwidSettings")
custom_remarks: Optional[CustomRemarksDto] = Field(None, alias="customRemarks")
subpage_config_uuid: Optional[UUID] = Field(None, alias="subpageConfigUuid")
created_at: datetime = Field(alias="createdAt")
updated_at: datetime = Field(alias="updatedAt")
@ -65,7 +66,7 @@ class ExternalSquadDto(BaseModel):
# Request/Response models
class GetExternalSquadsResponseDto(BaseModel):
"""Response with all external squads"""
total: int = Field(alias="total")
total: float = Field(alias="total")
external_squads: List[ExternalSquadDto] = Field(alias="externalSquads")
@ -94,6 +95,7 @@ class UpdateExternalSquadRequestDto(BaseModel):
hwid_settings: Optional[HwidSettingsDto] = Field(None, alias="hwidSettings")
custom_remarks: Optional[CustomRemarksDto] = Field(None, alias="customRemarks")
response_headers: Optional[Dict[str, str]] = Field(None, serialization_alias="responseHeaders")
subpage_config_uuid: Optional[UUID] = Field(None, serialization_alias="subpageConfigUuid")
class UpdateExternalSquadResponseDto(ExternalSquadDto):
@ -117,15 +119,10 @@ class ReorderExternalSquadsRequestDto(BaseModel):
class ReorderExternalSquadsResponseDto(BaseModel):
"""Response after reordering external squads"""
total: int = Field(alias="total")
total: float = Field(alias="total")
external_squads: List[ExternalSquadDto] = Field(alias="externalSquads")
class DeleteExternalSquadResponseDto(BaseModel):
"""Response after deleting external squad"""
is_deleted: bool = Field(alias="isDeleted")
class AddUsersToExternalSquadResponseDto(BaseModel):
"""Response after adding users to external squad"""
event_sent: bool = Field(alias="eventSent")

View file

@ -43,7 +43,7 @@ class UpdateHostRequestDto(BaseModel):
tag: Optional[Annotated[str, StringConstraints(max_length=32, pattern=r"^[A-Z0-9_:]+$")]] = None
is_hidden: Optional[bool] = Field(None, serialization_alias="isHidden")
override_sni_from_address: Optional[bool] = Field(None, serialization_alias="overrideSniFromAddress")
keep_blank_sni: Optional[bool] = Field(None, serialization_alias="keepBlankSni")
keep_blank_sni: Optional[bool] = Field(None, serialization_alias="keepSniBlank")
vless_route_id: Optional[int] = Field(None, serialization_alias="vlessRouteId", ge=0, le=65535)
shuffle_host: Optional[bool] = Field(None, serialization_alias="shuffleHost")
mihomo_x25519: Optional[bool] = Field(None, serialization_alias="mihomoX25519")
@ -89,7 +89,7 @@ class HostResponseDto(BaseModel):
security_layer: SecurityLayer = Field(SecurityLayer.DEFAULT, alias="securityLayer")
is_hidden: bool = Field(False, alias="isHidden")
override_sni_from_address: bool = Field(False, alias="overrideSniFromAddress")
keep_blank_sni: bool = Field(False, alias="keepBlankSni")
keep_blank_sni: bool = Field(False, alias="keepSniBlank")
allow_insecure: bool = Field(False, alias="allowInsecure")
xray_json_template_uuid: UUID | None = Field(alias="xrayJsonTemplateUuid")
excluded_internal_squads: List[UUID] = Field(default_factory=list, alias="excludedInternalSquads")
@ -128,7 +128,7 @@ class CreateHostRequestDto(BaseModel):
security_layer: SecurityLayer = Field(SecurityLayer.DEFAULT, serialization_alias="securityLayer")
is_hidden: bool = Field(False, serialization_alias="isHidden")
override_sni_from_address: bool = Field(False, serialization_alias="overrideSniFromAddress")
keep_blank_sni: bool = Field(False, serialization_alias="keepBlankSni")
keep_blank_sni: bool = Field(False, serialization_alias="keepSniBlank")
xray_json_template_uuid: Optional[UUID] = Field(None, serialization_alias="xrayJsonTemplateUuid")
excluded_internal_squads: List[UUID] = Field(default_factory=list, serialization_alias="excludedInternalSquads")
exclude_from_subscription_types: List[SubscriptionType] = Field(
@ -147,6 +147,10 @@ class CreateHostRequestDto(BaseModel):
config_profile_uuid: Optional[UUID] = None,
**data,
):
# Backward-compatible support for misspelled helper argument used in old tests/examples
if config_profile_uuid is None and "config_profile_inbound_uuid" in data:
config_profile_uuid = data.pop("config_profile_inbound_uuid")
if inbound_uuid is not None and "inbound" not in data:
data["inbound"] = CreateHostInboundData(
config_profile_uuid=config_profile_uuid

View file

@ -31,22 +31,22 @@ class HwidDeviceDto(BaseModel):
class HwidDevicesData(BaseModel):
total: int
total: float
devices: List[HwidDeviceDto]
class CreateUserHwidDeviceResponseDto(BaseModel):
total: int
total: float
devices: List[HwidDeviceDto]
class DeleteUserHwidDeviceResponseDto(BaseModel):
total: int
total: float
devices: List[HwidDeviceDto]
class GetUserHwidDevicesResponseDto(BaseModel):
total: int
total: float
devices: List[HwidDeviceDto]
class PlatformStatItem(BaseModel):
@ -82,13 +82,13 @@ class TopUserByHwidDevicesDto(BaseModel):
user_uuid: UUID = Field(alias="userUuid")
id: int
username: str
devices_count: int = Field(alias="devicesCount")
devices_count: float = Field(alias="devicesCount")
class TopUsersByHwidDevicesData(BaseModel):
"""Top users by HWID devices data"""
users: list[TopUserByHwidDevicesDto]
total: int
total: float
class GetTopUsersByHwidDevicesResponseDto(TopUsersByHwidDevicesData):

View file

@ -13,11 +13,11 @@ class InboundResponseDto(BaseModel):
security: Optional[str] = None
port: Optional[float] = None
raw_inbound: Optional[Any] = Field(None, alias="rawInbound")
active_squads: Optional[list[UUID]] = Field(None, alias="activeSquads")
active_squads: List[UUID] = Field(default_factory=list, alias="activeSquads")
class AllInboundsData(BaseModel):
total: int
total: float
inbounds: List[InboundResponseDto]
@ -26,7 +26,7 @@ class GetAllInboundsResponseDto(AllInboundsData):
class InboundsByProfileData(BaseModel):
total: int
total: float
inbounds: List[InboundResponseDto]

View file

@ -102,7 +102,7 @@ class UpdateInfraProviderResponseDto(InfraProviderDto):
class AllInfraProvidersData(BaseModel):
total: int = Field(alias="total")
total: float = Field(alias="total")
providers: List[InfraProviderDto]
@ -135,7 +135,7 @@ class CreateInfraBillingHistoryRecordResponseDto(InfraBillingHistoryDto):
class InfraBillingHistoryData(BaseModel):
records: List[InfraBillingHistoryDto]
total: int
total: float
class GetInfraBillingHistoryRecordsResponseDto(InfraBillingHistoryData):
@ -167,8 +167,12 @@ class UpdateInfraBillingNodeRequestDto(BaseModel):
next_billing_at: datetime = Field(serialization_alias="nextBillingAt")
class UpdateInfraBillingNodeResponseDto(InfraBillingNodeDto):
pass
class UpdateInfraBillingNodeResponseDto(BaseModel):
total_billing_nodes: float = Field(alias="totalBillingNodes")
billing_nodes: List[InfraBillingNodeDto] = Field(alias="billingNodes")
available_billing_nodes: List[AvailableBillingNodeDto] = Field(alias="availableBillingNodes")
total_available_billing_nodes: float = Field(alias="totalAvailableBillingNodes")
stats: BillingStatsDto
class InfraBillingNodesData(BaseModel):

View file

@ -1,8 +1,8 @@
from datetime import datetime
from typing import List, Optional
from typing import Annotated, List, Optional
from uuid import UUID
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, StringConstraints
class InboundsDto(BaseModel):
@ -17,8 +17,8 @@ class InboundsDto(BaseModel):
class InfoDto(BaseModel):
members_count: int = Field(alias="membersCount")
inbounds_count: int = Field(alias="inboundsCount")
members_count: float = Field(alias="membersCount")
inbounds_count: float = Field(alias="inboundsCount")
class InternalSquadDto(BaseModel):
@ -32,7 +32,7 @@ class InternalSquadDto(BaseModel):
class CreateInternalSquadRequestDto(BaseModel):
name: str
name: Annotated[str, StringConstraints(min_length=2, max_length=30, pattern=r"^[A-Za-z0-9_\s-]+$")]
inbounds: List[UUID] = Field(default_factory=list)
@ -43,7 +43,7 @@ class CreateInternalSquadResponseDto(InternalSquadDto):
class UpdateInternalSquadRequestDto(BaseModel):
uuid: UUID
inbounds: List[UUID] = Field(default_factory=list)
name: Optional[str] = Field(None, pattern=r"^[A-Za-z0-9_-]+$")
name: Optional[Annotated[str, StringConstraints(min_length=2, max_length=30, pattern=r"^[A-Za-z0-9_\s-]+$")]] = None
class UpdateInternalSquadResponseDto(InternalSquadDto):
@ -51,7 +51,7 @@ class UpdateInternalSquadResponseDto(InternalSquadDto):
class GetAllInternalSquadsResponse(BaseModel):
total: int
total: float
internal_squads: List[InternalSquadDto] = Field(alias="internalSquads")

View file

@ -89,6 +89,9 @@ class CreateNodeRequestDto(BaseModel):
serialization_alias="tags",
max_length=10
)
active_plugin_uuid: Optional[UUID] = Field(
None, serialization_alias="activePluginUuid"
)
class UpdateNodeRequestDto(BaseModel):
@ -126,6 +129,9 @@ class UpdateNodeRequestDto(BaseModel):
serialization_alias="tags",
max_length=10
)
active_plugin_uuid: Optional[UUID] = Field(
None, serialization_alias="activePluginUuid"
)
class ReorderNodeRequestDto(BaseModel):
@ -163,6 +169,7 @@ class NodeResponseDto(BaseModel):
provider_uuid: Optional[UUID] = Field(None, alias="providerUuid")
provider: Optional[NodeProviderDto] = None
tags: List[str] = Field(default_factory=list, alias="tags")
active_plugin_uuid: Optional[UUID] = Field(None, alias="activePluginUuid")
class CreateNodeResponseDto(NodeResponseDto):

View file

@ -176,7 +176,7 @@ class GetRawSubscriptionByShortUuidResponseDto(RawSubscriptionResponse):
class UserSubscription(BaseModel):
short_uuid: str = Field(alias="shortUuid")
username: str
days_left: int = Field(alias="daysLeft")
days_left: float = Field(alias="daysLeft")
traffic_used: str = Field(alias="trafficUsed")
traffic_limit: str = Field(alias="trafficLimit")
lifetime_traffic_used: str = Field(alias="lifetimeTrafficUsed")
@ -222,7 +222,7 @@ class SubscriptionWithoutHapp(BaseModel):
class GetAllSubscriptionsResponseDto(BaseModel):
subscriptions: List[SubscriptionWithoutHapp]
total: int
total: float
class GetSubscriptionByUsernameResponseDto(BaseModel):
@ -242,7 +242,14 @@ class GetSubscriptionByUUIDResponseDto(GetSubscriptionByUsernameResponseDto):
class GetConnectionKeysByUuidResponseDto(BaseModel):
connection_keys: List[str] = Field(alias="connectionKeys")
enabled_keys: List[str] = Field(alias="enabledKeys")
hidden_keys: List[str] = Field(alias="hiddenKeys")
disabled_keys: List[str] = Field(alias="disabledKeys")
@property
def connection_keys(self) -> List[str]:
"""Backward compatibility: historically SDK exposed a flat list of keys."""
return self.enabled_keys
# Legacy alias for backward compatibility

View file

@ -16,7 +16,7 @@ class SubscriptionPageConfigDto(BaseModel):
class GetSubscriptionPageConfigsData(BaseModel):
"""Data for getting all subscription page configs"""
total: int
total: float
configs: List[SubscriptionPageConfigDto]

View file

@ -29,7 +29,7 @@ class GetTemplateResponseDto(TemplateResponseDto):
pass
class GetTemplatesData(BaseModel):
total: int
total: float
templates: List[TemplateInfoDto]
class GetTemplatesResponseDto(GetTemplatesData):

View file

@ -10,7 +10,7 @@ from remnawave.models.subscriptions_settings import ResponseRule, ResponseRules
class NodeStatistic(BaseModel):
node_name: str = Field(alias="nodeName")
date: datetime.date
total_bytes: int = Field(alias="totalBytes")
total_bytes: str = Field(alias="totalBytes")
class NodesStatisticResponseDto(BaseModel):
@ -32,16 +32,16 @@ class BandwidthStatisticResponseDto(BaseModel):
class CPUStatistic(BaseModel):
cores: int
physical_cores: int = Field(alias="physicalCores")
cores: float
physical_cores: float = Field(alias="physicalCores")
class MemoryStatistic(BaseModel):
total: int
free: int
used: int
active: int
available: int
total: float
free: float
used: float
active: float
available: float
class StatusCounts(BaseModel):
@ -59,18 +59,18 @@ class StatusCounts(BaseModel):
class UsersStatistic(BaseModel):
status_counts: StatusCounts = Field(alias="statusCounts")
total_users: int = Field(alias="totalUsers")
total_users: float = Field(alias="totalUsers")
class OnlineStatistic(BaseModel):
last_day: int = Field(alias="lastDay")
last_week: int = Field(alias="lastWeek")
never_online: int = Field(alias="neverOnline")
online_now: int = Field(alias="onlineNow")
last_day: float = Field(alias="lastDay")
last_week: float = Field(alias="lastWeek")
never_online: float = Field(alias="neverOnline")
online_now: float = Field(alias="onlineNow")
class NodesStatistic(BaseModel):
total_online: int = Field(alias="totalOnline")
total_online: float = Field(alias="totalOnline")
total_bytes_lifetime: str = Field(alias="totalBytesLifetime")
@ -79,7 +79,7 @@ class StatisticResponseDto(BaseModel):
cpu: CPUStatistic
memory: MemoryStatistic
uptime: float
timestamp: int
timestamp: float
users: UsersStatistic
online_stats: OnlineStatistic = Field(alias="onlineStats")
nodes: NodesStatistic
@ -116,21 +116,57 @@ class GetRemnawaveHealthResponseDto(BaseModel):
pm2_stats: List[PM2Stat] = Field(alias="pm2Stats")
class TrafficStatDto(BaseModel):
tag: str
upload: str
download: str
class NodeMetric(BaseModel):
"""Node metric data"""
uuid: str = Field(alias="nodeUuid")
name: Optional[str] = None
address: Optional[str] = None
is_online: Optional[bool] = Field(None, alias="isOnline")
cpu_usage: Optional[float] = Field(None, alias="cpuUsage")
memory_usage: Optional[float] = Field(None, alias="memoryUsage")
network_upload: Optional[int] = Field(None, alias="networkUpload")
network_download: Optional[int] = Field(None, alias="networkDownload")
uptime: Optional[int] = None
last_seen: Optional[datetime.datetime] = Field(None, alias="lastSeen")
connected_users: Optional[int] = Field(None, alias="connectedUsers")
upload: Optional[str] = None
download: Optional[str] = None
"""Node metric data (API v1.10)"""
node_uuid: str = Field(alias="nodeUuid")
node_name: str = Field(alias="nodeName")
country_emoji: str = Field(alias="countryEmoji")
provider_name: str = Field(alias="providerName")
users_online: float = Field(alias="usersOnline")
inbounds_stats: List[TrafficStatDto] = Field(alias="inboundsStats")
outbounds_stats: List[TrafficStatDto] = Field(alias="outboundsStats")
@property
def uuid(self) -> str:
return self.node_uuid
@property
def name(self) -> str:
return self.node_name
@property
def connected_users(self) -> float:
return self.users_online
@property
def cpu_usage(self) -> None:
return None
@property
def memory_usage(self) -> None:
return None
@property
def network_upload(self) -> None:
return None
@property
def network_download(self) -> None:
return None
@property
def uptime(self) -> None:
return None
@property
def last_seen(self) -> None:
return None
class GetNodesMetricsResponseDto(BaseModel):
@ -143,7 +179,11 @@ class X25519KeyPair(BaseModel):
class GetX25519KeyPairResponseDto(BaseModel):
key_pairs: List[X25519KeyPair] = Field(alias="keyPairs")
key_pairs: List[X25519KeyPair] = Field(alias="keypairs")
# OpenAPI v1.10 schema name
GenerateX25519ResponseDto = GetX25519KeyPairResponseDto
class EncryptHappCryptoLinkRequestDto(BaseModel):

View file

@ -109,8 +109,8 @@ class UpdateUserRequestDto(BaseModel):
class UserTrafficDto(BaseModel):
"""User traffic information"""
used_traffic_bytes: int = Field(alias="usedTrafficBytes")
lifetime_used_traffic_bytes: int = Field(alias="lifetimeUsedTrafficBytes")
used_traffic_bytes: float = Field(alias="usedTrafficBytes")
lifetime_used_traffic_bytes: float = Field(alias="lifetimeUsedTrafficBytes")
online_at: Optional[datetime] = Field(None, alias="onlineAt")
first_connected_at: Optional[datetime] = Field(None, alias="firstConnectedAt")
last_connected_node_uuid: Optional[UUID] = Field(None, alias="lastConnectedNodeUuid")
@ -149,12 +149,12 @@ class UserResponseDto(BaseModel):
user_traffic: UserTrafficDto = Field(alias="userTraffic")
@property
def used_traffic_bytes(self) -> int:
def used_traffic_bytes(self) -> float:
"""Backward compatibility property"""
return self.user_traffic.used_traffic_bytes
@property
def lifetime_used_traffic_bytes(self) -> int:
def lifetime_used_traffic_bytes(self) -> float:
"""Backward compatibility property"""
return self.user_traffic.lifetime_used_traffic_bytes

View file

@ -139,7 +139,7 @@ class BulkAllExtendExpirationDateRequestDto(BaseModel):
# Base Response DTOs (без обертки response)
class BulkResponseData(BaseModel):
"""Common bulk response with affected rows"""
affected_rows: int = Field(alias="affectedRows")
affected_rows: float = Field(alias="affectedRows")
class BulkEventResponseData(BaseModel):