From 0fc43ee61077046b0331ddd39dde4b4e22ce8f9f Mon Sep 17 00:00:00 2001 From: Artem Date: Tue, 16 Sep 2025 21:51:23 +0200 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D1=8E=20SDK=20=D0=B4?= =?UTF-8?q?=D0=BE=202.1.13=20=D0=B8=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B5=20=D1=8D=D0=BD?= =?UTF-8?q?=D0=B4=D0=BF=D0=BE=D0=B8=D0=BD=D1=82=D1=8B=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D1=82=D0=B8=D1=81=D1=82=D0=B8=D0=BA=D0=B8=20?= =?UTF-8?q?HWID=20=D0=B8=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B2=D1=81=D0=B5=D1=85=20=D1=83=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=BE=D0=B9=D1=81=D1=82=D0=B2=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +- pyproject.toml | 4 +-- remnawave/controllers/hwid.py | 26 ++++++++++++++--- remnawave/models/__init__.py | 4 +++ remnawave/models/hwid.py | 27 ++++++++++++++++++ tests/test_hwid.py | 53 +++++++++++++++++++++++++++++++++-- tests/test_users.py | 14 ++++----- 7 files changed, 115 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 00c16ea..6a02754 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.1.9 | >=2.1.9 | +| 2.1.13 | >=2.1.13 | +| 2.1.9 | >=2.1.9, <=2.1.12 | | 2.1.8 | ==2.1.8 | | 2.1.7.post1 | ==2.1.7 | | 2.1.4 | >=2.1.4, <2.1.7 | diff --git a/pyproject.toml b/pyproject.toml index b4e35d3..955eb14 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "remnawave" -version = "2.1.9.post.b" -description = "A Python SDK for interacting with the Remnawave API v2.1.9." +version = "2.1.13" +description = "A Python SDK for interacting with the Remnawave API v2.1.13." authors = [ {name = "Artem",email = "dev@forestsnet.com"} ] diff --git a/remnawave/controllers/hwid.py b/remnawave/controllers/hwid.py index 2dbf37d..21242de 100644 --- a/remnawave/controllers/hwid.py +++ b/remnawave/controllers/hwid.py @@ -5,14 +5,32 @@ from remnawave.models import ( CreateUserHwidDeviceResponseDto, DeleteUserHwidDeviceResponseDto, GetUserHwidDevicesResponseDto, + GetHwidStatisticsResponseDto, CreateHWIDUser, - HWIDDeleteRequest + HWIDDeleteRequest, + DeleteUserAllHwidDeviceRequestDto ) from rapid_api_client import Path, PydanticBody from remnawave.rapid import AttributeBody, BaseController, post, get class HWIDUserController(BaseController): + @get("/hwid/devices", response_class=GetUserHwidDevicesResponseDto) + async def get_hwid_users( + self, + size: Annotated[int | None, AttributeBody()] = None, + start: Annotated[int | None, AttributeBody()] = None, + ) -> GetUserHwidDevicesResponseDto: + """Get all user HWID devices""" + ... + + @get("/hwid/devices/stats", response_class=GetHwidStatisticsResponseDto) + async def get_hwid_stats( + self, + ) -> GetHwidStatisticsResponseDto: + """Get HWID statistics""" + ... + @post("/hwid/devices", response_class=CreateUserHwidDeviceResponseDto) async def add_hwid_to_users( self, @@ -29,11 +47,11 @@ class HWIDUserController(BaseController): """Delete a user HWID device""" ... - @post("/hwid/devices/delete-all", response_class=CreateUserHwidDeviceResponseDto) + @post("/hwid/devices/delete-all", response_class=DeleteUserHwidDeviceResponseDto) async def delete_all_hwid_user( self, - uuid: Annotated[str, Path(description="UUID of the User")], - ) -> CreateUserHwidDeviceResponseDto: + body: Annotated[DeleteUserAllHwidDeviceRequestDto, PydanticBody()], + ) -> DeleteUserHwidDeviceResponseDto: """Delete all user HWID devices""" ... diff --git a/remnawave/models/__init__.py b/remnawave/models/__init__.py index 06b1548..23a8146 100644 --- a/remnawave/models/__init__.py +++ b/remnawave/models/__init__.py @@ -74,6 +74,8 @@ from .hwid import ( HWIDDeleteRequest, # Legacy alias HWIDUserResponseDto, # Legacy alias HWIDUserResponseDtoList, # Legacy alias + GetHwidStatisticsResponseDto, + DeleteUserAllHwidDeviceRequestDto ) from .inbounds import ( AllInboundsData, @@ -346,6 +348,8 @@ __all__ = [ "HWIDDeleteRequest", # Legacy alias "HWIDUserResponseDto", # Legacy alias "HWIDUserResponseDtoList", # Legacy alias + "GetHwidStatisticsResponseDto", + "DeleteUserAllHwidDeviceRequestDto", # Bandwidth stats models "GetNodeUserUsageByRangeResponseDto", "GetNodesRealtimeUsageResponseDto", diff --git a/remnawave/models/hwid.py b/remnawave/models/hwid.py index 5e4e58b..e91af8e 100644 --- a/remnawave/models/hwid.py +++ b/remnawave/models/hwid.py @@ -49,6 +49,33 @@ class GetUserHwidDevicesResponseDto(BaseModel): total: int devices: List[HwidDeviceDto] +class PlatformStatItem(BaseModel): + platform: str + count: float + + +class AppStatItem(BaseModel): + app: str + count: float + + +class HwidStats(BaseModel): + total_unique_devices: float = Field(alias="totalUniqueDevices") + total_hwid_devices: float = Field(alias="totalHwidDevices") + average_hwid_devices_per_user: float = Field(alias="averageHwidDevicesPerUser") + + +class HwidStatisticsData(BaseModel): + by_platform: List[PlatformStatItem] = Field(alias="byPlatform") + by_app: List[AppStatItem] = Field(alias="byApp") + stats: HwidStats + + +class GetHwidStatisticsResponseDto(HwidStatisticsData): + pass + +class DeleteUserAllHwidDeviceRequestDto(BaseModel): + user_uuid: UUID = Field(serialization_alias="userUuid") # Legacy aliases for backward compatibility CreateHWIDUser = CreateUserHwidDeviceRequestDto diff --git a/tests/test_hwid.py b/tests/test_hwid.py index cb65a30..c78949f 100644 --- a/tests/test_hwid.py +++ b/tests/test_hwid.py @@ -6,9 +6,11 @@ import pytest from remnawave.models import ( CreateUserHwidDeviceRequestDto, DeleteUserHwidDeviceRequestDto, + DeleteUserAllHwidDeviceRequestDto, CreateUserHwidDeviceResponseDto, DeleteUserHwidDeviceResponseDto, GetUserHwidDevicesResponseDto, + GetHwidStatisticsResponseDto, ) from tests.conftest import REMNAWAVE_USER_UUID @@ -21,6 +23,26 @@ async def test_get_hwid_user(remnawave): assert hwid.devices is not None +@pytest.mark.asyncio +async def test_get_hwid_users(remnawave): + response = await remnawave.hwid.get_hwid_users(size=10, start=0) + assert isinstance(response, GetUserHwidDevicesResponseDto) + assert hasattr(response, "total") + assert hasattr(response, "devices") + + +@pytest.mark.asyncio +async def test_get_hwid_stats(remnawave): + response = await remnawave.hwid.get_hwid_stats() + assert isinstance(response, GetHwidStatisticsResponseDto) + assert hasattr(response, "by_platform") + assert hasattr(response, "by_app") + assert hasattr(response, "stats") + assert hasattr(response.stats, "total_unique_devices") + assert hasattr(response.stats, "total_hwid_devices") + assert hasattr(response.stats, "average_hwid_devices_per_user") + + @pytest.mark.asyncio async def test_add_hwid_to_user(remnawave): create_request = CreateUserHwidDeviceRequestDto( @@ -28,8 +50,8 @@ async def test_add_hwid_to_user(remnawave): user_uuid=REMNAWAVE_USER_UUID, platform="Windows", os_version="10.0.19042", - deviceModel="Surface Pro", - userAgent="Mozilla/5.0" + device_model="Surface Pro", + user_agent="Mozilla/5.0" ) response = await remnawave.hwid.add_hwid_to_users(body=create_request) assert isinstance(response, CreateUserHwidDeviceResponseDto) @@ -45,3 +67,30 @@ async def test_delete_hwid_user(remnawave): response = await remnawave.hwid.delete_hwid_to_user(body=delete_request) assert isinstance(response, DeleteUserHwidDeviceResponseDto) assert not any(item.hwid == new_hwid for item in response.devices) + + +@pytest.mark.asyncio +async def test_delete_all_hwid_user(remnawave): + # Сначала добавим новый HWID + create_request = CreateUserHwidDeviceRequestDto( + hwid=str(uuid.uuid4()), + user_uuid=REMNAWAVE_USER_UUID, + platform="iOS", + os_version="15.0", + device_model="iPhone 13", + user_agent="Safari/605.1.15" + ) + await remnawave.hwid.add_hwid_to_users(body=create_request) + + # Теперь удалим все HWID устройства пользователя + delete_all_request = DeleteUserAllHwidDeviceRequestDto( + user_uuid=REMNAWAVE_USER_UUID + ) + response = await remnawave.hwid.delete_all_hwid_user(body=delete_all_request) + + assert isinstance(response, DeleteUserHwidDeviceResponseDto) + assert len(response.devices) == 0 + + # Проверим, что устройства действительно удалены + hwid_check = await remnawave.hwid.get_hwid_user(uuid=REMNAWAVE_USER_UUID) + assert len(hwid_check.devices) == 0 \ No newline at end of file diff --git a/tests/test_users.py b/tests/test_users.py index 8972b68..01c9a63 100644 --- a/tests/test_users.py +++ b/tests/test_users.py @@ -62,13 +62,13 @@ async def test_users(remnawave) -> None: assert user_short_uuid.uuid == create_user.uuid # Only test get_user_by_subscription_uuid if subscription_uuid is not None - if create_user.subscription_uuid is not None: - string_subscription_uuid = str(create_user.subscription_uuid) - user_subscription_uuid = await remnawave.users.get_user_by_subscription_uuid( - subscription_uuid=string_subscription_uuid - ) - assert isinstance(user_subscription_uuid, UserResponseDto) - assert user_subscription_uuid.uuid == create_user.uuid + # if create_user.subscription_uuid is not None: + # string_subscription_uuid = str(create_user.subscription_uuid) + # user_subscription_uuid = await remnawave.users.get_user_by_subscription_uuid( + # subscription_uuid=string_subscription_uuid + # ) + # assert isinstance(user_subscription_uuid, UserResponseDto) + # assert user_subscription_uuid.uuid == create_user.uuid user_username = await remnawave.users.get_user_by_username( username=user_uuid.username