Merge pull request #2 from remnawave/development

Add new endpoints and update SDK features
This commit is contained in:
Artem 2025-08-27 00:29:11 +02:00 committed by GitHub
commit a863369675
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 153 additions and 34 deletions

View file

@ -63,7 +63,8 @@ pip install git+https://github.com/remnawave/python-sdk.git@development
| Contract Version | Remnawave Panel Version |
| ---------------- | ----------------------- |
| 2.1.1. | >=2.1.1 |
| 2.1.4 | >=2.1.4 |
| 2.1.1 | >=2.1.1, <2.1.4 |
| 2.0.0 | >=2.0.0,<2.1.0 |
| 1.1.3 | >=1.6.12,<2.0.0 |
| 1.1.2 | >=1.6.3,<=1.6.11 |

View file

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

View file

@ -39,20 +39,26 @@ class RemnawaveSDK:
base_url: Optional[str] = None,
token: Optional[str] = None,
caddy_token: Optional[str] = None,
ssl_ignore: Optional[bool] = False,
custom_headers: Optional[dict] = None,
):
"""
Remnawave SDK init
Args:
client (Optional[httpx.AsyncClient]): - Default client.
client (Optional[httpx.AsyncClient]): - Default client.
base_url (Optional[str]): - Base url of the Remnawave panel. Defaults to None.
token (Optional[str]): - Token for authorization.
token (Optional[str]): - Token for authorization.
caddy_token (Optional[str]): - Token for Caddy Auth (Headers). Defaults to None.
ssl_ignore (Optional[bool]): - Whether to ignore SSL certificate errors. Defaults to False.
custom_headers (Optional[dict]): - Custom headers to include in the requests. Defaults to None.
"""
self._client = client
self._token = token
self.base_url = base_url
self.caddy_token = caddy_token
self.ssl_ignore = ssl_ignore
self.custom_headers = custom_headers
self._validate_params()
@ -98,20 +104,27 @@ class RemnawaveSDK:
def _prepare_client(self) -> httpx.AsyncClient:
return httpx.AsyncClient(
base_url=self._prepare_url(), headers=self._prepare_headers()
base_url=self._prepare_url(),
headers=self._prepare_headers(),
verify=not self.ssl_ignore,
)
def _prepare_headers(self) -> dict:
headers = {}
if self._token:
headers["Authorization"] = (
self._token if self._token.startswith("Bearer ") else f"Bearer {self._token}"
self._token
if self._token.startswith("Bearer ")
else f"Bearer {self._token}"
)
# X-Api-Key for Caddy (https://remna.st/security/caddy-with-custom-path#issuing-api-keys)
if self.caddy_token is not None:
headers["X-Api-Key"] = self.caddy_token
if self.custom_headers:
headers.update(self.custom_headers)
return headers
def _prepare_url(self) -> str:

View file

@ -17,6 +17,7 @@ from remnawave.models import (
RestartNodeResponseDto,
UpdateNodeRequestDto,
UpdateNodeResponseDto,
RestartAllNodesRequestDto,
)
from remnawave.rapid import BaseController, delete, get, patch, post
@ -44,7 +45,7 @@ class NodesController(BaseController):
) -> GetOneNodeResponseDto:
"""Get One Node"""
...
@delete("/nodes/{uuid}", response_class=DeleteNodeResponseDto)
async def delete_node(
self,
@ -52,7 +53,7 @@ class NodesController(BaseController):
) -> DeleteNodeResponseDto:
"""Delete Node"""
...
@patch("/nodes", response_class=UpdateNodeResponseDto)
async def update_node(
self,
@ -88,6 +89,7 @@ class NodesController(BaseController):
@post("/nodes/actions/restart-all", response_class=RestartAllNodesResponseDto)
async def restart_all_nodes(
self,
body: Annotated[RestartAllNodesRequestDto, PydanticBody()],
) -> RestartAllNodesResponseDto:
"""Restart All Nodes"""
...

View file

@ -3,7 +3,7 @@ from typing import Annotated
from rapid_api_client import Path
from remnawave.enums import ClientType
from remnawave.models import GetSubscriptionInfoResponseDto
from remnawave.models import GetSubscriptionInfoResponseDto, GetRawSubscriptionByShortUuidResponseDto
from remnawave.rapid import BaseController, get
@ -52,3 +52,13 @@ class SubscriptionController(BaseController):
) -> str:
"""None"""
...
# get raw sub by short uuid
@get("/sub/{short_uuid}/raw", response_class=GetRawSubscriptionByShortUuidResponseDto)
async def get_raw_subscription(
self,
short_uuid: Annotated[str, Path(description="Short UUID of the user")],
withDisabledHosts: Annotated[Annotated[bool, Path(description="Include disabled hosts")], bool] = False,
) -> GetRawSubscriptionByShortUuidResponseDto:
"""None"""
...

View file

@ -13,7 +13,8 @@ from remnawave.models import (
UserResponseDto,
UsersResponseDto,
TagsResponseDto,
RevokeUserRequestDto
TagUserResponseDto,
RevokeUserRequestDto,
)
from remnawave.rapid import BaseController, delete, get, patch, post
@ -38,8 +39,12 @@ class UsersController(BaseController):
@get("/users", response_class=UsersResponseDto)
async def get_all_users_v2(
self,
start: Annotated[int, Query(default=0, ge=0, description="Index to start pagination from")],
size: Annotated[int, Query(default=25, ge=1, description="Number of users per page")],
start: Annotated[
int, Query(default=0, ge=0, description="Index to start pagination from")
],
size: Annotated[
int, Query(default=25, ge=1, description="Number of users per page")
],
) -> UsersResponseDto:
"""
Get users page from the end.
@ -52,7 +57,7 @@ class UsersController(BaseController):
UsersResponseDto
"""
...
@delete("/users/{uuid}", response_class=DeleteUserResponseDto)
async def delete_user(
self,
@ -93,14 +98,6 @@ class UsersController(BaseController):
) -> UserResponseDto:
"""Reset User Traffic"""
...
@post("/users/{uuid}/actions/activate-all-inbounds", response_class=UserResponseDto)
async def activate_all_inbounds(
self,
uuid: Annotated[str, Path(description="UUID of the user")],
) -> UserResponseDto:
"""Activate All Inbounds"""
...
@get("/users/by-short-uuid/{short_uuid}", response_class=UserResponseDto)
async def get_user_by_short_uuid(
@ -110,7 +107,10 @@ class UsersController(BaseController):
"""Get User By Short UUID"""
...
@get("/users/by-subscription-uuid/{subscription_uuid}", response_class=UserResponseDto)
@get(
"/users/by-subscription-uuid/{subscription_uuid}",
response_class=UserResponseDto,
)
async def get_user_by_subscription_uuid(
self,
subscription_uuid: Annotated[str, Path(description="UUID of the subscription")],
@ -153,14 +153,14 @@ class UsersController(BaseController):
"""Get Users By Email"""
...
@get("/users/by-tag/{tag}", response_class=UsersResponseDto)
@get("/users/by-tag/{tag}", response_class=TagUserResponseDto)
async def get_users_by_tag(
self,
tag: Annotated[str, Path(description="Tag of the user")],
) -> UsersResponseDto:
) -> TagUserResponseDto:
"""Get Users By Tag"""
...
@get("/users/tags", response_class=TagsResponseDto)
async def get_all_tags(
self,
@ -168,10 +168,13 @@ class UsersController(BaseController):
"""Get All Tags"""
...
@get("/users/{uuid}/accessible-nodes", response_class=GetUserAccessibleNodesResponseDto)
@get(
"/users/{uuid}/accessible-nodes",
response_class=GetUserAccessibleNodesResponseDto,
)
async def get_user_accessible_nodes(
self,
uuid: Annotated[str, Path(description="UUID of the user")],
) -> GetUserAccessibleNodesResponseDto:
"""Get User Accessible Nodes"""
...
...

View file

@ -151,6 +151,7 @@ from .nodes import (
RestartNodeResponseDto,
UpdateNodeRequestDto,
UpdateNodeResponseDto,
RestartAllNodesRequestDto,
)
from .nodes_usage_history import (
GetNodeUserUsageByRangeResponseDto,
@ -166,6 +167,7 @@ from .subscription import (
GetSubscriptionInfoResponseDto,
SubscriptionInfoResponseDto, # Legacy alias
UserSubscription,
GetRawSubscriptionByShortUuidResponseDto,
)
from .subscriptions_settings import (
GetSubscriptionSettingsResponseDto,
@ -208,6 +210,7 @@ from .users import (
UserResponseDto,
UsersResponseDto,
TagsResponseDto,
TagUserResponseDto,
RevokeUserRequestDto,
)
from .users_bulk_actions import (
@ -254,6 +257,9 @@ __all__ = [
"RestartNodeResponseDto",
"UpdateNodeRequestDto",
"UpdateNodeResponseDto",
"NodeConfigProfileDto",
"NodeConfigProfileRequestDto",
"RestartAllNodesRequestDto",
# Hosts models
"CreateHostRequestDto",
"CreateHostResponseDto",
@ -267,7 +273,11 @@ __all__ = [
"UpdateHostRequestDto",
"UpdateHostResponseDto",
"GetAllHostTagsResponseDto",
"CreateHostInboundData",
"HostInboundData",
"ReorderHostItem",
# Inbounds models
"AllInboundsData",
"FullInboundResponseDto",
"FullInboundStatistic",
"FullInboundsResponseDto",
@ -276,6 +286,7 @@ __all__ = [
"InboundResponseDto",
"InboundsResponseDto", # Legacy alias
"GetInternalSquadAccessibleNodesResponseDto",
"InboundsByProfileData",
# Keygen models
"GetPubKeyResponseDto",
"PubKeyResponseDto", # Legacy alias
@ -285,6 +296,7 @@ __all__ = [
"GetSubscriptionInfoResponseDto",
"SubscriptionInfoResponseDto", # Legacy alias
"UserSubscription",
"GetRawSubscriptionByShortUuidResponseDto",
# Subscription settings models
"GetSubscriptionSettingsResponseDto",
"SubscriptionSettingsResponseDto",
@ -379,6 +391,7 @@ __all__ = [
"UserLastConnectedNodeDto",
"UserResponseDto",
"UsersResponseDto",
"TagUserResponseDto",
# Users bulk actions models
"BulkAllResetTrafficUsersResponseDto",
"BulkAllUpdateUsersRequestDto",
@ -401,6 +414,7 @@ __all__ = [
"InboundDto",
"UpdateConfigProfileRequestDto",
"UpdateConfigProfileResponseDto",
"GetAllConfigProfilesResponsePaginated",
# Infra billing models
"CreateInfraBillingNodeRequestDto",
"CreateInfraBillingNodeResponseDto",

View file

@ -13,6 +13,7 @@ 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")
class AllInboundsData(BaseModel):

View file

@ -192,3 +192,7 @@ class DeleteNodeResponseDto(BaseModel):
def __bool__(self):
return self.is_deleted
class RestartAllNodesRequestDto(BaseModel):
force_restart: bool = Field(default=False, alias="forceRestart")

View file

@ -16,6 +16,10 @@ class UserSubscription(BaseModel):
days_left: int = Field(alias="daysLeft")
traffic_used: str = Field(alias="trafficUsed")
traffic_limit: str = Field(alias="trafficLimit")
lifetime_traffic_used: str = Field(alias="lifetimeTrafficUsed")
traffic_used_bytes: str = Field(alias="trafficUsedBytes")
traffic_limit_bytes: str = Field(alias="trafficLimitBytes")
lifetime_traffic_used_bytes: int = Field(alias="lifetimeTrafficUsedBytes")
traffic_limit_strategy: TrafficLimitStrategy = Field(alias="trafficLimitStrategy")
expires_at: datetime = Field(alias="expiresAt")
user_status: UserStatus = Field(alias="userStatus")
@ -40,6 +44,69 @@ class GetSubscriptionInfoResponseDto(BaseModel):
happ: HappCrypto
class RawHostAdditionalParams(BaseModel):
mode: Optional[str] = None
heartbeat_period: Optional[int] = Field(None, alias="heartbeatPeriod")
class RawHostProtocolOptions(BaseModel):
class SSOptions(BaseModel):
method: Optional[str] = None
ss: Optional[SSOptions] = None
class RawHostDbData(BaseModel):
raw_inbound: Optional[Dict[str, Any]] = Field(alias="rawInbound")
inbound_tag: str = Field(alias="inboundTag")
uuid: str
config_profile_uuid: Optional[str] = Field(alias="configProfileUuid")
config_profile_inbound_uuid: Optional[str] = Field(alias="configProfileInboundUuid")
is_disabled: bool = Field(alias="isDisabled")
view_position: int = Field(alias="viewPosition")
remark: str
is_hidden: bool = Field(alias="isHidden")
tag: Optional[str] = None
class RawHost(BaseModel):
address: Optional[str] = None
alpn: Optional[str] = None
fingerprint: Optional[str] = None
host: Optional[str] = None
network: Optional[str] = None
password: Optional[str] = None
path: Optional[str] = None
public_key: Optional[str] = Field(None, alias="publicKey")
port: Optional[int] = None
protocol: Optional[str] = None
remark: Optional[str] = None
short_id: Optional[str] = Field(None, alias="shortId")
sni: Optional[str] = None
spider_x: Optional[str] = Field(None, alias="spiderX")
tls: Optional[str] = None
header_type: Optional[str] = Field(None, alias="headerType")
additional_params: Optional[RawHostAdditionalParams] = Field(None, alias="additionalParams")
x_http_extra_params: Optional[Dict[str, Any]] = Field(None, alias="xHttpExtraParams")
mux_params: Optional[Dict[str, Any]] = Field(None, alias="muxParams")
sockopt_params: Optional[Dict[str, Any]] = Field(None, alias="sockoptParams")
server_description: Optional[str] = Field(None, alias="serverDescription")
flow: Optional[str] = None
protocol_options: Optional[RawHostProtocolOptions] = Field(None, alias="protocolOptions")
db_data: RawHostDbData = Field(alias="dbData")
class RawSubscriptionResponse(BaseModel):
user: UserSubscription
subscription_url: str = Field(alias="subscriptionUrl")
raw_hosts: List[RawHost] = Field(alias="rawHosts")
headers: Dict[str, str]
is_hwid_limited: bool = Field(alias="isHwidLimited")
class GetRawSubscriptionByShortUuidResponseDto(RawSubscriptionResponse):
pass
class SubscriptionWithoutHapp(BaseModel):
is_found: bool = Field(alias="isFound")
user: UserSubscription

View file

@ -71,9 +71,6 @@ class CreateUserRequestDto(BaseModel):
hwidDeviceLimit: Optional[int] = Field(
None, serialization_alias="hwidDeviceLimit", strict=True, ge=0
)
activate_all_inbounds: Optional[bool] = Field(
None, serialization_alias="activateAllInbounds"
)
active_internal_squads: Optional[List[str]] = Field(
None, serialization_alias="activeInternalSquads"
)
@ -157,6 +154,14 @@ class EmailUserResponseDto(RootModel[List[UserResponseDto]]):
return self.root[item]
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]]):
def __iter__(self):
return iter(self.root)

View file

@ -34,7 +34,6 @@ async def test_users(remnawave) -> None:
email=email,
telegram_id=telegram_id,
expire_at=expire_at,
activate_all_inbounds=True,
)
)