Merge pull request #28 from remnawave:development

Update API versions and add new models for HWID and users
This commit is contained in:
Artem 2025-12-10 05:39:55 +01:00 committed by GitHub
commit 95a93d72d5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 220 additions and 19 deletions

View file

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

View file

@ -1,7 +1,7 @@
[project]
name = "remnawave"
version = "2.3.0"
description = "A Python SDK for interacting with the Remnawave API v2.3.0."
version = "2.3.2"
description = "A Python SDK for interacting with the Remnawave API v2.3.2."
authors = [
{name = "Artem",email = "dev@forestsnet.com"}
]

View file

@ -1,4 +1,4 @@
from typing import Annotated
from typing import Annotated, Optional
from uuid import UUID
from remnawave.models import (
@ -8,9 +8,10 @@ from remnawave.models import (
GetHwidStatisticsResponseDto,
CreateHWIDUser,
HWIDDeleteRequest,
DeleteUserAllHwidDeviceRequestDto
DeleteUserAllHwidDeviceRequestDto,
GetTopUsersByHwidDevicesResponseDto
)
from rapid_api_client import Path, PydanticBody
from rapid_api_client import Path, PydanticBody, Query
from remnawave.rapid import AttributeBody, BaseController, post, get
@ -62,3 +63,12 @@ class HWIDUserController(BaseController):
) -> GetUserHwidDevicesResponseDto:
"""Get a user HWID device"""
...
@get("/hwid/devices/top-users", response_class=GetTopUsersByHwidDevicesResponseDto)
async def get_top_users_by_hwid_devices(
self,
size: Annotated[Optional[int], Query(default=None, description="Page size for pagination")] = None,
start: Annotated[Optional[int], Query(default=None, description="Offset for pagination")] = None,
) -> GetTopUsersByHwidDevicesResponseDto:
"""Get top users by HWID devices"""
...

View file

@ -84,7 +84,10 @@ from .hwid import (
HWIDUserResponseDto, # Legacy alias
HWIDUserResponseDtoList, # Legacy alias
GetHwidStatisticsResponseDto,
DeleteUserAllHwidDeviceRequestDto
DeleteUserAllHwidDeviceRequestDto,
GetTopUsersByHwidDevicesResponseDto,
TopUserByHwidDevicesDto,
TopUsersByHwidDevicesData,
)
from .inbounds import (
AllInboundsData,
@ -351,6 +354,7 @@ from .webhook import (
CustomErrorEventDto,
CrmEventDto,
WebhookPayloadDto,
UserTrafficDto
)
from .passkeys import (
DeletePasskeyRequestDto,
@ -545,6 +549,9 @@ __all__ = [
"HWIDUserResponseDtoList", # Legacy alias
"GetHwidStatisticsResponseDto",
"DeleteUserAllHwidDeviceRequestDto",
"GetTopUsersByHwidDevicesResponseDto",
"TopUserByHwidDevicesDto",
"TopUsersByHwidDevicesData",
# Bandwidth stats models
"GetNodeUserUsageByRangeResponseDto",
"GetNodesRealtimeUsageResponseDto",
@ -712,6 +719,7 @@ __all__ = [
"BaseUserDto",
"UserDto",
"UserEventDto",
"UserTrafficDto",
# HWID DEVICES
"HwidUserDeviceDto",

View file

@ -24,6 +24,14 @@ class NodesUsageResponseDto(RootModel[List[NodeUsageResponseDto]]):
def __getitem__(self, item):
return self.root[item]
def __bool__(self):
"""Return True if list is not empty"""
return bool(self.root)
def __len__(self):
"""Return length of list"""
return len(self.root)
class GetNodesUsageByRangeResponseDto(RootModel[List[NodeUsageResponseDto]]):
def __iter__(self):
@ -32,6 +40,14 @@ class GetNodesUsageByRangeResponseDto(RootModel[List[NodeUsageResponseDto]]):
def __getitem__(self, item):
return self.root[item]
def __bool__(self):
"""Return True if list is not empty"""
return bool(self.root)
def __len__(self):
"""Return length of list"""
return len(self.root)
class NodeRealtimeUsageResponseDto(BaseModel):
node_uuid: UUID = Field(alias="nodeUuid")
@ -52,6 +68,14 @@ class NodesRealtimeUsageResponseDto(RootModel[List[NodeRealtimeUsageResponseDto]
def __getitem__(self, item):
return self.root[item]
def __bool__(self):
"""Return True if list is not empty"""
return bool(self.root)
def __len__(self):
"""Return length of list"""
return len(self.root)
class GetNodesRealtimeUsageResponseDto(RootModel[List[NodeRealtimeUsageResponseDto]]):
def __iter__(self):
@ -60,6 +84,14 @@ class GetNodesRealtimeUsageResponseDto(RootModel[List[NodeRealtimeUsageResponseD
def __getitem__(self, item):
return self.root[item]
def __bool__(self):
"""Return True if list is not empty"""
return bool(self.root)
def __len__(self):
"""Return length of list"""
return len(self.root)
class UserUsageByRangeItem(BaseModel):
user_uuid: UUID = Field(alias="userUuid")
@ -76,6 +108,14 @@ class GetUserUsageByRangeResponseDto(RootModel[List[UserUsageByRangeItem]]):
def __getitem__(self, item):
return self.root[item]
def __bool__(self):
"""Return True if list is not empty"""
return bool(self.root)
def __len__(self):
"""Return length of list"""
return len(self.root)
class NodeUserUsageItem(BaseModel):
user_uuid: UUID = Field(alias="userUuid")
@ -91,3 +131,11 @@ class GetNodeUserUsageByRangeResponseDto(RootModel[List[NodeUserUsageItem]]):
def __getitem__(self, item):
return self.root[item]
def __bool__(self):
"""Return True if list is not empty"""
return bool(self.root)
def __len__(self):
"""Return length of list"""
return len(self.root)

View file

@ -39,8 +39,6 @@ class ExternalSquadSubscriptionSettingsDto(BaseModel):
happ_routing: Optional[str] = Field(None, alias="happRouting")
randomize_hosts: bool = Field(alias="randomizeHosts")
# НОВЫЕ МОДЕЛИ
class ExternalSquadHostOverridesDto(BaseModel):
"""External squad host overrides"""
server_description: Optional[str] = Field(None, alias="serverDescription", max_length=30)
@ -61,7 +59,7 @@ class ExternalSquadDto(BaseModel):
# Request/Response models
class GetExternalSquadsResponseDto(ExternalSquadDto):
class GetExternalSquadsResponseDto(BaseModel):
"""Response with all external squads"""
total: int = Field(alias="total")
external_squads: List[ExternalSquadDto] = Field(alias="externalSquads")

View file

@ -43,6 +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")
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")
@ -83,6 +84,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")
allow_insecure: bool = Field(False, alias="allowInsecure")
xray_json_template_uuid: Optional[UUID] = Field(None, alias="xrayJsonTemplateUuid")
excluded_internal_squads: List[UUID] = Field(default_factory=list, alias="excludedInternalSquads")
@ -116,6 +118,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")
xray_json_template_uuid: Optional[UUID] = Field(None, serialization_alias="xrayJsonTemplateUuid")
excluded_internal_squads: List[UUID] = Field(default_factory=list, serialization_alias="excludedInternalSquads")
@ -162,6 +165,14 @@ class GetAllHostsResponseDto(RootModel[List[HostResponseDto]]):
def __getitem__(self, item):
return self.root[item]
def __bool__(self):
"""Return True if list is not empty"""
return bool(self.root)
def __len__(self):
"""Return length of list"""
return len(self.root)
class GetOneHostResponseDto(HostResponseDto):
"""Get one host response"""

View file

@ -77,6 +77,24 @@ class GetHwidStatisticsResponseDto(HwidStatisticsData):
class DeleteUserAllHwidDeviceRequestDto(BaseModel):
user_uuid: UUID = Field(serialization_alias="userUuid")
class TopUserByHwidDevicesDto(BaseModel):
"""Top user by HWID devices"""
user_uuid: UUID = Field(alias="userUuid")
id: int
username: str
devices_count: int = Field(alias="devicesCount")
class TopUsersByHwidDevicesData(BaseModel):
"""Top users by HWID devices data"""
users: list[TopUserByHwidDevicesDto]
total: int
class GetTopUsersByHwidDevicesResponseDto(TopUsersByHwidDevicesData):
"""Response for get top users by HWID devices"""
pass
# Legacy aliases for backward compatibility
CreateHWIDUser = CreateUserHwidDeviceRequestDto
HWIDUserResponseDto = HwidDeviceDto

View file

@ -42,6 +42,14 @@ class GetInboundsResponseDto(RootModel[List[InboundResponseDto]]):
def __getitem__(self, item):
return self.root[item]
def __bool__(self):
"""Return True if list is not empty"""
return bool(self.root)
def __len__(self):
"""Return length of list"""
return len(self.root)
class FullInboundStatistic(BaseModel):
enabled: float
@ -67,6 +75,14 @@ class GetFullInboundsResponseDto(RootModel[List[FullInboundResponseDto]]):
def __getitem__(self, item):
return self.root[item]
def __bool__(self):
"""Return True if list is not empty"""
return bool(self.root)
def __len__(self):
"""Return length of list"""
return len(self.root)
# Legacy aliases for backward compatibility
InboundsResponseDto = GetInboundsResponseDto

View file

@ -170,6 +170,15 @@ class GetAllNodesResponseDto(RootModel[List[NodeResponseDto]]):
def __getitem__(self, item):
return self.root[item]
def __bool__(self):
"""Return True if list is not empty"""
return bool(self.root)
def __len__(self):
"""Return length of list"""
return len(self.root)
class EnableNodeResponseDto(NodeResponseDto):
pass
@ -196,6 +205,14 @@ class ReorderNodeResponseDto(RootModel[List[NodeResponseDto]]):
def __getitem__(self, item):
return self.root[item]
def __bool__(self):
"""Return True if list is not empty"""
return bool(self.root)
def __len__(self):
"""Return length of list"""
return len(self.root)
class DeleteNodeResponseDto(BaseModel):
is_deleted: bool = Field(alias="isDeleted")

View file

@ -39,6 +39,14 @@ class GetNodesUsageByRangeResponseDto(RootModel[List[NodeUsageDto]]):
def __getitem__(self, item):
return self.root[item]
def __bool__(self):
"""Return True if list is not empty"""
return bool(self.root)
def __len__(self):
"""Return length of list"""
return len(self.root)
class UserUsageDto(BaseModel):
user_uuid: UUID = Field(alias="userUuid")
@ -53,3 +61,11 @@ class GetNodeUserUsageByRangeResponseDto(RootModel[List[UserUsageDto]]):
def __getitem__(self, item):
return self.root[item]
def __bool__(self):
"""Return True if list is not empty"""
return bool(self.root)
def __len__(self):
"""Return length of list"""
return len(self.root)

View file

@ -305,6 +305,14 @@ class EmailUserResponseDto(RootModel[list[UserResponseDto]]):
def __getitem__(self, item):
return self.root[item]
def __bool__(self):
"""Return True if list is not empty"""
return bool(self.root)
def __len__(self):
"""Return length of list"""
return len(self.root)
class TagUserResponseDto(RootModel[list[UserResponseDto]]):
"""Response for get users by tag"""
@ -314,6 +322,14 @@ class TagUserResponseDto(RootModel[list[UserResponseDto]]):
def __getitem__(self, item):
return self.root[item]
def __bool__(self):
"""Return True if list is not empty"""
return bool(self.root)
def __len__(self):
"""Return length of list"""
return len(self.root)
class TelegramUserResponseDto(RootModel[list[UserResponseDto]]):
"""Response for get users by telegram ID"""
@ -322,3 +338,11 @@ class TelegramUserResponseDto(RootModel[list[UserResponseDto]]):
def __getitem__(self, item):
return self.root[item]
def __bool__(self):
"""Return True if list is not empty"""
return bool(self.root)
def __len__(self):
"""Return length of list"""
return len(self.root)

View file

@ -23,13 +23,24 @@ class InternalSquadDto(BaseModel):
model_config = {"alias_generator": to_camel, "populate_by_name": True}
class UserTrafficDto(BaseModel):
"""User traffic information for webhooks"""
used_traffic_bytes: int
lifetime_used_traffic_bytes: int
online_at: Optional[datetime] = None
first_connected_at: Optional[datetime] = None
last_connected_node_uuid: Optional[UUID] = None
model_config = {"alias_generator": to_camel, "populate_by_name": True}
class BaseUserDto(BaseModel):
uuid: UUID
short_uuid: str
username: str
status: TUsersStatus
used_traffic_bytes: int
lifetime_used_traffic_bytes: int
user_traffic: UserTrafficDto
traffic_limit_bytes: int
traffic_limit_strategy: TResetPeriods
@ -50,18 +61,41 @@ class BaseUserDto(BaseModel):
email: Optional[str] = None
hwid_device_limit: Optional[int] = None
first_connected_at: Optional[datetime] = None
last_triggered_threshold: int
online_at: Optional[datetime] = None
last_connected_node_uuid: Optional[UUID] = None
created_at: datetime
updated_at: datetime
model_config = {"alias_generator": to_camel, "populate_by_name": True}
# Backward compatibility properties
@property
def used_traffic_bytes(self) -> int:
"""Backward compatibility property"""
return self.user_traffic.used_traffic_bytes
@property
def lifetime_used_traffic_bytes(self) -> int:
"""Backward compatibility property"""
return self.user_traffic.lifetime_used_traffic_bytes
@property
def online_at(self) -> Optional[datetime]:
"""Backward compatibility property"""
return self.user_traffic.online_at
@property
def first_connected_at(self) -> Optional[datetime]:
"""Backward compatibility property"""
return self.user_traffic.first_connected_at
@property
def last_connected_node_uuid(self) -> Optional[UUID]:
"""Backward compatibility property"""
return self.user_traffic.last_connected_node_uuid
class UserDto(BaseUserDto):
active_internal_squads: List[InternalSquadDto] = Field(default_factory=list)