diff --git a/README.md b/README.md index cb28358..e08f39a 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/pyproject.toml b/pyproject.toml index 0faaf8e..0a00229 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"} ] diff --git a/remnawave/controllers/hwid.py b/remnawave/controllers/hwid.py index 21242de..084d65c 100644 --- a/remnawave/controllers/hwid.py +++ b/remnawave/controllers/hwid.py @@ -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""" + ... \ No newline at end of file diff --git a/remnawave/models/__init__.py b/remnawave/models/__init__.py index 844b1d0..c5a1c5d 100644 --- a/remnawave/models/__init__.py +++ b/remnawave/models/__init__.py @@ -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", diff --git a/remnawave/models/bandwidthstats.py b/remnawave/models/bandwidthstats.py index 712e1eb..f7f49a8 100644 --- a/remnawave/models/bandwidthstats.py +++ b/remnawave/models/bandwidthstats.py @@ -23,6 +23,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]]): @@ -31,6 +39,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): @@ -51,6 +67,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]]): @@ -59,6 +83,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): @@ -75,6 +107,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): @@ -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) diff --git a/remnawave/models/external_squads.py b/remnawave/models/external_squads.py index 7e3b1e5..634256e 100644 --- a/remnawave/models/external_squads.py +++ b/remnawave/models/external_squads.py @@ -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") diff --git a/remnawave/models/hosts.py b/remnawave/models/hosts.py index 1ceb4d7..f456f17 100644 --- a/remnawave/models/hosts.py +++ b/remnawave/models/hosts.py @@ -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") @@ -161,6 +164,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): diff --git a/remnawave/models/hwid.py b/remnawave/models/hwid.py index e91af8e..8cf80a7 100644 --- a/remnawave/models/hwid.py +++ b/remnawave/models/hwid.py @@ -76,9 +76,27 @@ 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 HWIDUserResponseDtoList = HwidDevicesData -HWIDDeleteRequest = DeleteUserHwidDeviceRequestDto +HWIDDeleteRequest = DeleteUserHwidDeviceRequestDto \ No newline at end of file diff --git a/remnawave/models/inbounds.py b/remnawave/models/inbounds.py index 0b375cb..c74ed3c 100644 --- a/remnawave/models/inbounds.py +++ b/remnawave/models/inbounds.py @@ -41,6 +41,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): @@ -66,6 +74,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 diff --git a/remnawave/models/nodes.py b/remnawave/models/nodes.py index 871df82..500530b 100644 --- a/remnawave/models/nodes.py +++ b/remnawave/models/nodes.py @@ -169,6 +169,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): @@ -195,6 +204,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): diff --git a/remnawave/models/nodes_usage_history.py b/remnawave/models/nodes_usage_history.py index cfa55b2..c827219 100644 --- a/remnawave/models/nodes_usage_history.py +++ b/remnawave/models/nodes_usage_history.py @@ -38,6 +38,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): @@ -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) diff --git a/remnawave/models/users.py b/remnawave/models/users.py index f817d58..12c544b 100644 --- a/remnawave/models/users.py +++ b/remnawave/models/users.py @@ -304,6 +304,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]]): @@ -313,6 +321,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]]): @@ -321,4 +337,12 @@ class TelegramUserResponseDto(RootModel[list[UserResponseDto]]): return iter(self.root) def __getitem__(self, item): - return self.root[item] \ No newline at end of file + 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) \ No newline at end of file diff --git a/remnawave/models/webhook.py b/remnawave/models/webhook.py index 83c7604..34b15b1 100644 --- a/remnawave/models/webhook.py +++ b/remnawave/models/webhook.py @@ -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)