Merge pull request #56 from remnawave/development
Some checks failed
Publish Python Package / publish (push) Has been cancelled

Upgrade SDK to Remnawave API v2.8.0
This commit is contained in:
Artem 2026-07-02 02:49:21 +02:00 committed by GitHub
commit eee9be821a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 753 additions and 140 deletions

View file

@ -50,6 +50,7 @@ pip install git+https://github.com/remnawave/python-sdk.git@development
| Contract Version | Remnawave Panel Version |
| ---------------- | ----------------------- |
| 2.8.0 | >=2.8.0 |
| 2.7.0 | >=2.7.0 |
| 2.6.3 | >=2.6.3 |
| 2.6.2 | >=2.6.2 |

4
poetry.lock generated
View file

@ -894,5 +894,5 @@ typing-extensions = ">=4.12.0"
[metadata]
lock-version = "2.1"
python-versions = ">=3.11,<4.0"
content-hash = "2e64664ad5ef0cd863e8a3898493f4cdcf0544ada35d9ecad6aa686b32bdb1ac"
python-versions = ">=3.11,<3.15"
content-hash = "7b2d287bb0036b4254e0589beb3d86856735c088d39dcbf1bb2c965ce04a045f"

View file

@ -1,20 +1,18 @@
[project]
name = "remnawave"
version = "2.7.1"
description = "A Python SDK for interacting with the Remnawave API v2.7.1."
version = "2.8.0"
description = "A Python SDK for interacting with the Remnawave API v2.8.0."
authors = [
{name = "Artem",email = "dev@forestsnet.com"}
]
license = { text = "MIT" }
readme = "README.md"
requires-python = ">=3.11,<3.14"
requires-python = ">=3.11,<3.15"
dependencies = [
"rapid-api-client (==0.6.0)",
"orjson (>=3.10.15,<4.0.0)",
"httpx (>=0.27.2,<0.28.0)",
"pydantic[email]>=2.9.2,<3.0.0",
"pydantic-core>=2.33.1,<2.34.0",
"pydantic>=2.9.2,<3.0.0",
"cryptography (>=46.0.3,<47.0.0)",
]
keywords = ["remnawave", "api", "sdk", "proxy", "httpx", "async", "xray"]
@ -26,6 +24,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Framework :: AsyncIO",

View file

@ -9,6 +9,7 @@ from remnawave.models import (
CreateApiTokenResponseDto,
DeleteApiTokenResponseDto,
FindAllApiTokensResponseDto,
GetApiTokenScopesResponseDto,
)
from remnawave.rapid import BaseController, delete, get, post
@ -36,3 +37,10 @@ class APITokensManagementController(BaseController):
) -> FindAllApiTokensResponseDto:
"""Get all API tokens"""
...
@get("/tokens/scopes", response_class=GetApiTokenScopesResponseDto)
async def get_scopes(
self,
) -> GetApiTokenScopesResponseDto:
"""Get available API token scopes"""
...

View file

@ -1,6 +1,7 @@
from typing import Annotated
from rapid_api_client import Path, Query
from rapid_api_client.annotations import PydanticBody
from remnawave.models.bandwidthstats import (
GetLegacyStatsNodesUsersUsageResponseDto,
@ -11,10 +12,12 @@ from remnawave.models.bandwidthstats import (
GetStatsNodesRealtimeUsageResponseDto,
GetStatsNodesUsageResponseDto,
GetStatsNodeUsersUsageResponseDto,
GetStatsNodesUsersUsageRequestDto,
GetStatsNodesUsersUsageResponseDto,
GetStatsUserUsageResponseDto,
GetUserUsageByRangeResponseDto,
)
from remnawave.rapid import BaseController, get
from remnawave.rapid import BaseController, get, post
class BandWidthStatsController(BaseController):
@ -70,6 +73,17 @@ class BandWidthStatsController(BaseController):
"""Get Node Users Usage by Node UUID"""
...
@post("/bandwidth-stats/nodes/users", response_class=GetStatsNodesUsersUsageResponseDto)
async def get_stats_nodes_users_usage(
self,
body: Annotated[GetStatsNodesUsersUsageRequestDto, PydanticBody()],
top_users_limit: Annotated[int, Query(description="Limit of top users to return", alias="topUsersLimit")],
start: Annotated[str, Query(description="Start date (YYYY-MM-DD)")],
end: Annotated[str, Query(description="End date (YYYY-MM-DD)")],
) -> GetStatsNodesUsersUsageResponseDto:
"""Get Nodes Users Usage by Nodes UUIDs"""
...
@get("/bandwidth-stats/users/{uuid}", response_class=GetStatsUserUsageResponseDto)
async def get_stats_user_usage(
self,

View file

@ -7,12 +7,10 @@ from remnawave.models import (
BulkDeleteHostsResponseDto,
BulkDisableHostsResponseDto,
BulkEnableHostsResponseDto,
SetInboundToManyHostsRequestDto,
SetInboundToManyHostsResponseDto,
SetPortToManyHostsResponseDto,
SetPortToManyHostsRequestDto
UpdateManyHostsRequestDto,
UpdateManyHostsResponseDto,
)
from remnawave.rapid import AttributeBody, BaseController, post
from remnawave.rapid import AttributeBody, BaseController, patch, post
class HostsBulkActionsController(BaseController):
@ -40,21 +38,10 @@ class HostsBulkActionsController(BaseController):
"""Enable many hosts"""
...
@post(
"/hosts/bulk/set-inbound",
response_class=SetInboundToManyHostsResponseDto,
)
async def set_inbound_to_hosts(
@patch("/hosts/bulk/update", response_class=UpdateManyHostsResponseDto)
async def update_hosts(
self,
body: Annotated[SetInboundToManyHostsRequestDto, PydanticBody()],
) -> SetInboundToManyHostsResponseDto:
"""Set inbound to many hosts"""
...
@post("/hosts/bulk/set-port", response_class=SetPortToManyHostsResponseDto)
async def set_port_to_hosts(
self,
body: Annotated[SetPortToManyHostsRequestDto, PydanticBody()],
) -> SetPortToManyHostsResponseDto:
"""Set port to many hosts"""
body: Annotated[UpdateManyHostsRequestDto, PydanticBody()],
) -> UpdateManyHostsResponseDto:
"""Update many hosts"""
...

View file

@ -19,6 +19,7 @@ from remnawave.models import (
UpdateNodeRequestDto,
UpdateNodeResponseDto,
RestartAllNodesRequestBodyDto,
RestartNodeRequestBodyDto,
ResetNodeTrafficRequestDto,
ResetNodeTrafficResponseDto,
ProfileModificationRequestDto,
@ -98,6 +99,7 @@ class NodesController(BaseController):
async def restart_node(
self,
uuid: Annotated[str, Path(description="Node UUID")],
body: Annotated[RestartNodeRequestBodyDto | None, PydanticBody()] = None,
) -> RestartNodeResponseDto:
"""Restart Node"""
...

View file

@ -7,8 +7,6 @@ from remnawave.models import (
GetNodesMetricsResponseDto,
GetRemnawaveHealthResponseDto,
GetX25519KeyPairResponseDto,
EncryptHappCryptoLinkRequestDto,
EncryptHappCryptoLinkResponseDto,
DebugSrrMatcherRequestDto,
DebugSrrMatcherResponseDto,
GetMetadataResponseDto,
@ -67,14 +65,6 @@ class SystemController(BaseController):
"""Get X25519 Key Pair"""
...
@post("/system/tools/happ/encrypt", response_class=EncryptHappCryptoLinkResponseDto)
async def encrypt_happ_crypto_link(
self,
body: Annotated[EncryptHappCryptoLinkRequestDto, PydanticBody()],
) -> EncryptHappCryptoLinkResponseDto:
"""Encrypt Happ Crypto Link"""
...
@post("/system/testers/srr-matcher", response_class=DebugSrrMatcherResponseDto)
async def debug_srr_matcher(
self,

View file

@ -15,6 +15,7 @@ from remnawave.models import (
GetUserByUuidResponseDto,
GetUserAccessibleNodesResponseDto,
GetUserSubscriptionRequestHistoryResponseDto,
GetUsersStreamResponseDto,
TelegramUserResponseDto,
EmailUserResponseDto,
TagUserResponseDto,
@ -56,13 +57,31 @@ class UsersController(BaseController):
Query(default=None, description="Offset for pagination")
] = None,
size: Annotated[
Optional[int],
Optional[int],
Query(default=None, description="Page size for pagination")
] = None,
) -> GetAllUsersResponseDto:
"""Get all users"""
...
@get("/users/stream", response_class=GetUsersStreamResponseDto)
async def get_users_stream(
self,
size: Annotated[
Optional[int],
Query(default=None, description="Page size, no more than 1000 (default 250)"),
] = None,
cursor: Annotated[
Optional[str],
Query(
default=None,
description="Cursor from the previous response (nextCursor). Omit on the first request",
),
] = None,
) -> GetUsersStreamResponseDto:
"""Get all users using cursor-based (keyset) pagination"""
...
@delete("/users/{uuid}", response_class=DeleteUserResponseDto)
async def delete_user(
self,
@ -75,7 +94,7 @@ class UsersController(BaseController):
async def revoke_user_subscription(
self,
uuid: Annotated[str, Path(description="UUID of the user")],
body: Optional[Annotated[RevokeUserRequestDto, PydanticBody()]] = None,
body: Annotated[Optional[RevokeUserRequestDto], PydanticBody()] = None,
) -> RevokeUserSubscriptionResponseDto:
"""Revoke User Subscription"""
...

View file

@ -2,6 +2,8 @@ from .alpn import ALPN
from .client_type import ClientType
from .error_code import ErrorCode
from .fingerprint import Fingerprint
from .mihomo import MihomoIpVersion
from .scopes import Scope
from .security_layer import SecurityLayer
from .template_type import TemplateType
from .users import TrafficLimitStrategy, UserStatus
@ -25,6 +27,8 @@ __all__ = [
"ClientType",
"ALPN",
"Fingerprint",
"MihomoIpVersion",
"Scope",
"SecurityLayer",
"TemplateType",
"ResponseRuleConditionOperator",

View file

@ -0,0 +1,9 @@
from enum import StrEnum
class MihomoIpVersion(StrEnum):
DUAL = "dual"
IPV4 = "ipv4"
IPV6 = "ipv6"
IPV4_PREFER = "ipv4-prefer"
IPV6_PREFER = "ipv6-prefer"

279
remnawave/enums/scopes.py Normal file
View file

@ -0,0 +1,279 @@
from enum import StrEnum
class Scope(StrEnum):
"""API token scopes available in Remnawave API v2.8.0.
Mirrors the catalog returned by ``GET /api/tokens/scopes``. Use these members
when creating API tokens, e.g. ``CreateApiTokenRequestDto(scopes=[Scope.USERS_READ])``.
Coarse scopes per resource: ``<resource>:*`` (``*_ALL``), ``<resource>:read``,
``<resource>:write``. Fine-grained per-endpoint scopes are also available.
"""
WILDCARD = "*"
# Users
USERS_ALL = "users:*"
USERS_READ = "users:read"
USERS_WRITE = "users:write"
USERS_CREATE = "users:create"
USERS_UPDATE = "users:update"
USERS_DELETE = "users:delete"
USERS_LIST = "users:list"
USERS_STREAM = "users:stream"
USERS_LIST_TAGS = "users:list-tags"
USERS_ACCESSIBLE_NODES = "users:accessible-nodes"
USERS_SUBSCRIPTION_REQUEST_HISTORY = "users:subscription-request-history"
USERS_BY_SHORT_UUID = "users:by-short-uuid"
USERS_BY_UUID = "users:by-uuid"
USERS_BY_USERNAME = "users:by-username"
USERS_BY_ID = "users:by-id"
USERS_BY_TELEGRAM_ID = "users:by-telegram-id"
USERS_BY_EMAIL = "users:by-email"
USERS_BY_TAG = "users:by-tag"
USERS_REVOKE_SUBSCRIPTION = "users:revoke-subscription"
USERS_DISABLE = "users:disable"
USERS_ENABLE = "users:enable"
USERS_RESET_TRAFFIC = "users:reset-traffic"
USERS_RESOLVE = "users:resolve"
USERS_BULK_DELETE_BY_STATUS = "users:bulk-delete-by-status"
USERS_BULK_DELETE = "users:bulk-delete"
USERS_BULK_REVOKE_SUBSCRIPTION = "users:bulk-revoke-subscription"
USERS_BULK_RESET_TRAFFIC = "users:bulk-reset-traffic"
USERS_BULK_UPDATE_USERS = "users:bulk-update-users"
USERS_BULK_UPDATE_SQUADS = "users:bulk-update-squads"
USERS_BULK_EXTEND_EXPIRATION_DATE = "users:bulk-extend-expiration-date"
USERS_BULK_ALL_UPDATE_USERS = "users:bulk-all-update-users"
USERS_BULK_ALL_RESET_TRAFFIC = "users:bulk-all-reset-traffic"
USERS_BULK_ALL_EXTEND_EXPIRATION_DATE = "users:bulk-all-extend-expiration-date"
# Hwid User Devices
HWID_USER_DEVICES_ALL = "hwid-user-devices:*"
HWID_USER_DEVICES_READ = "hwid-user-devices:read"
HWID_USER_DEVICES_WRITE = "hwid-user-devices:write"
HWID_USER_DEVICES_LIST = "hwid-user-devices:list"
HWID_USER_DEVICES_CREATE = "hwid-user-devices:create"
HWID_USER_DEVICES_DELETE = "hwid-user-devices:delete"
HWID_USER_DEVICES_DELETE_ALL = "hwid-user-devices:delete-all"
HWID_USER_DEVICES_STATS = "hwid-user-devices:stats"
HWID_USER_DEVICES_TOP_USERS = "hwid-user-devices:top-users"
HWID_USER_DEVICES_LIST_BY_USER = "hwid-user-devices:list-by-user"
# Subscriptions
SUBSCRIPTIONS_ALL = "subscriptions:*"
SUBSCRIPTIONS_READ = "subscriptions:read"
SUBSCRIPTIONS_WRITE = "subscriptions:write"
SUBSCRIPTIONS_LIST = "subscriptions:list"
SUBSCRIPTIONS_BY_USERNAME = "subscriptions:by-username"
SUBSCRIPTIONS_BY_SHORT_UUID_PROTECTED = "subscriptions:by-short-uuid-protected"
SUBSCRIPTIONS_BY_UUID = "subscriptions:by-uuid"
SUBSCRIPTIONS_RAW = "subscriptions:raw"
SUBSCRIPTIONS_SUBPAGE_CONFIG = "subscriptions:subpage-config"
SUBSCRIPTIONS_CONNECTION_KEYS = "subscriptions:connection-keys"
# Nodes
NODES_ALL = "nodes:*"
NODES_READ = "nodes:read"
NODES_WRITE = "nodes:write"
NODES_LIST_TAGS = "nodes:list-tags"
NODES_CREATE = "nodes:create"
NODES_LIST = "nodes:list"
NODES_GET = "nodes:get"
NODES_ENABLE = "nodes:enable"
NODES_DISABLE = "nodes:disable"
NODES_DELETE = "nodes:delete"
NODES_UPDATE = "nodes:update"
NODES_RESTART = "nodes:restart"
NODES_RESET_TRAFFIC = "nodes:reset-traffic"
NODES_RESTART_ALL = "nodes:restart-all"
NODES_REORDER = "nodes:reorder"
NODES_BULK_PROFILE_MODIFICATION = "nodes:bulk-profile-modification"
NODES_BULK_ACTIONS = "nodes:bulk-actions"
NODES_BULK_UPDATE = "nodes:bulk-update"
# Node Plugins
NODE_PLUGINS_ALL = "node-plugins:*"
NODE_PLUGINS_READ = "node-plugins:read"
NODE_PLUGINS_WRITE = "node-plugins:write"
NODE_PLUGINS_TORRENT_BLOCKER_REPORTS = "node-plugins:torrent-blocker-reports"
NODE_PLUGINS_TORRENT_BLOCKER_STATS = "node-plugins:torrent-blocker-stats"
NODE_PLUGINS_TRUNCATE = "node-plugins:truncate"
NODE_PLUGINS_LIST = "node-plugins:list"
NODE_PLUGINS_GET = "node-plugins:get"
NODE_PLUGINS_UPDATE = "node-plugins:update"
NODE_PLUGINS_DELETE = "node-plugins:delete"
NODE_PLUGINS_CREATE = "node-plugins:create"
NODE_PLUGINS_REORDER = "node-plugins:reorder"
NODE_PLUGINS_CLONE = "node-plugins:clone"
NODE_PLUGINS_EXECUTOR = "node-plugins:executor"
# Bandwidth Stats
BANDWIDTH_STATS_ALL = "bandwidth-stats:*"
BANDWIDTH_STATS_READ = "bandwidth-stats:read"
BANDWIDTH_STATS_WRITE = "bandwidth-stats:write"
BANDWIDTH_STATS_NODE_USERS_USAGE_LEGACY = "bandwidth-stats:node-users-usage-legacy"
BANDWIDTH_STATS_NODE_USERS_USAGE = "bandwidth-stats:node-users-usage"
BANDWIDTH_STATS_NODES_USERS_USAGE = "bandwidth-stats:nodes-users-usage"
BANDWIDTH_STATS_USER_USAGE_LEGACY = "bandwidth-stats:user-usage-legacy"
BANDWIDTH_STATS_USER_USAGE = "bandwidth-stats:user-usage"
BANDWIDTH_STATS_NODES_USAGE = "bandwidth-stats:nodes-usage"
# Ip Control
IP_CONTROL_ALL = "ip-control:*"
IP_CONTROL_READ = "ip-control:read"
IP_CONTROL_WRITE = "ip-control:write"
IP_CONTROL_FETCH_IPS = "ip-control:fetch-ips"
IP_CONTROL_FETCH_IPS_RESULT = "ip-control:fetch-ips-result"
IP_CONTROL_DROP_CONNECTIONS = "ip-control:drop-connections"
IP_CONTROL_FETCH_USERS_IPS = "ip-control:fetch-users-ips"
IP_CONTROL_FETCH_USERS_IPS_RESULT = "ip-control:fetch-users-ips-result"
# Config Profiles
CONFIG_PROFILES_ALL = "config-profiles:*"
CONFIG_PROFILES_READ = "config-profiles:read"
CONFIG_PROFILES_WRITE = "config-profiles:write"
CONFIG_PROFILES_LIST = "config-profiles:list"
CONFIG_PROFILES_LIST_INBOUNDS = "config-profiles:list-inbounds"
CONFIG_PROFILES_LIST_PROFILE_INBOUNDS = "config-profiles:list-profile-inbounds"
CONFIG_PROFILES_GET = "config-profiles:get"
CONFIG_PROFILES_GET_COMPUTED = "config-profiles:get-computed"
CONFIG_PROFILES_DELETE = "config-profiles:delete"
CONFIG_PROFILES_CREATE = "config-profiles:create"
CONFIG_PROFILES_UPDATE = "config-profiles:update"
CONFIG_PROFILES_REORDER = "config-profiles:reorder"
# Internal Squads
INTERNAL_SQUADS_ALL = "internal-squads:*"
INTERNAL_SQUADS_READ = "internal-squads:read"
INTERNAL_SQUADS_WRITE = "internal-squads:write"
INTERNAL_SQUADS_LIST = "internal-squads:list"
INTERNAL_SQUADS_GET = "internal-squads:get"
INTERNAL_SQUADS_CREATE = "internal-squads:create"
INTERNAL_SQUADS_ACCESSIBLE_NODES = "internal-squads:accessible-nodes"
INTERNAL_SQUADS_UPDATE = "internal-squads:update"
INTERNAL_SQUADS_DELETE = "internal-squads:delete"
INTERNAL_SQUADS_ADD_USERS = "internal-squads:add-users"
INTERNAL_SQUADS_REMOVE_USERS = "internal-squads:remove-users"
INTERNAL_SQUADS_REORDER = "internal-squads:reorder"
# External Squads
EXTERNAL_SQUADS_ALL = "external-squads:*"
EXTERNAL_SQUADS_READ = "external-squads:read"
EXTERNAL_SQUADS_WRITE = "external-squads:write"
EXTERNAL_SQUADS_LIST = "external-squads:list"
EXTERNAL_SQUADS_GET = "external-squads:get"
EXTERNAL_SQUADS_CREATE = "external-squads:create"
EXTERNAL_SQUADS_UPDATE = "external-squads:update"
EXTERNAL_SQUADS_DELETE = "external-squads:delete"
EXTERNAL_SQUADS_ADD_USERS = "external-squads:add-users"
EXTERNAL_SQUADS_REMOVE_USERS = "external-squads:remove-users"
EXTERNAL_SQUADS_REORDER = "external-squads:reorder"
# Hosts
HOSTS_ALL = "hosts:*"
HOSTS_READ = "hosts:read"
HOSTS_WRITE = "hosts:write"
HOSTS_LIST_TAGS = "hosts:list-tags"
HOSTS_CREATE = "hosts:create"
HOSTS_UPDATE = "hosts:update"
HOSTS_LIST = "hosts:list"
HOSTS_GET = "hosts:get"
HOSTS_REORDER = "hosts:reorder"
HOSTS_DELETE = "hosts:delete"
HOSTS_BULK_DELETE = "hosts:bulk-delete"
HOSTS_BULK_DISABLE = "hosts:bulk-disable"
HOSTS_BULK_ENABLE = "hosts:bulk-enable"
HOSTS_BULK_UPDATE = "hosts:bulk-update"
# Subscription Template
SUBSCRIPTION_TEMPLATE_ALL = "subscription-template:*"
SUBSCRIPTION_TEMPLATE_READ = "subscription-template:read"
SUBSCRIPTION_TEMPLATE_WRITE = "subscription-template:write"
SUBSCRIPTION_TEMPLATE_LIST = "subscription-template:list"
SUBSCRIPTION_TEMPLATE_GET = "subscription-template:get"
SUBSCRIPTION_TEMPLATE_UPDATE = "subscription-template:update"
SUBSCRIPTION_TEMPLATE_DELETE = "subscription-template:delete"
SUBSCRIPTION_TEMPLATE_CREATE = "subscription-template:create"
SUBSCRIPTION_TEMPLATE_REORDER = "subscription-template:reorder"
# Subscription Settings
SUBSCRIPTION_SETTINGS_ALL = "subscription-settings:*"
SUBSCRIPTION_SETTINGS_READ = "subscription-settings:read"
SUBSCRIPTION_SETTINGS_WRITE = "subscription-settings:write"
SUBSCRIPTION_SETTINGS_GET = "subscription-settings:get"
SUBSCRIPTION_SETTINGS_UPDATE = "subscription-settings:update"
# Infra Billing
INFRA_BILLING_ALL = "infra-billing:*"
INFRA_BILLING_READ = "infra-billing:read"
INFRA_BILLING_WRITE = "infra-billing:write"
INFRA_BILLING_LIST_PROVIDERS = "infra-billing:list-providers"
INFRA_BILLING_GET_PROVIDER = "infra-billing:get-provider"
INFRA_BILLING_DELETE_PROVIDER = "infra-billing:delete-provider"
INFRA_BILLING_CREATE_PROVIDER = "infra-billing:create-provider"
INFRA_BILLING_UPDATE_PROVIDER = "infra-billing:update-provider"
INFRA_BILLING_CREATE_BILL_RECORD = "infra-billing:create-bill-record"
INFRA_BILLING_LIST_BILL_RECORDS = "infra-billing:list-bill-records"
INFRA_BILLING_DELETE_BILL_RECORD = "infra-billing:delete-bill-record"
INFRA_BILLING_LIST_BILLING_NODES = "infra-billing:list-billing-nodes"
INFRA_BILLING_UPDATE_BILLING_NODE = "infra-billing:update-billing-node"
INFRA_BILLING_CREATE_BILLING_NODE = "infra-billing:create-billing-node"
INFRA_BILLING_DELETE_BILLING_NODE = "infra-billing:delete-billing-node"
# System
SYSTEM_ALL = "system:*"
SYSTEM_READ = "system:read"
SYSTEM_WRITE = "system:write"
SYSTEM_METADATA = "system:metadata"
SYSTEM_STATS = "system:stats"
SYSTEM_BANDWIDTH_STATS = "system:bandwidth-stats"
SYSTEM_NODES_STATISTICS = "system:nodes-statistics"
SYSTEM_REMNAWAVE_HEALTH = "system:remnawave-health"
SYSTEM_NODES_METRICS = "system:nodes-metrics"
SYSTEM_GENERATE_X25519 = "system:generate-x25519"
SYSTEM_TEST_SRR_MATCHER = "system:test-srr-matcher"
SYSTEM_RECAP = "system:recap"
# Keygen
KEYGEN_ALL = "keygen:*"
KEYGEN_READ = "keygen:read"
KEYGEN_WRITE = "keygen:write"
KEYGEN_GET = "keygen:get"
# Subscription Request History
SUBSCRIPTION_REQUEST_HISTORY_ALL = "subscription-request-history:*"
SUBSCRIPTION_REQUEST_HISTORY_READ = "subscription-request-history:read"
SUBSCRIPTION_REQUEST_HISTORY_WRITE = "subscription-request-history:write"
SUBSCRIPTION_REQUEST_HISTORY_LIST = "subscription-request-history:list"
SUBSCRIPTION_REQUEST_HISTORY_STATS = "subscription-request-history:stats"
# Snippets
SNIPPETS_ALL = "snippets:*"
SNIPPETS_READ = "snippets:read"
SNIPPETS_WRITE = "snippets:write"
SNIPPETS_LIST = "snippets:list"
SNIPPETS_DELETE = "snippets:delete"
SNIPPETS_CREATE = "snippets:create"
SNIPPETS_UPDATE = "snippets:update"
# Subscription Page Configs
SUBSCRIPTION_PAGE_CONFIGS_ALL = "subscription-page-configs:*"
SUBSCRIPTION_PAGE_CONFIGS_READ = "subscription-page-configs:read"
SUBSCRIPTION_PAGE_CONFIGS_WRITE = "subscription-page-configs:write"
SUBSCRIPTION_PAGE_CONFIGS_LIST = "subscription-page-configs:list"
SUBSCRIPTION_PAGE_CONFIGS_GET = "subscription-page-configs:get"
SUBSCRIPTION_PAGE_CONFIGS_UPDATE = "subscription-page-configs:update"
SUBSCRIPTION_PAGE_CONFIGS_DELETE = "subscription-page-configs:delete"
SUBSCRIPTION_PAGE_CONFIGS_CREATE = "subscription-page-configs:create"
SUBSCRIPTION_PAGE_CONFIGS_REORDER = "subscription-page-configs:reorder"
SUBSCRIPTION_PAGE_CONFIGS_CLONE = "subscription-page-configs:clone"
# Metadata
METADATA_ALL = "metadata:*"
METADATA_READ = "metadata:read"
METADATA_WRITE = "metadata:write"
METADATA_GET_USER = "metadata:get-user"
METADATA_UPSERT_USER = "metadata:upsert-user"
METADATA_GET_NODE = "metadata:get-node"
METADATA_UPSERT_NODE = "metadata:upsert-node"

View file

@ -23,12 +23,10 @@ TUserEvents = Literal[
"user.limited",
"user.expired",
"user.traffic_reset",
"user.expires_in_72_hours",
"user.expires_in_48_hours",
"user.expires_in_24_hours",
"user.expired_24_hours_ago",
"user.first_connected",
"user.bandwidth_usage_threshold_reached",
"user.not_connected",
"user.expiration",
]
TServiceEvents = Literal[
@ -36,6 +34,8 @@ TServiceEvents = Literal[
"service.login_attempt_failed",
"service.login_attempt_success",
"service.subpage_config_changed",
"service.api_token_created",
"service.api_token_deleted",
]
TErrorsEvents = Literal[

View file

@ -1,8 +1,13 @@
from .api_tokens_management import (
ApiTokenDto,
ApiTokenScopeEndpointDto,
ApiTokenScopeResourceDto,
CreateApiTokenRequestDto,
CreateApiTokenResponseDto,
DeleteApiTokenResponseDto,
DocsInfoDto,
FindAllApiTokensResponseDto,
GetApiTokenScopesResponseDto,
)
from .auth import (
GetStatusResponseDto,
@ -36,6 +41,8 @@ from .bandwidthstats import (
GetStatsNodesRealtimeUsageResponseDto,
GetStatsNodesUsageResponseDto,
GetStatsNodeUsersUsageResponseDto,
GetStatsNodesUsersUsageRequestDto,
GetStatsNodesUsersUsageResponseDto,
GetStatsUserUsageResponseDto,
# Data Models
@ -44,9 +51,11 @@ from .bandwidthstats import (
NodeRealtimeUsageItem,
TopNodeItem,
TopUserItem,
TopNodesUserItem,
NodeSeriesItem,
StatsNodesUsageData,
StatsNodeUsersUsageData,
StatsNodesUsersUsageData,
StatsUserUsageData,
)
from .config_profiles import (
@ -88,10 +97,8 @@ from .hosts_bulk_actions import (
BulkDeleteHostsResponseDto,
BulkDisableHostsResponseDto,
BulkEnableHostsResponseDto,
SetInboundToManyHostsRequestDto,
SetInboundToManyHostsResponseDto,
SetPortToManyHostsResponseDto,
SetPortToManyHostsRequestDto
UpdateManyHostsRequestDto,
UpdateManyHostsResponseDto,
)
from .hwid import (
CreateHWIDUser, # Legacy alias
@ -199,6 +206,7 @@ from .nodes import (
UpdateNodeResponseDto,
RestartAllNodesRequestDto, # Legacy alias,
RestartAllNodesRequestBodyDto,
RestartNodeRequestBodyDto,
ResetNodeTrafficRequestDto,
ResetNodeTrafficResponseDto,
ProfileModificationRequestDto,
@ -281,8 +289,6 @@ from .system import (
X25519KeyPair,
DebugSrrMatcherRequestDto,
DebugSrrMatcherResponseDto,
EncryptHappCryptoLinkRequestDto,
EncryptHappCryptoLinkResponseDto,
GetMetadataResponseDto,
GetRecapResponseDto,
RecapThisMonth,
@ -313,6 +319,8 @@ from .users import (
GetAllUsersResponseDto,
GetAllTagsResponseDto,
GetUserSubscriptionRequestHistoryResponseDto,
GetUsersStreamResponseDto,
UsersStreamData,
# Response DTOs - Arrays (RootModel)
TelegramUserResponseDto,
@ -589,6 +597,7 @@ __all__ = [
"NodeConfigProfileRequestDto",
"RestartAllNodesRequestDto", # Legacy alias
"RestartAllNodesRequestBodyDto",
"RestartNodeRequestBodyDto",
"ResetNodeTrafficRequestDto",
"ResetNodeTrafficResponseDto",
"ProfileModificationRequestDto",
@ -689,8 +698,6 @@ __all__ = [
"X25519KeyPair",
"DebugSrrMatcherRequestDto",
"DebugSrrMatcherResponseDto",
"EncryptHappCryptoLinkRequestDto",
"EncryptHappCryptoLinkResponseDto",
"GetMetadataResponseDto",
"GetRecapResponseDto",
"RecapThisMonth",
@ -729,21 +736,30 @@ __all__ = [
"GetStatsNodesRealtimeUsageResponseDto",
"GetStatsNodesUsageResponseDto",
"GetStatsNodeUsersUsageResponseDto",
"GetStatsNodesUsersUsageRequestDto",
"GetStatsNodesUsersUsageResponseDto",
"GetStatsUserUsageResponseDto",
"LegacyUserUsageItem",
"LegacyNodeUserUsageItem",
"NodeRealtimeUsageItem",
"TopNodeItem",
"TopUserItem",
"TopNodesUserItem",
"NodeSeriesItem",
"StatsNodesUsageData",
"StatsNodeUsersUsageData",
"StatsNodesUsersUsageData",
"StatsUserUsageData",
# API Tokens models
"ApiTokenDto",
"ApiTokenScopeEndpointDto",
"ApiTokenScopeResourceDto",
"CreateApiTokenRequestDto",
"CreateApiTokenResponseDto",
"DeleteApiTokenResponseDto",
"DocsInfoDto",
"FindAllApiTokensResponseDto",
"GetApiTokenScopesResponseDto",
# Inbound bulk actions models
"AddInboundToNodesResponseDto",
"AddInboundToUsersResponseDto",
@ -753,10 +769,8 @@ __all__ = [
"BulkDeleteHostsResponseDto",
"BulkDisableHostsResponseDto",
"BulkEnableHostsResponseDto",
"SetInboundToManyHostsRequestDto",
"SetInboundToManyHostsResponseDto",
"SetPortToManyHostsResponseDto",
"SetPortToManyHostsRequestDto",
"UpdateManyHostsRequestDto",
"UpdateManyHostsResponseDto",
# Users models
"CreateUserRequestDto",
"UpdateUserRequestDto",
@ -777,6 +791,8 @@ __all__ = [
"GetAllUsersResponseDto",
"GetAllTagsResponseDto",
"GetUserSubscriptionRequestHistoryResponseDto",
"GetUsersStreamResponseDto",
"UsersStreamData",
"TelegramUserResponseDto",
"EmailUserResponseDto",
"TagUserResponseDto",

View file

@ -1,16 +1,37 @@
from datetime import datetime
from typing import List, Optional
from typing import Annotated, List, Literal, Optional
from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field
from pydantic import BaseModel, Field, StringConstraints
from remnawave.enums import Scope
class CreateApiTokenRequestDto(BaseModel):
token_name: str = Field(serialization_alias="tokenName")
name: Annotated[str, StringConstraints(min_length=2, max_length=30)] = Field(
serialization_alias="name"
)
expires_in_days: float = Field(serialization_alias="expiresInDays", ge=1)
scopes: List[str] = Field(
default_factory=lambda: [Scope.WILDCARD],
description='API token scopes. Pass :class:`remnawave.enums.Scope` members (or raw strings). Defaults to ["*"] (full access). See GET /api/tokens/scopes for the catalog.',
)
def __init__(self, **data):
# Backward compatibility: `token_name` was renamed to `name` in v2.8.0
if "token_name" in data and "name" not in data:
data["name"] = data.pop("token_name")
super().__init__(**data)
class CreateApiTokenResponseData(BaseModel):
uuid: UUID
name: str
expire_at: datetime = Field(alias="expireAt")
scopes: List[str]
created_at: datetime = Field(alias="createdAt")
updated_at: datetime = Field(alias="updatedAt")
token: str
uuid: str
class CreateApiTokenResponseDto(CreateApiTokenResponseData):
@ -22,23 +43,62 @@ class DeleteApiTokenResponseDto(BaseModel):
class ApiTokenDto(BaseModel):
uuid: str
token: str
token_name: str = Field(..., alias="tokenName")
uuid: UUID
name: str
expire_at: datetime = Field(..., alias="expireAt")
scopes: List[str] = Field(default_factory=list)
created_at: datetime = Field(..., alias="createdAt")
updated_at: datetime = Field(..., alias="updatedAt")
@property
def token_name(self) -> str:
"""Backward compatibility property (renamed to `name` in v2.8.0)"""
return self.name
class DocsInfoDto(BaseModel):
is_docs_enabled: bool = Field(..., alias="isDocsEnabled")
enabled: bool = Field(..., alias="enabled")
scalar_path: Optional[str] = Field(None, alias="scalarPath")
swagger_path: Optional[str] = Field(None, alias="swaggerPath")
@property
def is_docs_enabled(self) -> bool:
"""Backward compatibility property (renamed to `enabled` in v2.8.0)"""
return self.enabled
class FindAllApiTokensResponseData(BaseModel):
api_keys: List[ApiTokenDto] = Field(..., alias="apiKeys")
tokens: List[ApiTokenDto] = Field(..., alias="tokens")
docs: DocsInfoDto
@property
def api_keys(self) -> List[ApiTokenDto]:
"""Backward compatibility property (renamed to `tokens` in v2.8.0)"""
return self.tokens
class FindAllApiTokensResponseDto(FindAllApiTokensResponseData):
pass
class ApiTokenScopeEndpointDto(BaseModel):
key: str
kind: Literal["read", "write"]
method: str
path: str
description: str
class ApiTokenScopeResourceDto(BaseModel):
resource: str
resource_scopes: List[str] = Field(..., alias="resourceScopes")
endpoints: List[ApiTokenScopeEndpointDto]
class GetApiTokenScopesResponseData(BaseModel):
wildcard: str
resources: List[ApiTokenScopeResourceDto]
class GetApiTokenScopesResponseDto(GetApiTokenScopesResponseData):
pass

View file

@ -277,6 +277,34 @@ class GetStatsNodeUsersUsageResponseDto(RootModel[StatsNodeUsersUsageData]):
return self.root
# Stats Nodes Users Usage by Nodes UUIDs (POST /bandwidth-stats/nodes/users)
class GetStatsNodesUsersUsageRequestDto(BaseModel):
"""Request for nodes users usage by nodes UUIDs"""
nodes_uuids: List[UUID] = Field(serialization_alias="nodesUuids", min_length=1)
class TopNodesUserItem(BaseModel):
"""Top user item for nodes users usage"""
color: str
username: str
total: float
class StatsNodesUsersUsageData(BaseModel):
"""Stats nodes users usage data"""
categories: List[str]
sparkline_data: List[float] = Field(alias="sparklineData")
top_users: List[TopNodesUserItem] = Field(alias="topUsers")
class GetStatsNodesUsersUsageResponseDto(RootModel[StatsNodesUsersUsageData]):
"""Response for stats nodes users usage by nodes UUIDs"""
@property
def response(self) -> StatsNodesUsersUsageData:
return self.root
# Stats User Usage (with charts)
class StatsUserUsageData(BaseModel):

View file

@ -3,7 +3,10 @@ from uuid import UUID
from pydantic import BaseModel, Field, StringConstraints, RootModel
from remnawave.enums import ALPN, Fingerprint, SecurityLayer, SubscriptionType
from remnawave.enums import ALPN, MihomoIpVersion, SecurityLayer, SubscriptionType
# Tag for a single host tag entry: uppercase alphanumeric, underscores and colons, max 36 chars
HostTag = Annotated[str, StringConstraints(max_length=36, pattern=r"^[A-Z0-9_:]+$")]
class ReorderHostItem(BaseModel):
@ -35,21 +38,24 @@ class UpdateHostRequestDto(BaseModel):
sni: Optional[str] = None
host: Optional[str] = None
alpn: Optional[ALPN] = None
fingerprint: Optional[Fingerprint] = None
allow_insecure: Optional[bool] = Field(None, serialization_alias="allowInsecure")
fingerprint: Optional[str] = None
is_disabled: Optional[bool] = Field(None, serialization_alias="isDisabled")
security_layer: Optional[SecurityLayer] = Field(None, serialization_alias="securityLayer")
server_description: Optional[str] = Field(None, serialization_alias="serverDescription", max_length=30)
tag: Optional[Annotated[str, StringConstraints(max_length=32, pattern=r"^[A-Z0-9_:]+$")]] = None
tags: Optional[List[HostTag]] = Field(None, serialization_alias="tags", max_length=10)
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="keepSniBlank")
vless_route_id: Optional[int] = Field(None, serialization_alias="vlessRouteId", ge=0, le=65535)
pinned_peer_cert_sha256: Optional[str] = Field(None, serialization_alias="pinnedPeerCertSha256")
verify_peer_cert_by_name: Optional[str] = Field(None, serialization_alias="verifyPeerCertByName")
shuffle_host: Optional[bool] = Field(None, serialization_alias="shuffleHost")
mihomo_x25519: Optional[bool] = Field(None, serialization_alias="mihomoX25519")
x_http_extra_params: Optional[Dict[str, Any]] = Field(None, serialization_alias="xHttpExtraParams")
mihomo_ip_version: Optional[MihomoIpVersion] = Field(None, serialization_alias="mihomoIpVersion")
xhttp_extra_params: Optional[Dict[str, Any]] = Field(None, serialization_alias="xhttpExtraParams")
mux_params: Optional[Dict[str, Any]] = Field(None, serialization_alias="muxParams")
sockopt_params: Optional[Dict[str, Any]] = Field(None, serialization_alias="sockoptParams")
final_mask: Optional[Any] = Field(None, serialization_alias="finalMask")
nodes: Optional[List[UUID]] = None
xray_json_template_uuid: Optional[UUID] = Field(None, serialization_alias="xrayJsonTemplateUuid")
excluded_internal_squads: Optional[List[UUID]] = Field(None, serialization_alias="excludedInternalSquads")
@ -59,6 +65,28 @@ class UpdateHostRequestDto(BaseModel):
description="Subscription types from which this host will be excluded.",
)
def __init__(self, **data):
# Backward compatibility: `tag` (single value) was replaced by `tags` (list) in v2.8.0
if "tag" in data and "tags" not in data:
tag = data.pop("tag")
data["tags"] = [tag] if tag is not None else None
# Backward compatibility: `allow_insecure` was removed in v2.8.0 (use security_layer instead)
data.pop("allow_insecure", None)
# Backward compatibility: `xHttpExtraParams` alias was renamed to `xhttpExtraParams`
if "x_http_extra_params" in data and "xhttp_extra_params" not in data:
data["xhttp_extra_params"] = data.pop("x_http_extra_params")
super().__init__(**data)
@property
def x_http_extra_params(self) -> Optional[Dict[str, Any]]:
"""Backward compatibility property (renamed to xhttp_extra_params in v2.8.0)"""
return self.xhttp_extra_params
@property
def tag(self) -> Optional[str]:
"""Backward compatibility property (replaced by `tags` in v2.8.0)"""
return self.tags[0] if self.tags else None
@property
def inbound_uuid(self) -> Optional[UUID]:
return self.inbound.config_profile_inbound_uuid if self.inbound else None
@ -75,22 +103,25 @@ class HostResponseDto(BaseModel):
host: str | None = Field(alias="host")
alpn: str | None = Field(alias="alpn")
fingerprint: str | None = Field(alias="fingerprint")
x_http_extra_params: Dict[str, Any] | None = Field(alias="xHttpExtraParams")
xhttp_extra_params: Dict[str, Any] | None = Field(None, alias="xhttpExtraParams")
mux_params: Dict[str, Any] | None = Field(alias="muxParams")
sockopt_params: Dict[str, Any] | None = Field(alias="sockoptParams")
final_mask: Any | None = Field(None, alias="finalMask")
inbound: HostInboundData
server_description: str | None = Field(alias="serverDescription")
tag: str | None = Field(alias="tag")
tags: List[str] = Field(default_factory=list, alias="tags")
vless_route_id: int | None = Field(alias="vlessRouteId")
pinned_peer_cert_sha256: str | None = Field(None, alias="pinnedPeerCertSha256")
verify_peer_cert_by_name: str | None = Field(None, alias="verifyPeerCertByName")
shuffle_host: bool = Field(alias="shuffleHost")
mihomo_x25519: bool = Field(alias="mihomoX25519")
mihomo_ip_version: str | None = Field(None, alias="mihomoIpVersion")
nodes: List[UUID]
is_disabled: bool = Field(False, alias="isDisabled")
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="keepSniBlank")
allow_insecure: bool = Field(False, alias="allowInsecure")
xray_json_template_uuid: UUID | None = Field(alias="xrayJsonTemplateUuid")
excluded_internal_squads: List[UUID] = Field(default_factory=list, alias="excludedInternalSquads")
exclude_from_subscription_types: List[SubscriptionType] = Field(
@ -103,6 +134,21 @@ class HostResponseDto(BaseModel):
def inbound_uuid(self) -> Optional[UUID]:
return self.inbound.config_profile_inbound_uuid
@property
def x_http_extra_params(self) -> Dict[str, Any] | None:
"""Backward compatibility property (renamed to xhttp_extra_params in v2.8.0)"""
return self.xhttp_extra_params
@property
def tag(self) -> str | None:
"""Backward compatibility property (replaced by `tags` in v2.8.0)"""
return self.tags[0] if self.tags else None
@property
def allow_insecure(self) -> bool:
"""Backward compatibility property (removed in v2.8.0, derived from security_layer)"""
return self.security_layer == SecurityLayer.NONE
class CreateHostRequestDto(BaseModel):
inbound: CreateHostInboundData
@ -113,17 +159,20 @@ class CreateHostRequestDto(BaseModel):
sni: Optional[str] = None
host: Optional[str] = None
alpn: Optional[ALPN] = None
fingerprint: Optional[Fingerprint] = None
x_http_extra_params: Optional[Dict[str, Any]] = Field(None, serialization_alias="xHttpExtraParams")
fingerprint: Optional[str] = None
xhttp_extra_params: Optional[Dict[str, Any]] = Field(None, serialization_alias="xhttpExtraParams")
mux_params: Optional[Dict[str, Any]] = Field(None, serialization_alias="muxParams")
sockopt_params: Optional[Dict[str, Any]] = Field(None, serialization_alias="sockoptParams")
final_mask: Optional[Any] = Field(None, serialization_alias="finalMask")
server_description: Optional[str] = Field(None, serialization_alias="serverDescription", max_length=30)
tag: Optional[Annotated[str, StringConstraints(max_length=32, pattern=r"^[A-Z0-9_:]+$")]] = None
tags: Optional[List[HostTag]] = Field(None, serialization_alias="tags", max_length=10)
vless_route_id: Optional[int] = Field(None, serialization_alias="vlessRouteId", ge=0, le=65535)
pinned_peer_cert_sha256: Optional[str] = Field(None, serialization_alias="pinnedPeerCertSha256")
verify_peer_cert_by_name: Optional[str] = Field(None, serialization_alias="verifyPeerCertByName")
shuffle_host: bool = Field(False, serialization_alias="shuffleHost")
mihomo_x25519: bool = Field(False, serialization_alias="mihomoX25519")
mihomo_ip_version: Optional[MihomoIpVersion] = Field(None, serialization_alias="mihomoIpVersion")
nodes: List[UUID] = Field(default_factory=list)
allow_insecure: bool = Field(False, serialization_alias="allowInsecure")
is_disabled: bool = Field(False, serialization_alias="isDisabled")
security_layer: SecurityLayer = Field(SecurityLayer.DEFAULT, serialization_alias="securityLayer")
is_hidden: bool = Field(False, serialization_alias="isHidden")
@ -141,6 +190,16 @@ class CreateHostRequestDto(BaseModel):
def inbound_uuid(self) -> Optional[UUID]:
return self.inbound.config_profile_inbound_uuid
@property
def x_http_extra_params(self) -> Optional[Dict[str, Any]]:
"""Backward compatibility property (renamed to xhttp_extra_params in v2.8.0)"""
return self.xhttp_extra_params
@property
def tag(self) -> Optional[str]:
"""Backward compatibility property (replaced by `tags` in v2.8.0)"""
return self.tags[0] if self.tags else None
def __init__(
self,
inbound_uuid: Optional[UUID] = None,
@ -157,6 +216,17 @@ class CreateHostRequestDto(BaseModel):
or UUID("107541f1-ae1a-4e2d-9dec-7297557b5125"),
config_profile_inbound_uuid=inbound_uuid,
)
# Backward compatibility: `tag` (single value) was replaced by `tags` (list) in v2.8.0
if "tag" in data and "tags" not in data:
tag = data.pop("tag")
data["tags"] = [tag] if tag is not None else None
# Backward compatibility: `allow_insecure` was removed in v2.8.0 (use security_layer instead)
data.pop("allow_insecure", None)
# Backward compatibility: `xHttpExtraParams` alias was renamed to `xhttpExtraParams`
if "x_http_extra_params" in data and "xhttp_extra_params" not in data:
data["xhttp_extra_params"] = data.pop("x_http_extra_params")
super().__init__(**data)

View file

@ -1,36 +1,80 @@
from typing import List
from typing import Annotated, Any, Dict, List, Optional
from uuid import UUID
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, RootModel, StringConstraints
from remnawave.enums import ALPN, MihomoIpVersion, SecurityLayer, SubscriptionType
from remnawave.models import HostResponseDto
from remnawave.models.hosts import CreateHostInboundData, HostTag
class SetInboundToManyHostsRequestDto(BaseModel):
uuids: List[UUID]
config_profile_uuid: UUID = Field(serialization_alias="configProfileUuid")
config_profile_inbound_uuid: UUID = Field(
serialization_alias="configProfileInboundUuid"
class _HostListResponse(RootModel[List[HostResponseDto]]):
"""Base for bulk host responses that return a plain list of hosts."""
root: List[HostResponseDto]
def __iter__(self):
return iter(self.root)
def __getitem__(self, item):
return self.root[item]
def __bool__(self):
return bool(self.root)
def __len__(self):
return len(self.root)
class BulkDeleteHostsResponseDto(_HostListResponse):
pass
class BulkDisableHostsResponseDto(_HostListResponse):
pass
class BulkEnableHostsResponseDto(_HostListResponse):
pass
class UpdateManyHostsRequestDto(BaseModel):
"""Request to update many hosts at once (PATCH /hosts/bulk/update)."""
uuids: List[UUID] = Field(min_length=1)
inbound: Optional[CreateHostInboundData] = None
remark: Annotated[Optional[str], StringConstraints(max_length=40)] = None
address: Optional[str] = None
port: Optional[int] = None
path: Optional[str] = None
sni: Optional[str] = None
host: Optional[str] = None
alpn: Optional[ALPN] = None
fingerprint: Optional[str] = None
is_disabled: Optional[bool] = Field(None, serialization_alias="isDisabled")
security_layer: Optional[SecurityLayer] = Field(None, serialization_alias="securityLayer")
server_description: Optional[str] = Field(None, serialization_alias="serverDescription", max_length=30)
tags: Optional[List[HostTag]] = Field(None, serialization_alias="tags", max_length=10)
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="keepSniBlank")
vless_route_id: Optional[int] = Field(None, serialization_alias="vlessRouteId", ge=0, le=65535)
pinned_peer_cert_sha256: Optional[str] = Field(None, serialization_alias="pinnedPeerCertSha256")
verify_peer_cert_by_name: Optional[str] = Field(None, serialization_alias="verifyPeerCertByName")
shuffle_host: Optional[bool] = Field(None, serialization_alias="shuffleHost")
mihomo_x25519: Optional[bool] = Field(None, serialization_alias="mihomoX25519")
mihomo_ip_version: Optional[MihomoIpVersion] = Field(None, serialization_alias="mihomoIpVersion")
xhttp_extra_params: Optional[Dict[str, Any]] = Field(None, serialization_alias="xhttpExtraParams")
mux_params: Optional[Dict[str, Any]] = Field(None, serialization_alias="muxParams")
sockopt_params: Optional[Dict[str, Any]] = Field(None, serialization_alias="sockoptParams")
final_mask: Optional[Any] = Field(None, serialization_alias="finalMask")
nodes: Optional[List[UUID]] = None
xray_json_template_uuid: Optional[UUID] = Field(None, serialization_alias="xrayJsonTemplateUuid")
excluded_internal_squads: Optional[List[UUID]] = Field(None, serialization_alias="excludedInternalSquads")
exclude_from_subscription_types: Optional[List[SubscriptionType]] = Field(
None,
serialization_alias="excludeFromSubscriptionTypes",
description="Subscription types from which the hosts will be excluded.",
)
class SetPortToManyHostsRequestDto(BaseModel):
uuids: List[UUID]
port: int = Field(ge=1, le=65535)
class BulkDeleteHostsResponseDto(List[HostResponseDto]):
pass
class BulkDisableHostsResponseDto(List[HostResponseDto]):
pass
class BulkEnableHostsResponseDto(List[HostResponseDto]):
pass
class SetInboundToManyHostsResponseDto(List[HostResponseDto]):
pass
class SetPortToManyHostsResponseDto(List[HostResponseDto]):
class UpdateManyHostsResponseDto(_HostListResponse):
pass

View file

@ -12,6 +12,7 @@ class CreateUserHwidDeviceRequestDto(BaseModel):
os_version: Optional[str] = Field(None, serialization_alias="osVersion")
device_model: Optional[str] = Field(None, serialization_alias="deviceModel")
user_agent: Optional[str] = Field(None, serialization_alias="userAgent")
request_ip: Optional[str] = Field(None, serialization_alias="requestIp")
class DeleteUserHwidDeviceRequestDto(BaseModel):

View file

@ -146,9 +146,10 @@ class DeleteInfraBillingHistoryRecordByUuidResponseDto(BaseModel):
# Billing Nodes models
class CreateInfraBillingNodeRequestDto(BaseModel):
node_uuid: UUID = Field(serialization_alias="nodeUuid")
provider_uuid: UUID = Field(serialization_alias="providerUuid")
next_billing_at: Optional[datetime] = Field(None, serialization_alias="nextBillingAt")
node_uuid: Optional[UUID] = Field(None, serialization_alias="nodeUuid")
name: Optional[str] = Field(None, serialization_alias="name", min_length=1, max_length=255)
next_billing_at: datetime = Field(serialization_alias="nextBillingAt")
# ИСПРАВЛЕНО: API возвращает список всех billing nodes после создания, а не один созданный

View file

@ -2,7 +2,7 @@ from datetime import datetime
from typing import Annotated, List, Optional, Union, Literal
from uuid import UUID
from pydantic import BaseModel, Field, StringConstraints, RootModel
from pydantic import BaseModel, ConfigDict, Field, StringConstraints, RootModel
from remnawave.models.internal_squads import InboundsDto
@ -61,7 +61,7 @@ class CreateNodeRequestDto(BaseModel):
False,
serialization_alias="isTrafficTrackingActive",
)
traffic_limit_bytes: Optional[int] = Field(
traffic_limit_bytes: Optional[float] = Field(
None, serialization_alias="trafficLimitBytes", ge=0
)
notify_percent: Optional[int] = Field(
@ -74,11 +74,20 @@ class CreateNodeRequestDto(BaseModel):
None, serialization_alias="excludedInbounds"
)
country_code: Annotated[Optional[str], StringConstraints(max_length=2)] = Field(
"XX",
"XX",
serialization_alias="countryCode"
)
consumption_multiplier: Optional[float] = Field(
None, serialization_alias="consumptionMultiplier", ge=0.1
None, serialization_alias="consumptionMultiplier", ge=0, le=100
)
node_consumption_multiplier: Optional[float] = Field(
None, serialization_alias="nodeConsumptionMultiplier", ge=0, le=100
)
note: Optional[str] = Field(None, serialization_alias="note", max_length=255)
proxy_url: Optional[str] = Field(
None,
serialization_alias="proxyUrl",
pattern=r"^socks5://(?:[^:@/\s]+(?::[^@/\s]*)?@)?[^:@/\s]+:\d{1,5}$",
)
config_profile: NodeConfigProfileRequestDto = Field(
serialization_alias="configProfile"
@ -118,7 +127,16 @@ class UpdateNodeRequestDto(BaseModel):
None, serialization_alias="countryCode"
)
consumption_multiplier: Optional[float] = Field(
None, serialization_alias="consumptionMultiplier", ge=0.1
None, serialization_alias="consumptionMultiplier", ge=0, le=100
)
node_consumption_multiplier: Optional[float] = Field(
None, serialization_alias="nodeConsumptionMultiplier", ge=0, le=100
)
note: Optional[str] = Field(None, serialization_alias="note", max_length=255)
proxy_url: Optional[str] = Field(
None,
serialization_alias="proxyUrl",
pattern=r"^socks5://(?:[^:@/\s]+(?::[^@/\s]*)?@)?[^:@/\s]+:\d{1,5}$",
)
config_profile: Optional[NodeConfigProfileRequestDto] = Field(
None, serialization_alias="configProfile"
@ -160,6 +178,9 @@ class NodeResponseDto(BaseModel):
view_position: int = Field(alias="viewPosition")
country_code: str = Field(alias="countryCode")
consumption_multiplier: float = Field(alias="consumptionMultiplier")
node_consumption_multiplier: Optional[float] = Field(None, alias="nodeConsumptionMultiplier")
note: Optional[str] = Field(None, alias="note")
proxy_url: Optional[str] = Field(None, alias="proxyUrl")
cpu_count: Optional[int] = Field(None, alias="cpuCount")
cpu_model: Optional[str] = Field(None, alias="cpuModel")
total_ram: Optional[str] = Field(None, alias="totalRam")
@ -249,8 +270,16 @@ class DeleteNodeResponseDto(BaseModel):
class RestartAllNodesRequestBodyDto(BaseModel):
model_config = ConfigDict(populate_by_name=True)
force_restart: bool = Field(default=False, alias="forceRestart")
class RestartNodeRequestBodyDto(BaseModel):
model_config = ConfigDict(populate_by_name=True)
force_restart: bool = Field(default=False, alias="forceRestart")
class ResetNodeTrafficRequestDto(BaseModel):
uuid: Union[str, UUID] = Field(alias="uuid")

View file

@ -198,18 +198,6 @@ class GetX25519KeyPairResponseDto(BaseModel):
GenerateX25519ResponseDto = GetX25519KeyPairResponseDto
class EncryptHappCryptoLinkRequestDto(BaseModel):
link_to_encrypt: str = Field(serialization_alias="linkToEncrypt")
class EncryptHappCryptoLinkData(BaseModel):
encrypted_link: str = Field(alias="encryptedLink")
class EncryptHappCryptoLinkResponseDto(EncryptHappCryptoLinkData):
pass
class DebugSrrMatcherRequestDto(BaseModel):
response_rules: ResponseRules = Field(serialization_alias="responseRules")

View file

@ -62,7 +62,7 @@ class CreateUserRequestDto(BaseModel):
ss_password: Optional[Annotated[str, StringConstraints(min_length=8, max_length=32)]] = Field(
None, serialization_alias="ssPassword"
)
traffic_limit_bytes: Optional[int] = Field(
traffic_limit_bytes: Optional[float] = Field(
None, serialization_alias="trafficLimitBytes", ge=0
)
created_at: Optional[datetime] = Field(None, serialization_alias="createdAt")
@ -97,7 +97,7 @@ class UpdateUserRequestDto(BaseModel):
hwid_device_limit: Optional[int] = Field(None, serialization_alias="hwidDeviceLimit", ge=0)
tag: Optional[Annotated[str, StringConstraints(max_length=16, pattern=r"^[A-Z0-9_]+$")]] = None
telegram_id: Optional[int] = Field(None, serialization_alias="telegramId")
traffic_limit_bytes: Optional[int] = Field(None, serialization_alias="trafficLimitBytes", ge=0)
traffic_limit_bytes: Optional[float] = Field(None, serialization_alias="trafficLimitBytes", ge=0)
traffic_limit_strategy: Optional[TrafficLimitStrategy] = Field(
None, serialization_alias="trafficLimitStrategy"
)
@ -123,7 +123,7 @@ class UserResponseDto(BaseModel):
short_uuid: str = Field(alias="shortUuid")
username: str
status: UserStatus = Field(default=UserStatus.ACTIVE)
traffic_limit_bytes: int = Field(0, alias="trafficLimitBytes")
traffic_limit_bytes: float = Field(0, alias="trafficLimitBytes")
traffic_limit_strategy: TrafficLimitStrategy = Field(
TrafficLimitStrategy.NO_RESET, alias="trafficLimitStrategy"
)
@ -300,6 +300,22 @@ class GetAllUsersResponseDto(UsersResponseDto):
pass
class UsersStreamData(BaseModel):
"""Cursor-based (keyset) users stream page"""
users: list[UserResponseDto]
next_cursor: Optional[str] = Field(
None,
alias="nextCursor",
description="Cursor to fetch the next page, or null if there are no more results",
)
has_more: bool = Field(alias="hasMore", description="Whether there are more results to fetch")
class GetUsersStreamResponseDto(UsersStreamData):
"""Response for get all users using cursor-based (keyset) pagination"""
pass
class TagsResponseDto(BaseModel):
"""Tags collection response"""
tags: list[str]

View file

@ -38,9 +38,9 @@ class BulkResetTrafficUsersRequestDto(BaseModel):
class UpdateUserFields(BaseModel):
"""Fields to update for users"""
status: Optional[UserStatus] = None
traffic_limit_bytes: Optional[int] = Field(
None,
serialization_alias="trafficLimitBytes",
traffic_limit_bytes: Optional[float] = Field(
None,
serialization_alias="trafficLimitBytes",
ge=0,
description="Traffic limit in bytes. 0 - unlimited"
)
@ -98,7 +98,7 @@ class BulkExtendExpirationDateRequestDto(BaseModel):
class BulkAllUpdateUsersRequestDto(BaseModel):
"""Request to update all users"""
status: Optional[UserStatus] = Field(default=UserStatus.ACTIVE)
traffic_limit_bytes: Optional[int] = Field(
traffic_limit_bytes: Optional[float] = Field(
None,
serialization_alias="trafficLimitBytes",
ge=0,

View file

@ -5,6 +5,9 @@ import inspect
from remnawave.controllers.users import UsersController
from remnawave.controllers.system import SystemController
from remnawave.controllers.ip_control import IpControlController
from remnawave.controllers.api_tokens_management import APITokensManagementController
from remnawave.controllers.hosts_bulk_actions import HostsBulkActionsController
from remnawave.controllers.bandwidthstats import BandWidthStatsController
class TestUsersControllerEndpoints:
@ -66,6 +69,10 @@ class TestUsersControllerEndpoints:
def test_has_get_user_subscription_request_history(self):
assert hasattr(UsersController, "get_user_subscription_request_history")
def test_has_get_users_stream(self):
assert hasattr(UsersController, "get_users_stream")
assert callable(getattr(UsersController, "get_users_stream"))
class TestSystemControllerEndpoints:
def test_has_get_recap(self):
@ -93,12 +100,39 @@ class TestSystemControllerEndpoints:
def test_has_get_x25519_key_pair(self):
assert hasattr(SystemController, "get_x25519_key_pair")
def test_has_encrypt_happ_crypto_link(self):
assert hasattr(SystemController, "encrypt_happ_crypto_link")
def test_has_debug_srr_matcher(self):
assert hasattr(SystemController, "debug_srr_matcher")
def test_no_encrypt_happ_crypto_link(self):
# Removed in Remnawave API v2.8.0 (use client-side happ link generation instead)
assert not hasattr(SystemController, "encrypt_happ_crypto_link")
class TestApiTokensControllerEndpoints:
def test_has_get_scopes(self):
assert hasattr(APITokensManagementController, "get_scopes")
assert callable(getattr(APITokensManagementController, "get_scopes"))
class TestHostsBulkActionsControllerEndpoints:
def test_has_update_hosts(self):
assert hasattr(HostsBulkActionsController, "update_hosts")
assert callable(getattr(HostsBulkActionsController, "update_hosts"))
def test_no_set_inbound_to_hosts(self):
# Removed in Remnawave API v2.8.0 (replaced by update_hosts)
assert not hasattr(HostsBulkActionsController, "set_inbound_to_hosts")
def test_no_set_port_to_hosts(self):
# Removed in Remnawave API v2.8.0 (replaced by update_hosts)
assert not hasattr(HostsBulkActionsController, "set_port_to_hosts")
class TestBandwidthStatsControllerEndpoints:
def test_has_get_stats_nodes_users_usage(self):
assert hasattr(BandWidthStatsController, "get_stats_nodes_users_usage")
assert callable(getattr(BandWidthStatsController, "get_stats_nodes_users_usage"))
class TestIpControlControllerEndpoints:
def test_has_fetch_user_ips(self):

View file

@ -3,6 +3,8 @@ import pytest
from datetime import datetime, timezone
from uuid import uuid4
from pydantic import ValidationError
from remnawave.models import (
# Users
ResolveUserRequestBodyDto,
@ -180,12 +182,13 @@ class TestCreateInfraBillingHistoryRecordRequestDto:
class TestCreateInfraBillingNodeRequestDto:
def test_next_billing_at_optional(self):
dto = CreateInfraBillingNodeRequestDto(
node_uuid=uuid4(),
provider_uuid=uuid4(),
)
assert dto.next_billing_at is None
def test_next_billing_at_required(self):
"""next_billing_at became required in Remnawave API v2.8.0."""
with pytest.raises(ValidationError):
CreateInfraBillingNodeRequestDto(
node_uuid=uuid4(),
provider_uuid=uuid4(),
)
def test_next_billing_at_provided(self):
now = datetime.now(tz=timezone.utc)
@ -196,6 +199,17 @@ class TestCreateInfraBillingNodeRequestDto:
)
assert dto.next_billing_at == now
def test_name_supported_and_node_uuid_optional(self):
"""name was added and node_uuid became nullable in Remnawave API v2.8.0."""
now = datetime.now(tz=timezone.utc)
dto = CreateInfraBillingNodeRequestDto(
provider_uuid=uuid4(),
name="My server",
next_billing_at=now,
)
assert dto.name == "My server"
assert dto.node_uuid is None
class TestResponseRulesSettings:
def test_settings_field_exists(self):