Merge pull request #43 from remnawave:development

Update routes and add profile support for modifications
This commit is contained in:
Artem 2026-02-23 00:23:43 +01:00 committed by GitHub
commit f075a37efe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 572 additions and 68 deletions

View file

@ -50,6 +50,7 @@ pip install git+https://github.com/remnawave/python-sdk.git@development
| Contract Version | Remnawave Panel Version |
| ---------------- | ----------------------- |
| 2.6.1 | >=2.6.0 |
| 2.4.4 | >=2.4.0 |
| 2.3.2 | >=2.3.0, <2.4.0 |
| 2.3.0 | >=2.3.0, <2.3.2 |

View file

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

View file

@ -20,7 +20,7 @@ from remnawave.rapid import BaseController, get
class BandWidthStatsController(BaseController):
# ============ Legacy Endpoints (Deprecated) ============
@get("/bandwidth-stats/users/{user_uuid}/legacy-old", response_class=GetUserUsageByRangeResponseDto)
@get("/bandwidth-stats/users/{userUuid}/legacy", response_class=GetUserUsageByRangeResponseDto)
async def get_user_usage_legacy_old(
self,
user_uuid: Annotated[str, Path(description="UUID of the user", alias="userUuid")],
@ -30,7 +30,7 @@ class BandWidthStatsController(BaseController):
"""Get User Usage by Range (Legacy - Deprecated)"""
...
@get("/bandwidth-stats/nodes/{node_uuid}/users/legacy-old", response_class=GetNodeUserUsageByRangeResponseDto)
@get("/bandwidth-stats/nodes/{nodeUuid}/users/legacy", response_class=GetNodeUserUsageByRangeResponseDto)
async def get_node_user_usage_legacy_old(
self,
node_uuid: Annotated[str, Path(description="UUID of the node", alias="nodeUuid")],

View file

@ -11,6 +11,8 @@ from remnawave.models import (
GetAllInboundsResponseDto,
GetConfigProfileByUuidResponseDto,
GetInboundsByProfileUuidResponseDto,
ReorderConfigProfilesRequestDto,
ReorderConfigProfilesResponseDto,
UpdateConfigProfileRequestDto,
UpdateConfigProfileResponseDto,
)
@ -68,6 +70,14 @@ class ConfigProfilesController(BaseController):
"""Delete config profile"""
...
@post("/config-profiles/actions/reorder", response_class=ReorderConfigProfilesResponseDto)
async def reorder_config_profiles(
self,
body: Annotated[ReorderConfigProfilesRequestDto, PydanticBody()],
) -> ReorderConfigProfilesResponseDto:
"""Reorder config profiles"""
...
# Get computed config profile by uuid
@get("/config-profiles/{uuid}/computed-config", response_class=GetConfigProfileByUuidResponseDto)
async def get_computed_config_profile_by_uuid(

View file

@ -10,6 +10,8 @@ from remnawave.models import (
GetExternalSquadByUuidResponseDto,
GetExternalSquadsResponseDto,
RemoveUsersFromExternalSquadResponseDto,
ReorderExternalSquadsRequestDto,
ReorderExternalSquadsResponseDto,
UpdateExternalSquadRequestDto,
UpdateExternalSquadResponseDto,
)
@ -70,4 +72,11 @@ class ExternalSquadsController(BaseController):
uuid: str,
) -> RemoveUsersFromExternalSquadResponseDto:
"""Delete users from external squad"""
...
@post("/external-squads/actions/reorder", response_class=ReorderExternalSquadsResponseDto)
async def reorder_external_squads(
self,
body: Annotated[ReorderExternalSquadsRequestDto, PydanticBody()],
) -> ReorderExternalSquadsResponseDto:
"""Reorder external squads"""
...

View file

@ -13,6 +13,8 @@ from remnawave.models import (
DeleteUsersFromInternalSquadResponseDto,
GetAllInternalSquadsResponseDto,
GetInternalSquadByUuidResponseDto,
ReorderInternalSquadsRequestDto,
ReorderInternalSquadsResponseDto,
UpdateInternalSquadRequestDto,
UpdateInternalSquadResponseDto,
GetInternalSquadAccessibleNodesResponseDto,
@ -90,3 +92,11 @@ class InternalSquadsController(BaseController):
) -> GetInternalSquadAccessibleNodesResponseDto:
"""Get accessible nodes for internal squad"""
...
@post("/internal-squads/actions/reorder", response_class=ReorderInternalSquadsResponseDto)
async def reorder_internal_squads(
self,
body: Annotated[ReorderInternalSquadsRequestDto, PydanticBody()],
) -> ReorderInternalSquadsResponseDto:
"""Reorder internal squads"""
...

View file

@ -10,6 +10,7 @@ from remnawave.models import (
DisableNodeResponseDto,
EnableNodeResponseDto,
GetAllNodesResponseDto,
GetAllNodesTagsResponseDto,
GetOneNodeResponseDto,
ReorderNodeRequestDto,
ReorderNodeResponseDto,
@ -17,14 +18,25 @@ from remnawave.models import (
RestartNodeResponseDto,
UpdateNodeRequestDto,
UpdateNodeResponseDto,
RestartAllNodesRequestBodyDto,
RestartAllNodesRequestBodyDto,
ResetNodeTrafficRequestDto,
ResetNodeTrafficResponseDto
ResetNodeTrafficResponseDto,
ProfileModificationRequestDto,
ProfileModificationResponseDto,
NodesBulkActionsRequestDto,
NodesBulkActionsResponseDto,
)
from remnawave.rapid import BaseController, delete, get, patch, post
class NodesController(BaseController):
@get("/nodes/tags", response_class=GetAllNodesTagsResponseDto)
async def get_all_nodes_tags(
self,
) -> GetAllNodesTagsResponseDto:
"""Get all nodes tags"""
...
@post("/nodes", response_class=CreateNodeResponseDto)
async def create_node(
self,
@ -103,6 +115,14 @@ class NodesController(BaseController):
) -> ReorderNodeResponseDto:
"""Reorder Nodes"""
...
@post("/nodes/{uuid}/actions/reset-traffic", response_class=ResetNodeTrafficResponseDto)
async def reset_node_traffic(
self,
uuid: Annotated[str, Path(description="UUID of the node")],
) -> ResetNodeTrafficResponseDto:
"""Reset traffic for individual node"""
...
@post("/nodes/actions/reset-traffic", response_class=ResetNodeTrafficResponseDto)
async def reset_traffic_all_nodes(
@ -110,4 +130,20 @@ class NodesController(BaseController):
body: Annotated[ResetNodeTrafficRequestDto, PydanticBody()],
) -> ResetNodeTrafficResponseDto:
"""Reset Traffic All Nodes"""
...
@post("/nodes/bulk-actions/profile-modification", response_class=ProfileModificationResponseDto)
async def profile_modification(
self,
body: Annotated[ProfileModificationRequestDto, PydanticBody()],
) -> ProfileModificationResponseDto:
"""Modify Inbounds & Profile for many nodes"""
...
@post("/nodes/bulk-actions", response_class=NodesBulkActionsResponseDto)
async def nodes_bulk_actions(
self,
body: Annotated[NodesBulkActionsRequestDto, PydanticBody()],
) -> NodesBulkActionsResponseDto:
"""Perform actions for many nodes (ENABLE, DISABLE, RESTART, RESET_TRAFFIC)"""
...

View file

@ -7,10 +7,12 @@ from remnawave.models import (
DeletePasskeyResponseDto,
GetAllPasskeysResponseDto,
GetPasskeyRegistrationOptionsResponseDto,
UpdatePasskeyRequestDto,
UpdatePasskeyResponseDto,
VerifyPasskeyRegistrationRequestDto,
VerifyPasskeyRegistrationResponseDto,
)
from remnawave.rapid import BaseController, delete, get, post
from remnawave.rapid import BaseController, delete, get, patch, post
class PasskeysController(BaseController):
@ -42,4 +44,12 @@ class PasskeysController(BaseController):
body: Annotated[DeletePasskeyRequestDto, PydanticBody()],
) -> DeletePasskeyResponseDto:
"""Delete a passkey by ID"""
...
@patch("/passkeys", response_class=UpdatePasskeyResponseDto)
async def update_passkey(
self,
body: Annotated[UpdatePasskeyRequestDto, PydanticBody()],
) -> UpdatePasskeyResponseDto:
"""Update a passkey name"""
...

View file

@ -1,11 +1,19 @@
from typing import Annotated
from rapid_api_client import Path, Query
from rapid_api_client.annotations import PydanticBody
from remnawave.enums import ClientType
from remnawave.models.subscription import GetRawSubscriptionByShortUuidResponseDto
from remnawave.rapid import BaseController, get
from remnawave.models import GetAllSubscriptionsResponseDto, GetSubscriptionByUsernameResponseDto, GetSubscriptionByShortUUIDResponseDto, GetSubscriptionByUUIDResponseDto
from remnawave.models import (
GetAllSubscriptionsResponseDto,
GetSubscriptionByUsernameResponseDto,
GetSubscriptionByShortUUIDResponseDto,
GetSubscriptionByUUIDResponseDto,
GetSubpageConfigByShortUuidRequestBodyDto,
GetSubpageConfigByShortUuidResponseDto,
)
class SubscriptionsController(BaseController):
@ -47,6 +55,15 @@ class SubscriptionsController(BaseController):
"""None"""
...
@get("/subscriptions/subpage-config/{short_uuid}", response_class=GetSubpageConfigByShortUuidResponseDto)
async def get_subpage_config(
self,
short_uuid: Annotated[str, Path(description="Short UUID of the subscription")],
body: Annotated[GetSubpageConfigByShortUuidRequestBodyDto, PydanticBody()],
) -> GetSubpageConfigByShortUuidResponseDto:
"""Get subscription page config by short UUID"""
...
@get("/subscriptions/by-short-uuid/{short_uuid}/raw", response_class=GetRawSubscriptionByShortUuidResponseDto)
async def get_raw_subscription(
self,

View file

@ -9,6 +9,8 @@ from remnawave.models import (
DeleteSubscriptionTemplateResponseDto,
GetTemplateResponseDto,
GetTemplatesResponseDto,
ReorderSubscriptionTemplatesRequestDto,
ReorderSubscriptionTemplatesResponseDto,
UpdateTemplateRequestDto,
UpdateTemplateResponseDto,
)
@ -51,4 +53,11 @@ class SubscriptionsTemplateController(BaseController):
uuid: Annotated[str, Path(description="Template UUID")],
) -> DeleteSubscriptionTemplateResponseDto:
"""Delete subscription template"""
...
@post("/subscription-templates/actions/reorder", response_class=ReorderSubscriptionTemplatesResponseDto)
async def reorder_templates(
self,
body: Annotated[ReorderSubscriptionTemplatesRequestDto, PydanticBody()],
) -> ReorderSubscriptionTemplatesResponseDto:
"""Reorder subscription templates"""
...

View file

@ -11,11 +11,19 @@ from remnawave.models import (
EncryptHappCryptoLinkResponseDto,
DebugSrrMatcherRequestDto,
DebugSrrMatcherResponseDto,
GetMetadataResponseDto
)
from remnawave.rapid import BaseController, get, post
class SystemController(BaseController):
@get("/system/metadata", response_class=GetMetadataResponseDto)
async def get_metadata(
self,
) -> GetMetadataResponseDto:
"""Get Remnawave Information"""
...
@get("/system/stats", response_class=GetStatsResponseDto)
async def get_stats(
self,

View file

@ -4,4 +4,5 @@ class OAuth2Provider(StrEnum):
"""OAuth2 Provider enum"""
GITHUB = "github"
POCKETID = "pocketid"
YANDEX = "yandex"
YANDEX = "yandex"
KEYCLOAK = "keycloak"

View file

@ -35,6 +35,7 @@ TServiceEvents = Literal[
"service.panel_started",
"service.login_attempt_failed",
"service.login_attempt_success",
"service.subpage_config_changed",
]
TErrorsEvents = Literal[

View file

@ -61,6 +61,9 @@ from .config_profiles import (
GetInboundsByProfileUuidResponseDto,
InboundDto,
NodesProfileDto,
ReorderConfigProfileItem,
ReorderConfigProfilesRequestDto,
ReorderConfigProfilesResponseDto,
UpdateConfigProfileRequestDto,
UpdateConfigProfileResponseDto,
)
@ -166,6 +169,9 @@ from .internal_squads import (
GetAllInternalSquadsResponseDto,
GetInternalSquadByUuidResponseDto,
InternalSquadDto,
ReorderInternalSquadItem,
ReorderInternalSquadsRequestDto,
ReorderInternalSquadsResponseDto,
UpdateInternalSquadRequestDto,
UpdateInternalSquadResponseDto,
GetInternalSquadAccessibleNodesResponseDto,
@ -179,6 +185,7 @@ from .nodes import (
EnableNodeResponseDto,
ExcludedInbounds,
GetAllNodesResponseDto,
GetAllNodesTagsResponseDto,
GetOneNodeResponseDto,
NodeConfigProfileDto,
NodeConfigProfileRequestDto,
@ -193,7 +200,12 @@ from .nodes import (
RestartAllNodesRequestDto, # Legacy alias,
RestartAllNodesRequestBodyDto,
ResetNodeTrafficRequestDto,
ResetNodeTrafficResponseDto
ResetNodeTrafficResponseDto,
ProfileModificationRequestDto,
ProfileModificationResponseDto,
NodeBulkActionType,
NodesBulkActionsRequestDto,
NodesBulkActionsResponseDto,
)
from .nodes_usage_history import (
GetNodeUserUsageByRangeResponseDto,
@ -237,6 +249,9 @@ from .subscriptions_template import (
GetTemplatesResponseDto,
TemplateInfoDto,
GetTemplateResponseDto,
ReorderTemplateItem,
ReorderSubscriptionTemplatesRequestDto,
ReorderSubscriptionTemplatesResponseDto,
TemplateResponseDto,
UpdateTemplateRequestDto,
UpdateTemplateResponseDto,
@ -263,6 +278,7 @@ from .system import (
DebugSrrMatcherResponseDto,
EncryptHappCryptoLinkRequestDto,
EncryptHappCryptoLinkResponseDto,
GetMetadataResponseDto
)
from .users import (
# Request DTOs
@ -382,6 +398,8 @@ from .passkeys import (
GetAllPasskeysResponseDto,
GetPasskeyRegistrationOptionsResponseDto,
PasskeyDto,
UpdatePasskeyRequestDto,
UpdatePasskeyResponseDto,
VerifyPasskeyRegistrationRequestDto,
VerifyPasskeyRegistrationResponseDto,
)
@ -397,6 +415,9 @@ from .external_squads import (
GetExternalSquadByUuidResponseDto,
GetExternalSquadsResponseDto,
RemoveUsersFromExternalSquadResponseDto,
ReorderExternalSquadItem,
ReorderExternalSquadsRequestDto,
ReorderExternalSquadsResponseDto,
TemplateType,
UpdateExternalSquadRequestDto,
UpdateExternalSquadResponseDto,
@ -416,6 +437,8 @@ from .remnawave_settings import (
BrandingSettings,
GetRemnawaveSettingsResponseDto,
GitHubOAuth2Settings,
GenericOAuth2Settings,
KeycloakOAuth2Settings,
OAuth2Settings,
PasskeySettings,
PasswordSettings,
@ -434,6 +457,9 @@ from .subscription_page import (
DeleteSubscriptionPageConfigResponseDto,
GetSubscriptionPageConfigResponseDto,
GetSubscriptionPageConfigsResponseDto,
GetSubpageConfigByShortUuidRequestBodyDto,
GetSubpageConfigByShortUuidResponseDto,
SubpageConfigData,
ReorderSubscriptionPageConfigItem,
ReorderSubscriptionPageConfigsRequestDto,
ReorderSubscriptionPageConfigsResponseDto,
@ -468,6 +494,7 @@ __all__ = [
"EnableNodeResponseDto",
"ExcludedInbounds",
"GetAllNodesResponseDto",
"GetAllNodesTagsResponseDto",
"GetOneNodeResponseDto",
"NodeResponseDto",
"NodesResponseDto", # Legacy alias
@ -483,6 +510,11 @@ __all__ = [
"RestartAllNodesRequestBodyDto",
"ResetNodeTrafficRequestDto",
"ResetNodeTrafficResponseDto",
"ProfileModificationRequestDto",
"ProfileModificationResponseDto",
"NodeBulkActionType",
"NodesBulkActionsRequestDto",
"NodesBulkActionsResponseDto",
# Hosts models
"CreateHostRequestDto",
"CreateHostResponseDto",
@ -547,6 +579,9 @@ __all__ = [
"CreateSubscriptionTemplateResponseDto",
"DeleteSubscriptionTemplateResponseDto",
"GetTemplatesResponseDto",
"ReorderTemplateItem",
"ReorderSubscriptionTemplatesRequestDto",
"ReorderSubscriptionTemplatesResponseDto",
"TemplateInfoDto",
# System models
"BandwidthStatistic",
@ -570,6 +605,7 @@ __all__ = [
"DebugSrrMatcherResponseDto",
"EncryptHappCryptoLinkRequestDto",
"EncryptHappCryptoLinkResponseDto",
"GetMetadataResponseDto"
# XRay config models
"ConfigResponseDto", # Legacy alias
"GetConfigResponseDto",
@ -705,6 +741,9 @@ __all__ = [
"GetInboundsByProfileUuidResponseDto",
"InboundDto",
"NodesProfileDto",
"ReorderConfigProfileItem",
"ReorderConfigProfilesRequestDto",
"ReorderConfigProfilesResponseDto",
"UpdateConfigProfileRequestDto",
"UpdateConfigProfileResponseDto",
"GetAllConfigProfilesResponsePaginated",
@ -748,8 +787,12 @@ __all__ = [
"GetAllInternalSquadsResponseDto",
"GetInternalSquadByUuidResponseDto",
"InternalSquadDto",
"ReorderInternalSquadItem",
"ReorderInternalSquadsRequestDto",
"ReorderInternalSquadsResponseDto",
"UpdateInternalSquadRequestDto",
"UpdateInternalSquadResponseDto",
"GetInternalSquadAccessibleNodesResponseDto",
# Nodes usage history models
"GetNodeUserUsageByRangeResponseDto",
"GetNodesUsageByRangeResponseDto",
@ -803,6 +846,8 @@ __all__ = [
"GetAllPasskeysResponseDto",
"GetPasskeyRegistrationOptionsResponseDto",
"PasskeyDto",
"UpdatePasskeyRequestDto",
"UpdatePasskeyResponseDto",
"VerifyPasskeyRegistrationRequestDto",
"VerifyPasskeyRegistrationResponseDto",
@ -818,6 +863,9 @@ __all__ = [
"GetExternalSquadByUuidResponseDto",
"GetExternalSquadsResponseDto",
"RemoveUsersFromExternalSquadResponseDto",
"ReorderExternalSquadItem",
"ReorderExternalSquadsRequestDto",
"ReorderExternalSquadsResponseDto",
"TemplateType",
"UpdateExternalSquadRequestDto",
"UpdateExternalSquadResponseDto",
@ -838,7 +886,9 @@ __all__ = [
"BrandingSettings",
"GetRemnawaveSettingsResponseDto",
"GenericOAuth2Settings",
"GitHubOAuth2Settings",
"KeycloakOAuth2Settings",
"OAuth2Settings",
"PasskeySettings",
"PasswordSettings",
@ -857,6 +907,9 @@ __all__ = [
"DeleteSubscriptionPageConfigResponseDto",
"GetSubscriptionPageConfigResponseDto",
"GetSubscriptionPageConfigsResponseDto",
"GetSubpageConfigByShortUuidRequestBodyDto",
"GetSubpageConfigByShortUuidResponseDto",
"SubpageConfigData",
"ReorderSubscriptionPageConfigItem",
"ReorderSubscriptionPageConfigsRequestDto",
"ReorderSubscriptionPageConfigsResponseDto",

View file

@ -12,7 +12,7 @@ class InboundDto(BaseModel):
type: str
network: Optional[str] = None
security: Optional[str] = None
port: Optional[int] = None
port: Optional[float] = None
raw_inbound: Optional[Any] = Field(None, alias="rawInbound")
class NodesProfileDto(BaseModel):
@ -23,6 +23,7 @@ class NodesProfileDto(BaseModel):
class ConfigProfileDto(BaseModel):
uuid: UUID
name: str
view_position: int = Field(alias="viewPosition")
config: Dict[str, Any]
inbounds: List[InboundDto]
nodes: List[NodesProfileDto] = []
@ -72,3 +73,16 @@ class GetAllInboundsResponseDto(List[InboundDto]):
class GetInboundsByProfileUuidResponseDto(List[InboundDto]):
pass
class ReorderConfigProfileItem(BaseModel):
view_position: int = Field(serialization_alias="viewPosition")
uuid: UUID
class ReorderConfigProfilesRequestDto(BaseModel):
items: List[ReorderConfigProfileItem]
class ReorderConfigProfilesResponseDto(GetAllConfigProfilesResponsePaginated):
pass

View file

@ -49,6 +49,7 @@ class ExternalSquadHostOverridesDto(BaseModel):
class ExternalSquadDto(BaseModel):
"""External squad data model"""
uuid: UUID
view_position: int = Field(alias="viewPosition")
name: str
info: ExternalSquadInfoDto
templates: List[ExternalSquadTemplateDto]
@ -105,6 +106,26 @@ class DeleteExternalSquadResponseDto(BaseModel):
is_deleted: bool = Field(alias="isDeleted")
class ReorderExternalSquadItem(BaseModel):
view_position: int = Field(serialization_alias="viewPosition")
uuid: UUID
class ReorderExternalSquadsRequestDto(BaseModel):
items: List[ReorderExternalSquadItem]
class ReorderExternalSquadsResponseDto(BaseModel):
"""Response after reordering external squads"""
total: int = Field(alias="total")
external_squads: List[ExternalSquadDto] = Field(alias="externalSquads")
class DeleteExternalSquadResponseDto(BaseModel):
"""Response after deleting external squad"""
is_deleted: bool = Field(alias="isDeleted")
class AddUsersToExternalSquadResponseDto(BaseModel):
"""Response after adding users to external squad"""
event_sent: bool = Field(alias="eventSent")

View file

@ -65,18 +65,18 @@ class HostResponseDto(BaseModel):
remark: str
address: str
port: int
path: Optional[str] = None
sni: Optional[str] = None
host: Optional[str] = None
alpn: Optional[str] = None
fingerprint: Optional[str] = None
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")
path: str | None = Field(alias="path")
sni: str | None = Field(alias="sni")
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")
mux_params: Dict[str, Any] | None = Field(alias="muxParams")
sockopt_params: Dict[str, Any] | None = Field(alias="sockoptParams")
inbound: HostInboundData
server_description: Optional[str] = Field(None, alias="serverDescription")
tag: Optional[str] = None
vless_route_id: Optional[int] = Field(None, alias="vlessRouteId")
server_description: str | None = Field(alias="serverDescription")
tag: str | None = Field(alias="tag")
vless_route_id: int | None = Field(alias="vlessRouteId")
shuffle_host: bool = Field(alias="shuffleHost")
mihomo_x25519: bool = Field(alias="mihomoX25519")
nodes: List[UUID]
@ -86,7 +86,7 @@ class HostResponseDto(BaseModel):
override_sni_from_address: bool = Field(False, alias="overrideSniFromAddress")
keep_blank_sni: bool = Field(False, alias="keepBlankSni")
allow_insecure: bool = Field(False, alias="allowInsecure")
xray_json_template_uuid: Optional[UUID] = Field(None, alias="xrayJsonTemplateUuid")
xray_json_template_uuid: UUID | None = Field(alias="xrayJsonTemplateUuid")
excluded_internal_squads: List[UUID] = Field(default_factory=list, alias="excludedInternalSquads")
@property

View file

@ -10,10 +10,10 @@ class InboundsDto(BaseModel):
profile_uuid: UUID = Field(alias="profileUuid")
tag: str
type: str
network: Optional[str] = Field(default=None)
security: Optional[str] = Field(default=None)
port: Optional[float] = Field(default=None)
raw_inbound: Optional[dict] = Field(default=None, alias="rawInbound")
network: Optional[str] = None
security: Optional[str] = None
port: Optional[float] = None
raw_inbound: Optional[dict] = Field(None, alias="rawInbound")
class InfoDto(BaseModel):
@ -23,6 +23,7 @@ class InfoDto(BaseModel):
class InternalSquadDto(BaseModel):
uuid: UUID
view_position: int = Field(alias="viewPosition")
name: str
info: Optional[InfoDto] = Field(default=None)
inbounds: List[InboundsDto] = Field(default_factory=list)
@ -100,3 +101,16 @@ class AccessibleNodeDto(BaseModel):
class GetInternalSquadAccessibleNodesResponseDto(BaseModel):
squad_uuid: UUID = Field(alias="squadUuid")
accessible_nodes: List[AccessibleNodeDto] = Field(alias="accessibleNodes")
class ReorderInternalSquadItem(BaseModel):
view_position: int = Field(serialization_alias="viewPosition")
uuid: UUID
class ReorderInternalSquadsRequestDto(BaseModel):
items: List[ReorderInternalSquadItem]
class ReorderInternalSquadsResponseDto(GetAllInternalSquadsResponse):
pass

View file

@ -1,5 +1,5 @@
from datetime import datetime
from typing import Annotated, List, Optional, Union
from typing import Annotated, List, Optional, Union, Literal
from uuid import UUID
from pydantic import BaseModel, Field, StringConstraints, RootModel
@ -28,6 +28,11 @@ class ReorderNodeItem(BaseModel):
uuid: UUID
class GetAllNodesTagsResponseDto(BaseModel):
"""Response with all nodes tags"""
tags: List[str]
class NodeProviderDto(BaseModel):
"""Node provider information"""
uuid: UUID
@ -79,6 +84,11 @@ class CreateNodeRequestDto(BaseModel):
serialization_alias="configProfile"
)
provider_uuid: Optional[UUID] = Field(None, serialization_alias="providerUuid")
tags: Optional[List[Annotated[str, StringConstraints(max_length=36, pattern=r'^[A-Z0-9_:]+$')]]] = Field(
None,
serialization_alias="tags",
max_length=10
)
class UpdateNodeRequestDto(BaseModel):
@ -111,6 +121,11 @@ class UpdateNodeRequestDto(BaseModel):
None, serialization_alias="configProfile"
)
provider_uuid: Optional[UUID] = Field(None, serialization_alias="providerUuid")
tags: Optional[List[Annotated[str, StringConstraints(max_length=36, pattern=r'^[A-Z0-9_:]+$')]]] = Field(
None,
serialization_alias="tags",
max_length=10
)
class ReorderNodeRequestDto(BaseModel):
@ -147,6 +162,7 @@ class NodeResponseDto(BaseModel):
config_profile: NodeConfigProfileDto = Field(alias="configProfile")
provider_uuid: Optional[UUID] = Field(None, alias="providerUuid")
provider: Optional[NodeProviderDto] = None
tags: List[str] = Field(default_factory=list, alias="tags")
class CreateNodeResponseDto(NodeResponseDto):
@ -196,6 +212,10 @@ class RestartAllNodesResponseDto(BaseModel):
event_sent: bool = Field(alias="eventSent")
class ResetNodeTrafficResponseDto(BaseModel):
event_sent: bool = Field(alias="eventSent")
class ReorderNodeResponseDto(RootModel[List[NodeResponseDto]]):
root: List[NodeResponseDto]
@ -230,6 +250,41 @@ class ResetNodeTrafficRequestDto(BaseModel):
class ResetNodeTrafficResponseDto(RestartEventResponse):
pass
class ConfigProfileData(BaseModel):
"""Config profile data for modification"""
active_config_profile_uuid: str = Field(alias="activeConfigProfileUuid")
active_inbounds: List[str] = Field(alias="activeInbounds", min_length=1)
class ProfileModificationRequestDto(BaseModel):
"""Request to modify profiles for multiple nodes"""
uuids: List[str] = Field(min_length=1)
config_profile: ConfigProfileData = Field(alias="configProfile")
class ProfileModificationResponseData(BaseModel):
"""Profile modification response data"""
event_sent: bool = Field(alias="eventSent")
class ProfileModificationResponseDto(ProfileModificationResponseData):
"""Profile modification response"""
pass
# Для обратной совместимости
RestartAllNodesRequestDto = RestartAllNodesRequestBodyDto
NodesResponseDto = NodeResponseDto
NodesResponseDto = NodeResponseDto
NodeBulkActionType = Literal["ENABLE", "DISABLE", "RESTART", "RESET_TRAFFIC"]
class NodesBulkActionsRequestDto(BaseModel):
"""Request for performing bulk actions on nodes"""
uuids: List[UUID] = Field(min_length=1)
action: NodeBulkActionType = Field(description="Action to perform on nodes")
class NodesBulkActionsResponseDto(BaseModel):
"""Response after performing bulk actions on nodes"""
event_sent: bool = Field(alias="eventSent")

View file

@ -43,4 +43,15 @@ class DeletePasskeyRequestDto(BaseModel):
class DeletePasskeyResponseDto(BaseModel):
"""Response with updated passkeys list after deletion"""
passkeys: List[PasskeyDto]
passkeys: List[PasskeyDto]
class UpdatePasskeyRequestDto(BaseModel):
"""Request to update a passkey"""
id: str
name: str
class UpdatePasskeyResponseDto(BaseModel):
"""Response with updated passkey information"""
passkey: PasskeyDto

View file

@ -6,32 +6,55 @@ from pydantic import BaseModel, Field, HttpUrl
class PasskeySettings(BaseModel):
"""Passkey authentication settings"""
enabled: bool
rp_id: Optional[str] = Field(None, alias="rpId")
origin: Optional[str] = None
rp_id: str | None = Field(alias="rpId")
origin: str | None
class GitHubOAuth2Settings(BaseModel):
"""GitHub OAuth2 settings"""
enabled: bool
client_id: Optional[str] = Field(None, alias="clientId")
client_secret: Optional[str] = Field(None, alias="clientSecret")
client_id: str | None = Field(alias="clientId")
client_secret: str | None = Field(alias="clientSecret")
allowed_emails: List[str] = Field(alias="allowedEmails")
class PocketIdOAuth2Settings(BaseModel):
"""PocketID OAuth2 settings"""
enabled: bool
client_id: Optional[str] = Field(None, alias="clientId")
client_secret: Optional[str] = Field(None, alias="clientSecret")
plain_domain: Optional[str] = Field(None, alias="plainDomain")
client_id: str | None = Field(alias="clientId")
client_secret: str | None = Field(alias="clientSecret")
plain_domain: str | None = Field(alias="plainDomain")
allowed_emails: List[str] = Field(alias="allowedEmails")
class YandexOAuth2Settings(BaseModel):
"""Yandex OAuth2 settings"""
enabled: bool
client_id: Optional[str] = Field(None, alias="clientId")
client_secret: Optional[str] = Field(None, alias="clientSecret")
client_id: str | None = Field(alias="clientId")
client_secret: str | None = Field(alias="clientSecret")
allowed_emails: List[str] = Field(alias="allowedEmails")
class KeycloakOAuth2Settings(BaseModel):
"""Keycloak OAuth2 settings"""
enabled: bool
realm: str | None
client_id: str | None = Field(alias="clientId")
client_secret: str | None = Field(alias="clientSecret")
frontend_domain: str | None = Field(alias="frontendDomain")
keycloak_domain: str | None = Field(alias="keycloakDomain")
allowed_emails: List[str] = Field(alias="allowedEmails")
class GenericOAuth2Settings(BaseModel):
"""Generic OAuth2 settings"""
enabled: bool
client_id: str | None = Field(alias="clientId")
client_secret: str | None = Field(alias="clientSecret")
with_pkce: bool = Field(alias="withPkce")
authorization_url: str | None = Field(alias="authorizationUrl")
token_url: str | None = Field(alias="tokenUrl")
frontend_domain: str | None = Field(alias="frontendDomain")
allowed_emails: List[str] = Field(alias="allowedEmails")
@ -40,12 +63,14 @@ class OAuth2Settings(BaseModel):
github: GitHubOAuth2Settings
pocketid: PocketIdOAuth2Settings
yandex: YandexOAuth2Settings
keycloak: KeycloakOAuth2Settings
generic: GenericOAuth2Settings
class TelegramAuthSettings(BaseModel):
"""Telegram authentication settings"""
enabled: bool
bot_token: Optional[str] = Field(None, alias="botToken")
bot_token: str | None = Field(alias="botToken")
admin_ids: List[str] = Field(alias="adminIds")
@ -62,9 +87,9 @@ class BrandingSettings(BaseModel):
class RemnawaveSettingsData(BaseModel):
"""Remnawave settings data"""
passkey_settings: Optional[PasskeySettings] = Field(None, alias="passkeySettings")
oauth2_settings: Optional[OAuth2Settings] = Field(None, alias="oauth2Settings")
tg_auth_settings: Optional[TelegramAuthSettings] = Field(None, alias="tgAuthSettings")
passkey_settings: PasskeySettings | None = Field(alias="passkeySettings")
oauth2_settings: OAuth2Settings | None = Field(alias="oauth2Settings")
tg_auth_settings: TelegramAuthSettings | None = Field(alias="tgAuthSettings")
password_settings: Optional[PasswordSettings] = Field(None, alias="passwordSettings")
branding_settings: Optional[BrandingSettings] = Field(None, alias="brandingSettings")

View file

@ -157,7 +157,7 @@ class RawHost(BaseModel):
mldsa65_verify: Optional[str] = Field(None, alias="mldsa65Verify")
encryption: Optional[str] = None
protocol_options: Optional[RawHostProtocolOptions] = Field(None, alias="protocolOptions")
db_data: RawHostDbData = Field(alias="dbData")
db_data: Optional[RawHostDbData] = Field(None, alias="dbData")
xray_json_template: Optional[Dict[str, Any]] = Field(None, alias="xrayJsonTemplate")
@ -182,7 +182,7 @@ class UserSubscription(BaseModel):
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")
lifetime_traffic_used_bytes: str = Field(alias="lifetimeTrafficUsedBytes")
traffic_limit_strategy: TrafficLimitStrategy = Field(alias="trafficLimitStrategy")
expires_at: datetime = Field(alias="expiresAt")
user_status: UserStatus = Field(alias="userStatus")

View file

@ -11,7 +11,7 @@ class SubscriptionPageConfigDto(BaseModel):
uuid: UUID
view_position: int = Field(alias="viewPosition")
name: str
config: Optional[Any] = None
config: Any | None
class GetSubscriptionPageConfigsData(BaseModel):
@ -25,9 +25,14 @@ class GetSubscriptionPageConfigsResponseDto(GetSubscriptionPageConfigsData):
pass
class GetSubscriptionPageConfigResponseDto(SubscriptionPageConfigDto):
class GetSubscriptionPageConfigResponseDto(BaseModel):
"""Response with single subscription page config"""
pass
model_config = ConfigDict(populate_by_name=True)
uuid: UUID
view_position: int = Field(alias="viewPosition")
name: str
config: Any
class CreateSubscriptionPageConfigRequestDto(BaseModel):
@ -99,4 +104,26 @@ class CloneSubscriptionPageConfigRequestDto(BaseModel):
class CloneSubscriptionPageConfigResponseDto(SubscriptionPageConfigDto):
"""Response after cloning subscription page config"""
pass
pass
class GetSubpageConfigByShortUuidRequestBodyDto(BaseModel):
"""Request body for getting subpage config by short UUID"""
model_config = ConfigDict(populate_by_name=True)
request_headers: dict[str, str] = Field(default_factory=dict, serialization_alias="requestHeaders")
class SubpageConfigData(BaseModel):
"""Data inside GetSubpageConfigByShortUuidResponseDto"""
model_config = ConfigDict(populate_by_name=True)
subpage_config_uuid: UUID | None = Field(alias="subpageConfigUuid")
webpage_allowed: bool = Field(alias="webpageAllowed")
class GetSubpageConfigByShortUuidResponseDto(BaseModel):
"""Response for getting subpage config by short UUID"""
model_config = ConfigDict(populate_by_name=True)
response: SubpageConfigData

View file

@ -61,7 +61,8 @@ class CustomRemarksDto(BaseModel):
limited_users: List[str] = Field(alias="limitedUsers", min_length=1)
disabled_users: List[str] = Field(alias="disabledUsers", min_length=1)
empty_hosts: List[str] = Field(alias="emptyHosts", min_length=1)
empty_internal_squads: List[str] = Field(alias="emptyInternalSquads", min_length=1)
hwid_max_devices_exceeded: List[str] = Field(alias="HWIDMaxDevicesExceeded", min_length=1)
hwid_not_supported: List[str] = Field(alias="HWIDNotSupported", min_length=1)
class HwidSettingsDto(BaseModel):

View file

@ -9,15 +9,17 @@ from remnawave.enums import TemplateType
class TemplateResponseDto(BaseModel):
uuid: UUID
name: str
view_position: int = Field(alias="viewPosition")
template_type: TemplateType = Field(alias="templateType")
template_json: Optional[Any] = Field(None, alias="templateJson")
encoded_template_yaml: Optional[str] = Field(None, alias="encodedTemplateYaml")
template_json: Any | None = Field(alias="templateJson")
encoded_template_yaml: str | None = Field(alias="encodedTemplateYaml")
class TemplateInfoDto(BaseModel):
"""Template info without content - used in list responses"""
uuid: UUID
name: str
view_position: int = Field(alias="viewPosition")
template_type: TemplateType = Field(alias="templateType")
template_json: Optional[Any] = Field(None, alias="templateJson")
encoded_template_yaml: Optional[str] = Field(None, alias="encodedTemplateYaml")
@ -63,6 +65,19 @@ class DeleteSubscriptionTemplateResponseDto(DeleteTemplateData):
pass
class ReorderTemplateItem(BaseModel):
view_position: int = Field(serialization_alias="viewPosition")
uuid: UUID
class ReorderSubscriptionTemplatesRequestDto(BaseModel):
items: List[ReorderTemplateItem]
class ReorderSubscriptionTemplatesResponseDto(GetTemplatesData):
pass
# Legacy aliases for backward compatibility
class UpdateTemplateRequestDtoLegacy(BaseModel):
template_type: TemplateType = Field(serialization_alias="templateType")

View file

@ -171,4 +171,40 @@ class DebugSrrMatcherData(BaseModel):
class DebugSrrMatcherResponseDto(DebugSrrMatcherData):
pass
class BuildInfo(BaseModel):
"""Build information"""
time: str
number: str
class GitBackendInfo(BaseModel):
"""Git backend information"""
commit_sha: str = Field(alias="commitSha")
branch: str
commit_url: str = Field(alias="commitUrl")
class GitFrontendInfo(BaseModel):
"""Git frontend information"""
commit_sha: str = Field(alias="commitSha")
commit_url: str = Field(alias="commitUrl")
class GitInfo(BaseModel):
"""Git information"""
backend: GitBackendInfo
frontend: GitFrontendInfo
class MetadataResponse(BaseModel):
"""Metadata response data"""
version: str
build: BuildInfo
git: GitInfo
class GetMetadataResponseDto(MetadataResponse):
"""Get metadata response"""
pass

View file

@ -196,7 +196,11 @@ class RevokeUserRequestDto(BaseModel):
max_length=48,
pattern=r"^[a-zA-Z0-9_-]+$",
)
revoke_only_passwords: Optional[bool] = Field(
None,
serialization_alias="revokeOnlyPasswords",
description="Optional. If true, only passwords will be revoked without changing the short UUID.",
)
class SubscriptionRequestRecord(BaseModel):
"""Subscription request history record"""

20
test_imports.py Normal file
View file

@ -0,0 +1,20 @@
#!/usr/bin/env python3
"""Quick test to verify all new imports work"""
try:
from remnawave.models import (
ReorderConfigProfilesRequestDto,
ReorderSubscriptionTemplatesRequestDto,
ReorderInternalSquadsRequestDto,
ReorderExternalSquadsRequestDto,
GetSubpageConfigByShortUuidResponseDto,
)
print("Все новые модели успешно импортируются!")
print(" - ReorderConfigProfilesRequestDto")
print(" - ReorderSubscriptionTemplatesRequestDto")
print(" - ReorderInternalSquadsRequestDto")
print(" - ReorderExternalSquadsRequestDto")
print(" - GetSubpageConfigByShortUuidResponseDto")
except ImportError as e:
print(f"❌ Ошибка импорта: {e}")
exit(1)

View file

@ -16,7 +16,7 @@ from remnawave.models import (
GetStatsNodeUsersUsageResponseDto,
GetStatsUserUsageResponseDto,
)
from tests.utils import generate_isoformat_range
from tests.utils import generate_date_range, generate_isoformat_range
@pytest.mark.asyncio
async def test_legacy_user_usage(remnawave):
@ -27,15 +27,19 @@ async def test_legacy_user_usage(remnawave):
pytest.skip("No users available for testing")
user_uuid = str(users.users[0].uuid)
start, end = generate_isoformat_range()
start, end = generate_date_range()
user_usage = await remnawave.bandwidthstats.get_user_usage_legacy_old(
user_uuid=user_uuid,
start=start,
end=end
)
assert isinstance(user_usage, GetUserUsageByRangeResponseDto)
assert len(user_usage) >= 0
assert hasattr(user_usage, 'root')
assert isinstance(user_usage.root, list)
if user_usage.root:
first_item = user_usage.root[0]
assert hasattr(first_item, 'user_uuid')
assert hasattr(first_item, 'node_uuid')
@pytest.mark.asyncio
@ -47,14 +51,21 @@ async def test_legacy_node_user_usage(remnawave):
pytest.skip("No nodes available for testing")
node_uuid = str(nodes[0].uuid)
start, end = generate_isoformat_range()
start, end = generate_date_range()
node_user_usage = await remnawave.bandwidthstats.get_node_user_usage_legacy_old(
node_uuid=node_uuid,
start=start,
end=end
)
assert isinstance(node_user_usage, GetNodeUserUsageByRangeResponseDto)
assert hasattr(node_user_usage, 'root')
assert isinstance(node_user_usage.root, list)
if node_user_usage.root:
first_item = node_user_usage.root[0]
assert hasattr(first_item, 'user_uuid')
assert hasattr(first_item, 'username')
assert hasattr(first_item, 'node_uuid')
assert hasattr(first_item, 'total')
assert len(node_user_usage) >= 0
@ -79,7 +90,7 @@ async def test_stats_nodes_realtime_usage(remnawave):
@pytest.mark.asyncio
async def test_stats_nodes_usage(remnawave):
"""Test new stats nodes usage endpoint with charts"""
start, end = generate_isoformat_range()
start, end = generate_date_range()
nodes_usage = await remnawave.bandwidthstats.get_stats_nodes_usage(
start=start,
@ -109,7 +120,7 @@ async def test_stats_node_users_usage(remnawave):
pytest.skip("No nodes available for testing")
node_uuid = str(nodes[0].uuid)
start, end = generate_isoformat_range()
start, end = generate_date_range()
node_users_usage = await remnawave.bandwidthstats.get_stats_node_users_usage(
uuid=node_uuid,
@ -138,7 +149,7 @@ async def test_stats_user_usage(remnawave):
pytest.skip("No users available for testing")
user_uuid = str(users.users[0].uuid)
start, end = generate_isoformat_range()
start, end = generate_date_range()
user_usage = await remnawave.bandwidthstats.get_stats_user_usage(
uuid=user_uuid,
@ -169,7 +180,7 @@ async def test_legacy_stats_user_usage(remnawave):
pytest.skip("No users available for testing")
user_uuid = str(users.users[0].uuid)
start, end = generate_isoformat_range()
start, end = generate_date_range()
legacy_user_usage = await remnawave.bandwidthstats.get_user_usage_legacy_stats(
uuid=user_uuid,
@ -198,7 +209,7 @@ async def test_legacy_stats_nodes_users_usage(remnawave):
pytest.skip("No nodes available for testing")
node_uuid = str(nodes[0].uuid)
start, end = generate_isoformat_range()
start, end = generate_date_range()
legacy_node_users = await remnawave.bandwidthstats.get_node_users_usage_legacy_stats(
uuid=node_uuid,
@ -220,7 +231,7 @@ async def test_legacy_stats_nodes_users_usage(remnawave):
@pytest.mark.asyncio
async def test_bandwidth_data_structure(remnawave):
"""Test bandwidth stats data structure validity"""
start, end = generate_isoformat_range()
start, end = generate_date_range()
# Get realtime data
realtime = await remnawave.bandwidthstats.get_nodes_realtime_usage()

View file

@ -8,6 +8,9 @@ from remnawave.models import (
GetAllInboundsResponseDto,
GetConfigProfileByUuidResponseDto,
GetInboundsByProfileUuidResponseDto,
ReorderConfigProfileItem,
ReorderConfigProfilesRequestDto,
ReorderConfigProfilesResponseDto,
UpdateConfigProfileRequestDto,
UpdateConfigProfileResponseDto,
)
@ -128,6 +131,21 @@ async def test_config_profiles(remnawave) -> None:
inbounds_by_profile = await remnawave.config_profiles.get_inbounds_by_profile_uuid(profile_uuid)
assert isinstance(inbounds_by_profile, GetInboundsByProfileUuidResponseDto)
# Test reorder config profiles
all_profiles_before_reorder = await remnawave.config_profiles.get_config_profiles()
if len(all_profiles_before_reorder.config_profiles) >= 2:
items = [
ReorderConfigProfileItem(
uuid=profile.uuid,
view_position=idx
)
for idx, profile in enumerate(all_profiles_before_reorder.config_profiles)
]
reorder_result = await remnawave.config_profiles.reorder_config_profiles(
ReorderConfigProfilesRequestDto(items=items)
)
assert isinstance(reorder_result, ReorderConfigProfilesResponseDto)
# Test delete config profile
delete_profile = await remnawave.config_profiles.delete_config_profile_by_uuid(profile_uuid)
assert isinstance(delete_profile, DeleteConfigProfileResponseDto)

View file

@ -11,6 +11,9 @@ from remnawave.models import (
DeleteUsersFromInternalSquadResponseDto,
GetAllInternalSquadsResponseDto,
GetInternalSquadByUuidResponseDto,
ReorderInternalSquadItem,
ReorderInternalSquadsRequestDto,
ReorderInternalSquadsResponseDto,
UpdateInternalSquadRequestDto,
UpdateInternalSquadResponseDto,
)
@ -69,6 +72,21 @@ async def test_internal_squads(remnawave) -> None:
)
assert isinstance(remove_users, DeleteUsersFromInternalSquadResponseDto)
# Test reorder internal squads
all_squads = await remnawave.internal_squads.get_internal_squads()
if len(all_squads.internal_squads) >= 2:
items = [
ReorderInternalSquadItem(
uuid=squad.uuid,
view_position=idx
)
for idx, squad in enumerate(all_squads.internal_squads)
]
reorder_result = await remnawave.internal_squads.reorder_internal_squads(
ReorderInternalSquadsRequestDto(items=items)
)
assert isinstance(reorder_result, ReorderInternalSquadsResponseDto)
# Test delete internal squad
delete_squad = await remnawave.internal_squads.delete_internal_squad(squad_uuid)

View file

@ -11,6 +11,7 @@ from remnawave.models import (
NodesResponseDto,
ReorderNodeRequestDto,
ReorderNodeResponseDto,
ResetNodeTrafficResponseDto,
UpdateNodeRequestDto,
)
from remnawave.models.nodes import ReorderNodeItem
@ -64,6 +65,10 @@ async def test_nodes(remnawave):
assert update_node.uuid == create_node.uuid
assert update_node.name == update_name
reset_traffic = await remnawave.nodes.reset_node_traffic(uuid=string_uuid)
assert isinstance(reset_traffic, ResetNodeTrafficResponseDto)
assert reset_traffic.event_sent is True
delete_node = await remnawave.nodes.delete_node(uuid=string_uuid)
assert isinstance(delete_node, DeleteNodeResponseDto)
assert delete_node.is_deleted is True

View file

@ -6,7 +6,10 @@ from remnawave.models import (
GetSubscriptionInfoResponseDto,
GetRawSubscriptionByShortUuidResponseDto,
GetAllSubscriptionsResponseDto,
GetSubscriptionByUsernameResponseDto
GetSubscriptionByUsernameResponseDto,
GetSubpageConfigByShortUuidRequestBodyDto,
GetSubpageConfigByShortUuidResponseDto,
SubpageConfigData,
)
from tests.conftest import REMNAWAVE_SHORT_UUID, REMNAWAVE_USER_USERNAME
@ -71,4 +74,16 @@ class TestSubscriptionsManagement:
subscription_by_username = await remnawave.subscriptions.get_subscription_by_username(
username=REMNAWAVE_USER_USERNAME
)
assert isinstance(subscription_by_username, GetSubscriptionByUsernameResponseDto)
assert isinstance(subscription_by_username, GetSubscriptionByUsernameResponseDto)
@pytest.mark.asyncio
async def test_get_subpage_config(self, remnawave):
"""Тест получения конфига страницы подписки по short UUID"""
body = GetSubpageConfigByShortUuidRequestBodyDto(request_headers={})
subpage_config = await remnawave.subscriptions.get_subpage_config(
short_uuid=REMNAWAVE_SHORT_UUID,
body=body,
)
# Client auto-unwraps single "response" field → returns SubpageConfigData
assert isinstance(subpage_config, (GetSubpageConfigByShortUuidResponseDto, SubpageConfigData))
assert hasattr(subpage_config, 'webpage_allowed')

View file

@ -6,6 +6,9 @@ from remnawave.models import (
DeleteSubscriptionTemplateResponseDto,
GetTemplateResponseDto,
GetTemplatesResponseDto,
ReorderTemplateItem,
ReorderSubscriptionTemplatesRequestDto,
ReorderSubscriptionTemplatesResponseDto,
UpdateTemplateRequestDto,
UpdateTemplateResponseDto,
)
@ -88,4 +91,24 @@ async def test_delete_template(remnawave):
str(created.uuid)
)
assert isinstance(delete_response, DeleteSubscriptionTemplateResponseDto)
assert delete_response.is_deleted is True
assert delete_response.is_deleted is True
@pytest.mark.asyncio
async def test_reorder_templates(remnawave):
"""Проверка изменения порядка шаблонов"""
templates = await remnawave.subscriptions_template.get_all_templates()
assert isinstance(templates, GetTemplatesResponseDto)
if len(templates.templates) >= 2:
items = [
ReorderTemplateItem(
uuid=tmpl.uuid,
view_position=idx
)
for idx, tmpl in enumerate(templates.templates)
]
reorder_result = await remnawave.subscriptions_template.reorder_templates(
ReorderSubscriptionTemplatesRequestDto(items=items)
)
assert isinstance(reorder_result, ReorderSubscriptionTemplatesResponseDto)

View file

@ -22,3 +22,9 @@ def generate_isoformat_range() -> Tuple[str, str]:
start = (datetime.now() - timedelta(days=7)).isoformat(timespec="seconds")
end = datetime.now().isoformat(timespec="seconds")
return start, end
def generate_date_range() -> tuple[str, str]:
"""Generate date range in YYYY-MM-DD format for the past 7 days"""
end = datetime.now()
start = end - timedelta(days=7)
return start.strftime('%Y-%m-%d'), end.strftime('%Y-%m-%d')