Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[project]
name = "remnawave"
version = "2.1.9"
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"}
]
Expand Down Expand Up @@ -56,4 +56,4 @@ asyncio_default_fixture_loop_scope = "function"

[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"
build-backend = "poetry.core.masonry.api"
34 changes: 30 additions & 4 deletions remnawave/controllers/hwid.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,60 @@
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,
body: Annotated[CreateHWIDUser, PydanticBody()],
) -> CreateUserHwidDeviceResponseDto:
"""Create a user HWID device"""
...

@post("/hwid/devices/delete", response_class=DeleteUserHwidDeviceResponseDto)
async def delete_hwid_to_user(
self,
body: Annotated[HWIDDeleteRequest, PydanticBody()],
) -> DeleteUserHwidDeviceResponseDto:
"""Delete a user HWID device"""
...


@post("/hwid/devices/delete-all", response_class=DeleteUserHwidDeviceResponseDto)
async def delete_all_hwid_user(
self,
body: Annotated[DeleteUserAllHwidDeviceRequestDto, PydanticBody()],
) -> DeleteUserHwidDeviceResponseDto:
"""Delete all user HWID devices"""
...

@get("/hwid/devices/{uuid}", response_class=GetUserHwidDevicesResponseDto)
async def get_hwid_user(
self,
uuid: Annotated[str, Path(description="UUID of the User")],
) -> GetUserHwidDevicesResponseDto:
"""Get a user HWID device"""
...
...
4 changes: 4 additions & 0 deletions remnawave/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@
HWIDDeleteRequest, # Legacy alias
HWIDUserResponseDto, # Legacy alias
HWIDUserResponseDtoList, # Legacy alias
GetHwidStatisticsResponseDto,
DeleteUserAllHwidDeviceRequestDto
)
from .inbounds import (
AllInboundsData,
Expand Down Expand Up @@ -346,6 +348,8 @@
"HWIDDeleteRequest", # Legacy alias
"HWIDUserResponseDto", # Legacy alias
"HWIDUserResponseDtoList", # Legacy alias
"GetHwidStatisticsResponseDto",
"DeleteUserAllHwidDeviceRequestDto",
# Bandwidth stats models
"GetNodeUserUsageByRangeResponseDto",
"GetNodesRealtimeUsageResponseDto",
Expand Down
36 changes: 31 additions & 5 deletions remnawave/models/hwid.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,28 +31,54 @@ class HwidDeviceDto(BaseModel):


class HwidDevicesData(BaseModel):
total: float
total: int
devices: List[HwidDeviceDto]


class CreateUserHwidDeviceResponseDto(BaseModel):
total: float
total: int
devices: List[HwidDeviceDto]


class DeleteUserHwidDeviceResponseDto(BaseModel):
total: float
total: int
devices: List[HwidDeviceDto]


class GetUserHwidDevicesResponseDto(BaseModel):
total: float
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
HWIDUserResponseDto = HwidDeviceDto
HWIDUserResponseDtoList = HwidDevicesData
HWIDDeleteRequest = DeleteUserHwidDeviceRequestDto

112 changes: 54 additions & 58 deletions remnawave/models/users.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from datetime import datetime
from typing import Annotated, List, Optional
from typing import Annotated
from uuid import UUID

from pydantic import (
Expand All @@ -16,8 +16,8 @@ class UserActiveInboundsDto(BaseModel):
uuid: UUID
tag: str
type: str
network: Optional[str] = None
security: Optional[str] = None
network: str | None = None
security: str | None = None


class UserLastConnectedNodeDto(BaseModel):
Expand All @@ -39,119 +39,115 @@ class CreateUserRequestDto(BaseModel):
username: Annotated[
str, StringConstraints(pattern=r"^[a-zA-Z0-9_-]+$", min_length=3, max_length=36)
]
created_at: Optional[datetime] = Field(None, serialization_alias="createdAt")
status: Optional[UserStatus] = None
subscription_uuid: Optional[str] = Field(
None, serialization_alias="subscriptionUuid"
)
short_uuid: Optional[str] = Field(None, serialization_alias="shortUuid")
created_at: datetime | None = Field(None, serialization_alias="createdAt")
status: UserStatus | None = None
short_uuid: str | None = Field(None, serialization_alias="shortUuid")
trojan_password: Annotated[
Optional[str], StringConstraints(min_length=8, max_length=32)
str | None, StringConstraints(min_length=8, max_length=32)
] = Field(None, serialization_alias="trojanPassword")
vless_uuid: Optional[str] = Field(None, serialization_alias="vlessUuid")
vless_uuid: str | None = Field(None, serialization_alias="vlessUuid")
ss_password: Annotated[
Optional[str], StringConstraints(min_length=8, max_length=32)
str | None, StringConstraints(min_length=8, max_length=32)
] = Field(None, serialization_alias="ssPassword")
traffic_limit_bytes: Optional[int] = Field(
traffic_limit_bytes: int | None = Field(
None, serialization_alias="trafficLimitBytes", strict=True, ge=0
)
traffic_limit_strategy: Optional[TrafficLimitStrategy] = Field(
traffic_limit_strategy: TrafficLimitStrategy | None = Field(
None, serialization_alias="trafficLimitStrategy"
)
last_traffic_reset_at: Optional[datetime] = Field(
last_traffic_reset_at: datetime | None = Field(
None, serialization_alias="lastTrafficResetAt"
)
description: Optional[str] = None
tag: Optional[str] = None
telegram_id: Optional[int] = Field(None, serialization_alias="telegramId")
email: Optional[str] = None
hwidDeviceLimit: Optional[int] = Field(
description: str | None = None
tag: str | None = None
telegram_id: int | None = Field(None, serialization_alias="telegramId")
email: str | None = None
hwidDeviceLimit: int | None = Field(
None, serialization_alias="hwidDeviceLimit", strict=True, ge=0
)
active_internal_squads: Optional[List[str]] = Field(
active_internal_squads: list[str] | None = Field(
None, serialization_alias="activeInternalSquads"
)


class UpdateUserRequestDto(BaseModel):
uuid: UUID
active_internal_squads: Optional[List[str]] = Field(
active_internal_squads: list[str] | None = Field(
None, serialization_alias="activeInternalSquads"
)
description: Optional[str] = None
email: Optional[str] = None
expire_at: Optional[datetime] = Field(None, serialization_alias="expireAt")
hwidDeviceLimit: Optional[int] = Field(
description: str | None = None
email: str | None = None
expire_at: datetime | None = Field(None, serialization_alias="expireAt")
hwidDeviceLimit: int | None = Field(
None, serialization_alias="hwidDeviceLimit", strict=True, ge=0
)
status: Optional[UserStatus] = None
tag: Optional[str] = None
telegram_id: Optional[int] = Field(None, serialization_alias="telegramId")
traffic_limit_bytes: Optional[int] = Field(
status: UserStatus | None = None
tag: str | None = None
telegram_id: int | None = Field(None, serialization_alias="telegramId")
traffic_limit_bytes: int | None = Field(
None, serialization_alias="trafficLimitBytes", strict=True, ge=0
)
traffic_limit_strategy: Optional[TrafficLimitStrategy] = Field(
traffic_limit_strategy: TrafficLimitStrategy | None = Field(
None, serialization_alias="trafficLimitStrategy"
)


class UserResponseDto(BaseModel):
uuid: UUID
Comment thread
sm1ky marked this conversation as resolved.
subscription_uuid: Optional[UUID] = Field(None, alias="subscriptionUuid")
short_uuid: str = Field(alias="shortUuid")
username: str
status: Optional[UserStatus] = None
status: UserStatus | None = None
used_traffic_bytes: float = Field(alias="usedTrafficBytes")
lifetime_used_traffic_bytes: float = Field(alias="lifetimeUsedTrafficBytes")
traffic_limit_bytes: Optional[int] = Field(None, alias="trafficLimitBytes")
traffic_limit_strategy: Optional[str] = Field(None, alias="trafficLimitStrategy")
sub_last_user_agent: Optional[str] = Field(None, alias="subLastUserAgent")
sub_last_opened_at: Optional[datetime] = Field(None, alias="subLastOpenedAt")
expire_at: Optional[datetime] = Field(None, alias="expireAt")
online_at: Optional[datetime] = Field(None, alias="onlineAt")
sub_revoked_at: Optional[datetime] = Field(None, alias="subRevokedAt")
last_traffic_reset_at: Optional[datetime] = Field(None, alias="lastTrafficResetAt")
traffic_limit_bytes: int | None = Field(None, alias="trafficLimitBytes")
traffic_limit_strategy: str | None = Field(None, alias="trafficLimitStrategy")
sub_last_user_agent: str | None = Field(None, alias="subLastUserAgent")
sub_last_opened_at: datetime | None = Field(None, alias="subLastOpenedAt")
expire_at: datetime | None = Field(None, alias="expireAt")
online_at: datetime | None = Field(None, alias="onlineAt")
sub_revoked_at: datetime | None = Field(None, alias="subRevokedAt")
last_traffic_reset_at: datetime | None = Field(None, alias="lastTrafficResetAt")
trojan_password: str = Field(alias="trojanPassword")
vless_uuid: UUID = Field(alias="vlessUuid")
ss_password: str = Field(alias="ssPassword")
description: Optional[str] = None
telegram_id: Optional[int] = Field(None, alias="telegramId")
email: Optional[str] = None
hwidDeviceLimit: Optional[int] = Field(
description: str | None = None
telegram_id: int | None = Field(None, alias="telegramId")
email: str | None = None
hwidDeviceLimit: int | None = Field(
None, serialization_alias="hwidDeviceLimit", strict=True, ge=0
)
active_internal_squads: Optional[List[ActiveInternalSquadDto]] = Field(
active_internal_squads: list[ActiveInternalSquadDto] | None = Field(
None, alias="activeInternalSquads"
)
subscription_url: str = Field(alias="subscriptionUrl")
first_connected: Optional[datetime] = Field(None, alias="firstConnectedAt")
last_trigger_threshold: Optional[int] = Field(None, alias="lastTriggeredThreshold")
last_connected_node: Optional[UserLastConnectedNodeDto] = Field(
subscription_url: str | None = Field(None, alias="subscriptionUrl")
Copy link

Copilot AI Sep 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The subscription_url field type changed from str (required) to str | None (optional). This is a breaking change that could cause issues for consumers expecting this field to always be present.

Suggested change
subscription_url: str | None = Field(None, alias="subscriptionUrl")
subscription_url: str = Field(alias="subscriptionUrl")

Copilot uses AI. Check for mistakes.
first_connected: datetime | None = Field(None, alias="firstConnectedAt")
last_trigger_threshold: int | None = Field(None, alias="lastTriggeredThreshold")
last_connected_node: UserLastConnectedNodeDto | None = Field(
None, alias="lastConnectedNode"
)
happ: Optional[HappCrypto] = Field(None, alias="happ")
tag: Optional[str] = Field(None, alias="tag")
happ: HappCrypto | None = Field(None, alias="happ")
tag: str | None = Field(None, alias="tag")
created_at: datetime = Field(alias="createdAt")
updated_at: datetime = Field(alias="updatedAt")


class EmailUserResponseDto(RootModel[List[UserResponseDto]]):
class EmailUserResponseDto(RootModel[list[UserResponseDto]]):
def __iter__(self):
return iter(self.root)

def __getitem__(self, item):
return self.root[item]


class TagUserResponseDto(RootModel[List[UserResponseDto]]):
class TagUserResponseDto(RootModel[list[UserResponseDto]]):
def __iter__(self):
return iter(self.root)

def __getitem__(self, item):
return self.root[item]


class TelegramUserResponseDto(RootModel[List[UserResponseDto]]):
class TelegramUserResponseDto(RootModel[list[UserResponseDto]]):
def __iter__(self):
return iter(self.root)

Expand All @@ -160,7 +156,7 @@ def __getitem__(self, item):


class UsersResponseDto(BaseModel):
users: List[UserResponseDto]
users: list[UserResponseDto]
total: float


Expand All @@ -169,7 +165,7 @@ class DeleteUserResponseDto(BaseModel):


class TagsResponseDto(BaseModel):
tags: List[str]
tags: list[str]


class CreateUserResponseDto(UserResponseDto):
Expand Down Expand Up @@ -213,7 +209,7 @@ class GetUserByUsernameResponseDto(UserResponseDto):


class RevokeUserRequestDto(BaseModel):
short_uuid: Optional[str] = Field(
short_uuid: str | None = Field(
None,
serialization_alias="shortUuid",
description="Optional. If not provided, a new short UUID will be generated by Remnawave. Please note that it is strongly recommended to allow Remnawave to generate the short UUID.",
Expand Down
Loading