feat: add nodes metrics endpoint - метод get_nodes_metrics()

feat: add host tags management - метод get_hosts_tags()
feat: add advanced host options - поля tag, isHidden, muxParams, sockoptParams
update: relax validation - username min 3 chars, node name min 3 chars
update: reduce host description - max 30 chars (было 50)
update: add name fields - для config profiles и internal squads

‼️ fix: remove tokenDescription - убрано из CreateApiTokenRequestDto
This commit is contained in:
Artem 2025-08-14 15:01:15 +02:00
parent 9a3dff13cb
commit 08f2146d41
12 changed files with 159 additions and 245 deletions

View file

@ -1,197 +0,0 @@
# Remnawave SDK v2.0.0 Migration Summary
## ✅ Completed Tasks
### 1. Full API v2.0.0 Compatibility
- ✅ **Complete SDK refactoring** for OpenAPI v2.0.0 specification compliance
- ✅ **All controllers updated** with new/modified endpoints and response models
- ✅ **New controllers added**: ConfigProfiles, InternalSquads, InfraBilling, NodesUsageHistory
- ✅ **Deprecated endpoints removed** that are no longer present in API v2.0.0
- ✅ **Response handling enhanced** to support both wrapped and unwrapped API responses
### 2. Models Complete Overhaul
- ✅ **All models updated** to match real API v2.0.0 response structures
- ✅ **Field aliases corrected** based on actual API responses (camelCase vs snake_case)
- ✅ **Optional fields properly marked** where API may omit values
- ✅ **RootModel patterns implemented** for list responses with iteration/indexing support
- ✅ **Validation errors fixed** for complex nested models (NodeConfigProfileDto, InboundsDto, etc.)
- ✅ **Real API response structures verified** via curl testing and updated models accordingly
### 3. Comprehensive Testing
- ✅ **21 test suites updated** to work with new API v2.0.0 models
- ✅ **Model validation verified** against actual API responses
- ✅ **Error handling updated** for new API v2.0.0 error formats
- ✅ **Response unwrapping logic** tested and working correctly
### 4. Bug Fixes & Compatibility
- ✅ **Response handling in client.py** updated to handle both wrapped and direct responses
- ✅ **Import/export system** updated in all `__init__.py` files
- ✅ **AttributeBody vs PydanticBody** issues resolved in controllers
- ✅ **Field type mismatches** corrected (Optional vs required fields)
- ✅ **RootModel inheritance** fixed for list response models
## 🔧 Key Changes Summary
### Response Model Patterns Fixed
**Problem Solved: API Response Structure Mismatch**
```python
# API v2.0.0 returns paginated responses like:
{
"response": {
"total": 6,
"configProfiles": [...]
}
}
# Fixed models to handle this correctly:
class GetAllConfigProfilesResponsePaginated(BaseModel):
total: int
config_profiles: List[ConfigProfileDto] = Field(alias="configProfiles")
class GetAllConfigProfilesResponseDto(BaseModel):
response: GetAllConfigProfilesResponsePaginated
```
### RootModel Implementation for Lists
```python
# Enhanced list responses with direct access:
class GetAllNodesResponseDto(RootModel[List[NodeResponseDto]]):
def __iter__(self):
return iter(self.root)
def __getitem__(self, item):
return self.root[item]
# Usage:
nodes = await client.nodes.get_all_nodes()
for node in nodes: # Direct iteration
print(node.name)
first_node = nodes[0] # Direct indexing
```
### Field Alias Corrections
```python
# Fixed camelCase vs snake_case mapping issues:
class NodeConfigProfileDto(BaseModel):
active_config_profile_uuid: UUID = Field(alias="activeConfigProfileUuid")
active_inbounds: List[InboundsDto] = Field(alias="activeInbounds")
# Fixed optional fields where API may omit values:
class InboundsDto(BaseModel):
network: Optional[str] = None # Was: str = Field(default=None)
security: Optional[str] = None
port: Optional[float] = None
```
### New Controllers Available
- `ConfigProfilesController` - Configuration profile management
- `InternalSquadsController` - Squad operations with user management
- `InfraBillingController` - Infrastructure billing and provider management
- `NodesUsageHistoryController` - Historical usage data and statistics
## 🚀 Production Ready Status
The SDK is now **fully compatible** with Remnawave API v2.0.0 and ready for production use!
### What's Working:
- ✅ **All CRUD operations** for hosts, nodes, users, inbounds, etc.
- ✅ **New v2.0.0 features** including config profiles and internal squads
- ✅ **Response handling** automatically unwraps API responses
- ✅ **Type safety** with proper Pydantic model validation
- ✅ **Backward compatibility** maintained where possible
### Known Issues:
- ⚠️ **User creation endpoint** returns 500 error (backend issue, not SDK)
- ⚠️ **Some create operations** require valid UUIDs from existing resources
## 📋 Breaking Changes from v1.x
**Minimal breaking changes** - mostly additive improvements:
1. **List Response Models**: Now use `RootModel` for direct iteration
```python
# v1.x: hosts_list = response.__root__
# v2.0: for host in response: ... # Direct iteration now possible
```
2. **Response Structure Updates**: Some responses now have pagination info
```python
# Config profiles now return paginated response:
profiles = await client.config_profiles.get_config_profiles()
total = profiles.response.total
profile_list = profiles.response.config_profiles
```
3. **New Required Fields**: Some models have new required fields
```python
# NodeConfigProfileDto now requires active_config_profile_uuid and active_inbounds
# CreateHostRequestDto now requires config_profile_inbound_uuid
```
## 🔧 Migration Path
**Good news**: Most existing code will continue to work with minimal changes!
```python
# ✅ Basic operations unchanged
client = RemnawaveSDK(base_url="...", username="...", password="...")
async with client:
# Host operations work the same
host = await client.hosts.create_host(request)
uuid = host.uuid # Direct field access still works
# Authentication unchanged
login_response = await client.auth.login(credentials)
token = login_response.access_token
# ✅ List responses get enhanced capabilities
hosts = await client.hosts.get_all_hosts()
# Old way still works: host_list = hosts.root
# New way is more convenient:
for host in hosts: # ✅ Direct iteration
print(host.uuid)
first_host = hosts[0] # ✅ Direct indexing
# ✅ New paginated responses
profiles = await client.config_profiles.get_config_profiles()
total_count = profiles.total
profile_list = profiles.config_profiles
```
### Required Changes:
1. **Update imports** if using new controllers:
```python
# Add new imports for v2.0.0 features
from remnawave_api.models import (
GetAllConfigProfilesResponseDto,
CreateInternalSquadRequestDto,
# ... other new models
)
```
2. **Handle paginated responses** for some endpoints:
```python
# Config profiles now return paginated data
result = await client.config_profiles.get_config_profiles()
profiles = result.config_profiles
total = result.total
```
3. **Update creation requests** that now require additional fields:
```python
# Host creation now requires config_profile_inbound_uuid
host_request = CreateHostRequestDto(
inbound_uuid="...",
config_profile_inbound_uuid="...", # New required field
remark="...",
address="...",
port=...
)
```
---
**Status**: ✅ **SDK FULLY UPDATED AND PRODUCTION READY**
- **API Compatibility**: Full OpenAPI v2.0.0 compliance
- **Backward Compatibility**: Most existing code continues to work
- **New Features**: Config profiles, internal squads, billing management
- **Response Handling**: Enhanced with automatic unwrapping and direct iteration
**Ready for deployment!** 🚀

View file

View file

@ -13,6 +13,7 @@ from remnawave.models import (
ReorderHostResponseDto,
UpdateHostRequestDto,
UpdateHostResponseDto,
GetAllHostTagsResponseDto,
)
from remnawave.rapid import AttributeBody, BaseController, delete, get, post, patch
@ -40,7 +41,14 @@ class HostsController(BaseController):
) -> GetAllHostsResponseDto:
"""Get All Hosts"""
...
@get("/hosts/tags", response_class=GetAllHostTagsResponseDto)
async def get_hosts_tags(
self,
) -> GetAllHostTagsResponseDto:
"""Get Hosts Tags"""
...
@delete("/hosts/{uuid}", response_class=DeleteHostResponseDto)
async def delete_host(
self,

View file

@ -2,6 +2,8 @@ from remnawave.models import (
GetBandwidthStatsResponseDto,
GetNodesStatisticsResponseDto,
GetStatsResponseDto,
GetNodesMetricsResponseDto,
GetRemnawaveHealthResponseDto,
)
from remnawave.rapid import BaseController, get
@ -27,3 +29,17 @@ class SystemController(BaseController):
) -> GetNodesStatisticsResponseDto:
"""Get Nodes Statistics"""
...
@get("/system/health", response_class=GetRemnawaveHealthResponseDto)
async def get_health(
self,
) -> GetRemnawaveHealthResponseDto:
"""Get System Health"""
...
@get("/system/nodes/metrics", response_class=GetNodesMetricsResponseDto)
async def get_nodes_metrics(
self,
) -> GetNodesMetricsResponseDto:
"""Get Nodes Metrics"""
...

View file

@ -54,6 +54,7 @@ from .hosts import (
ReorderHostResponseDto,
UpdateHostRequestDto,
UpdateHostResponseDto,
GetAllHostTagsResponseDto,
)
from .hosts_bulk_actions import (
BulkDeleteHostsResponseDto,
@ -192,6 +193,7 @@ from .system import (
StatisticResponseDto,
StatusCounts,
UsersStatistic,
GetNodesMetricsResponseDto,
)
from .users import (
ActiveInternalSquadDto,
@ -205,7 +207,7 @@ from .users import (
UserResponseDto,
UsersResponseDto,
TagsResponseDto,
RevokeUserRequestDto
RevokeUserRequestDto,
)
from .users_bulk_actions import (
BulkAllResetTrafficUsersResponseDto,
@ -234,7 +236,6 @@ __all__ = [
"TelegramCallbackRequestDto",
"TelegramCallbackResponseDto",
"LoginTelegramRequestDto", # Legacy alias
# Nodes models
"CreateNodeRequestDto",
"CreateNodeResponseDto",
@ -252,7 +253,6 @@ __all__ = [
"RestartNodeResponseDto",
"UpdateNodeRequestDto",
"UpdateNodeResponseDto",
# Hosts models
"CreateHostRequestDto",
"CreateHostResponseDto",
@ -265,7 +265,7 @@ __all__ = [
"ReorderHostResponseDto",
"UpdateHostRequestDto",
"UpdateHostResponseDto",
"GetAllHostTagsResponseDto",
# Inbounds models
"FullInboundResponseDto",
"FullInboundStatistic",
@ -274,30 +274,25 @@ __all__ = [
"GetInboundsResponseDto",
"InboundResponseDto",
"InboundsResponseDto", # Legacy alias
# Keygen models
"GetPubKeyResponseDto",
"PubKeyResponseDto", # Legacy alias
# Subscription models
"GetAllSubscriptionsResponseDto",
"GetSubscriptionByUsernameResponseDto",
"GetSubscriptionInfoResponseDto",
"SubscriptionInfoResponseDto", # Legacy alias
"UserSubscription",
# Subscription settings models
"GetSubscriptionSettingsResponseDto",
"SubscriptionSettingsResponseDto",
"UpdateSubscriptionSettingsRequestDto",
"UpdateSubscriptionSettingsResponseDto",
# Subscription template models
"GetTemplateResponseDto",
"TemplateResponseDto",
"UpdateTemplateRequestDto",
"UpdateTemplateResponseDto",
# System models
"BandwidthStatistic",
"BandwidthStatisticResponseDto",
@ -313,13 +308,12 @@ __all__ = [
"StatisticResponseDto",
"StatusCounts",
"UsersStatistic",
"GetNodesMetricsResponseDto",
# XRay config models
"ConfigResponseDto", # Legacy alias
"GetConfigResponseDto",
"UpdateConfigRequestDto",
"UpdateConfigResponseDto",
# HWID models
"CreateHWIDUser", # Legacy alias
"CreateUserHwidDeviceRequestDto",
@ -330,7 +324,6 @@ __all__ = [
"HWIDDeleteRequest", # Legacy alias
"HWIDUserResponseDto", # Legacy alias
"HWIDUserResponseDtoList", # Legacy alias
# Bandwidth stats models
"GetNodeUserUsageByRangeResponseDto",
"GetNodesRealtimeUsageResponseDto",
@ -340,19 +333,16 @@ __all__ = [
"NodeUsageResponseDto",
"NodesRealtimeUsageResponseDto", # Legacy alias
"NodesUsageResponseDto", # Legacy alias
# API Tokens models
"CreateApiTokenRequestDto",
"CreateApiTokenResponseDto",
"DeleteApiTokenResponseDto",
"FindAllApiTokensResponseDto",
# Inbound bulk actions models
"AddInboundToNodesResponseDto",
"AddInboundToUsersResponseDto",
"RemoveInboundFromNodesResponseDto",
"RemoveInboundFromUsersResponseDto",
# Host bulk actions models
"BulkDeleteHostsResponseDto",
"BulkDisableHostsResponseDto",
@ -360,7 +350,6 @@ __all__ = [
"SetInboundToManyHostsRequestDto",
"SetInboundToManyHostsResponseDto",
"SetPortToManyHostsResponseDto",
# Users models
"ActiveInternalSquadDto",
"CreateUserRequestDto",
@ -388,7 +377,6 @@ __all__ = [
"UserLastConnectedNodeDto",
"UserResponseDto",
"UsersResponseDto",
# Users bulk actions models
"BulkAllResetTrafficUsersResponseDto",
"BulkAllUpdateUsersRequestDto",
@ -396,11 +384,9 @@ __all__ = [
"BulkResponseDto",
"BulkUpdateUsersInboundsRequestDto",
"UpdateUserFields",
# Users stats models
"UserUsageByRange",
"UserUsageByRangeResponseDto",
# Config profiles models
"ConfigProfileDto",
"CreateConfigProfileRequestDto",
@ -413,7 +399,6 @@ __all__ = [
"InboundDto",
"UpdateConfigProfileRequestDto",
"UpdateConfigProfileResponseDto",
# Infra billing models
"CreateInfraBillingNodeRequestDto",
"CreateInfraBillingNodeResponseDto",
@ -435,7 +420,6 @@ __all__ = [
"UpdateInfraBillingNodeResponseDto",
"UpdateInfraProviderRequestDto",
"UpdateInfraProviderResponseDto",
# Internal squads models
"AddUsersToInternalSquadRequestDto",
"AddUsersToInternalSquadResponseDto",
@ -449,7 +433,6 @@ __all__ = [
"InternalSquadDto",
"UpdateInternalSquadRequestDto",
"UpdateInternalSquadResponseDto",
# Nodes usage history models
"GetNodeUserUsageByRangeResponseDto",
"GetNodesUsageByRangeResponseDto",

View file

@ -6,9 +6,6 @@ from pydantic import BaseModel, ConfigDict, Field
class CreateApiTokenRequestDto(BaseModel):
token_name: str = Field(serialization_alias="tokenName")
token_description: Optional[str] = Field(
None, serialization_alias="tokenDescription"
)
class CreateApiTokenResponseData(BaseModel):
@ -28,7 +25,6 @@ class ApiTokenDto(BaseModel):
uuid: str
token: str
token_name: str = Field(..., alias="tokenName")
token_description: Optional[str] = Field(None, alias="tokenDescription")
created_at: datetime = Field(..., alias="createdAt")
updated_at: datetime = Field(..., alias="updatedAt")
@ -45,4 +41,4 @@ class FindAllApiTokensResponseData(BaseModel):
class FindAllApiTokensResponseDto(FindAllApiTokensResponseData):
pass
pass

View file

@ -37,7 +37,7 @@ class CreateConfigProfileResponseDto(ConfigProfileDto):
class UpdateConfigProfileRequestDto(BaseModel):
uuid: UUID
# name: Optional[str] = None
name: Optional[str] = Field(None, pattern=r"^[A-Za-z0-9_-]+$")
config: Optional[Dict[str, Any]] = None
@ -53,6 +53,7 @@ class GetAllConfigProfilesResponsePaginated(BaseModel):
class GetAllConfigProfilesResponseDto(GetAllConfigProfilesResponsePaginated):
pass
class GetConfigProfileByUuidResponseDto(ConfigProfileDto):
pass

View file

@ -39,6 +39,28 @@ class UpdateHostRequestDto(BaseModel):
security_layer: Optional[SecurityLayer] = Field(
None, serialization_alias="securityLayer"
)
server_description: Optional[str] = Field(
None, alias="serverDescription", max_length=30
)
muxParams: Optional[str] = Field(
None,
serialization_alias="muxParams",
)
sockopt_params: Optional[str] = Field(
None,
serialization_alias="sockoptParams",
)
tag: Optional[Annotated[str, StringConstraints(max_length=32)]] = Field(
None, serialization_alias="tag"
)
is_hidden: Optional[bool] = Field(
None,
serialization_alias="isHidden",
)
override_sni_from_address: Optional[bool] = Field(
None,
serialization_alias="overrideSniFromAddress",
)
class HostInboundData(BaseModel):
@ -70,11 +92,29 @@ class HostResponseDto(BaseModel):
alias="xHttpExtraParams",
)
server_description: Optional[str] = Field(
None,
alias="serverDescription",
None, alias="serverDescription", max_length=30
)
inbound: HostInboundData
muxParams: Optional[str] = Field(
None,
serialization_alias="muxParams",
)
sockopt_params: Optional[str] = Field(
None,
serialization_alias="sockoptParams",
)
tag: Optional[Annotated[str, StringConstraints(max_length=32)]] = Field(
None, serialization_alias="tag"
)
is_hidden: Optional[bool] = Field(
None,
serialization_alias="isHidden",
)
override_sni_from_address: Optional[bool] = Field(
None,
serialization_alias="overrideSniFromAddress",
)
# Legacy compatibility property
@property
def inbound_uuid(self) -> UUID:
@ -93,6 +133,10 @@ class UpdateHostResponseDto(HostResponseDto):
pass
class GetAllHostTagsResponseDto(BaseModel):
tags: list[str] = None
class GetAllHostsResponseDto(RootModel[List[HostResponseDto]]):
root: List[HostResponseDto]
@ -117,7 +161,9 @@ class DeleteHostResponseDto(BaseModel):
class CreateHostInboundData(BaseModel):
config_profile_uuid: UUID = Field(serialization_alias="configProfileUuid")
config_profile_inbound_uuid: UUID = Field(serialization_alias="configProfileInboundUuid")
config_profile_inbound_uuid: UUID = Field(
serialization_alias="configProfileInboundUuid"
)
class CreateHostRequestDto(BaseModel):
@ -142,14 +188,47 @@ class CreateHostRequestDto(BaseModel):
None,
serialization_alias="securityLayer",
)
muxParams: Optional[str] = Field(
None,
serialization_alias="muxParams",
)
sockopt_params: Optional[str] = Field(
None,
serialization_alias="sockoptParams",
)
tag: Optional[Annotated[str, StringConstraints(max_length=32)]] = Field(
None, serialization_alias="tag"
)
is_hidden: Optional[bool] = Field(
None,
serialization_alias="isHidden",
)
override_sni_from_address: Optional[bool] = Field(
None,
serialization_alias="overrideSniFromAddress",
)
server_description: Optional[str] = Field(
None, alias="serverDescription", max_length=30
)
# Legacy compatibility property
@property
def inbound_uuid(self) -> UUID:
return self.inbound.config_profile_inbound_uuid
# Constructor compatibility - support old-style inbound_uuid
def __init__(self, inbound_uuid: Optional[UUID] = None, config_profile_uuid: Optional[UUID] = None, **data):
if inbound_uuid is not None and 'inbound' not in data:
def __init__(
self,
inbound_uuid: Optional[UUID] = None,
config_profile_uuid: Optional[UUID] = None,
**data,
):
if inbound_uuid is not None and "inbound" not in data:
# Legacy mode: create inbound object from UUID
# Use hardcoded config_profile_uuid from API response for compatibility
data['inbound'] = CreateHostInboundData(
config_profile_uuid=config_profile_uuid or UUID("107541f1-ae1a-4e2d-9dec-7297557b5125"),
config_profile_inbound_uuid=inbound_uuid
data["inbound"] = CreateHostInboundData(
config_profile_uuid=config_profile_uuid
or UUID("107541f1-ae1a-4e2d-9dec-7297557b5125"),
config_profile_inbound_uuid=inbound_uuid,
)
super().__init__(**data)

View file

@ -4,6 +4,7 @@ from uuid import UUID
from pydantic import BaseModel, Field
class InboundsDto(BaseModel):
uuid: UUID
profile_uuid: UUID = Field(alias="profileUuid")
@ -14,10 +15,12 @@ class InboundsDto(BaseModel):
port: Optional[float] = Field(default=None)
raw_inbound: Optional[dict] = Field(default=None, alias="rawInbound")
class InfoDto(BaseModel):
members_count: int = Field(alias="membersCount")
inbounds_count: int = Field(alias="inboundsCount")
class InternalSquadDto(BaseModel):
uuid: UUID
name: str
@ -39,15 +42,18 @@ class CreateInternalSquadResponseDto(InternalSquadDto):
class UpdateInternalSquadRequestDto(BaseModel):
uuid: UUID
inbounds: List[UUID] = Field(default_factory=list)
name: Optional[str] = Field(None, pattern=r"^[A-Za-z0-9_-]+$")
class UpdateInternalSquadResponseDto(InternalSquadDto):
pass
class GetAllInternalSquadsResponse(BaseModel):
total: int
internal_squads: List[InternalSquadDto] = Field(alias="internalSquads")
class GetAllInternalSquadsResponseDto(GetAllInternalSquadsResponse):
pass
@ -63,9 +69,11 @@ class DeleteInternalSquadResponseDto(BaseModel):
class AddUsersToInternalSquadRequestDto(BaseModel):
user_uuids: List[UUID] = Field(alias="userUuids")
class BulkActionsResponseDto(BaseModel):
event_sent: bool = Field(alias="eventSent")
class AddUsersToInternalSquadResponseDto(BulkActionsResponseDto):
pass

View file

@ -39,7 +39,7 @@ class NodeConfigProfileRequestDto(BaseModel):
class CreateNodeRequestDto(BaseModel):
name: Annotated[str, StringConstraints(min_length=5)]
name: Annotated[str, StringConstraints(min_length=3)]
address: Annotated[str, StringConstraints(min_length=2)]
port: Optional[int] = Field(None, strict=True, ge=1)
is_traffic_tracking_active: Optional[bool] = Field(
@ -64,13 +64,15 @@ class CreateNodeRequestDto(BaseModel):
consumption_multiplier: Optional[float] = Field(
None, serialization_alias="consumptionMultiplier"
)
config_profile: NodeConfigProfileRequestDto = Field(serialization_alias="configProfile")
config_profile: NodeConfigProfileRequestDto = Field(
serialization_alias="configProfile"
)
provider_uuid: Optional[UUID] = Field(None, serialization_alias="providerUuid")
class UpdateNodeRequestDto(BaseModel):
uuid: UUID
name: Annotated[Optional[str], StringConstraints(min_length=5)] = None
name: Annotated[Optional[str], StringConstraints(min_length=3)] = None
address: Annotated[Optional[str], StringConstraints(min_length=2)] = None
port: Optional[int] = None
is_traffic_tracking_active: Optional[bool] = Field(

View file

@ -103,3 +103,21 @@ class GetNodesStatisticsResponseDto(BaseModel):
class GetRemnawaveHealthResponseDto(BaseModel):
pm2_stats: List[PM2Stat] = Field(alias="pm2Stats")
class NodeMetric(BaseModel):
uuid: str
name: str
address: str
is_online: bool = Field(alias="isOnline")
cpu_usage: float = Field(alias="cpuUsage")
memory_usage: float = Field(alias="memoryUsage")
network_upload: int = Field(alias="networkUpload")
network_download: int = Field(alias="networkDownload")
uptime: int
last_seen: datetime.datetime = Field(alias="lastSeen")
connected_users: int = Field(alias="connectedUsers")
class GetNodesMetricsResponseDto(BaseModel):
response: List[NodeMetric]

View file

@ -33,9 +33,10 @@ class ActiveInternalSquadDto(BaseModel):
class HappCrypto(BaseModel):
cryptoLink: str
class CreateUserRequestDto(BaseModel):
username: Annotated[
str, StringConstraints(pattern=r"^[a-zA-Z0-9_-]+$", min_length=6, max_length=36)
str, StringConstraints(pattern=r"^[a-zA-Z0-9_-]+$", min_length=3, max_length=36)
]
status: Optional[UserStatus] = None
subscription_uuid: Optional[str] = Field(
@ -139,9 +140,7 @@ class UserResponseDto(BaseModel):
)
subscription_url: str = Field(alias="subscriptionUrl")
first_connected: Optional[datetime] = Field(None, alias="firstConnectedAt")
last_trigger_threshold: Optional[int] = Field(
None, alias="lastTriggeredThreshold"
)
last_trigger_threshold: Optional[int] = Field(None, alias="lastTriggeredThreshold")
last_connected_node: Optional[UserLastConnectedNodeDto] = Field(
None, alias="lastConnectedNode"
)
@ -217,7 +216,8 @@ class GetUserByShortUuidResponseDto(UserResponseDto):
class GetUserByUsernameResponseDto(UserResponseDto):
pass
class RevokeUserRequestDto(BaseModel):
short_uuid: Optional[str] = Field(
None,
@ -226,4 +226,4 @@ class RevokeUserRequestDto(BaseModel):
min_length=6,
max_length=48,
pattern=r"^[a-zA-Z0-9_-]+$",
)
)