nolag 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nolag/__init__.py +84 -0
- nolag/api.py +268 -0
- nolag/api_types.py +330 -0
- nolag/client.py +862 -0
- nolag/types.py +104 -0
- nolag/webrtc.py +474 -0
- nolag-2.0.0.dist-info/METADATA +329 -0
- nolag-2.0.0.dist-info/RECORD +9 -0
- nolag-2.0.0.dist-info/WHEEL +4 -0
nolag/__init__.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NoLag Python SDK
|
|
3
|
+
Real-time messaging for Python applications
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .client import NoLag
|
|
7
|
+
from .types import (
|
|
8
|
+
NoLagOptions,
|
|
9
|
+
ConnectionStatus,
|
|
10
|
+
ActorType,
|
|
11
|
+
QoS,
|
|
12
|
+
SubscribeOptions,
|
|
13
|
+
EmitOptions,
|
|
14
|
+
MessageMeta,
|
|
15
|
+
ActorPresence,
|
|
16
|
+
LobbyPresenceEvent,
|
|
17
|
+
LobbyPresenceState,
|
|
18
|
+
LobbyPresenceHandler,
|
|
19
|
+
)
|
|
20
|
+
from .api import NoLagApi, NoLagApiError
|
|
21
|
+
from .api_types import (
|
|
22
|
+
NoLagApiOptions,
|
|
23
|
+
ListOptions,
|
|
24
|
+
PaginatedResult,
|
|
25
|
+
ApiError,
|
|
26
|
+
App,
|
|
27
|
+
AppCreate,
|
|
28
|
+
AppUpdate,
|
|
29
|
+
Room,
|
|
30
|
+
RoomCreate,
|
|
31
|
+
RoomUpdate,
|
|
32
|
+
Actor,
|
|
33
|
+
ActorWithToken,
|
|
34
|
+
ActorCreate,
|
|
35
|
+
ActorUpdate,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# WebRTC support (optional, requires aiortc)
|
|
39
|
+
try:
|
|
40
|
+
from .webrtc import WebRTCManager, WebRTCOptions, is_webrtc_available
|
|
41
|
+
_WEBRTC_AVAILABLE = True
|
|
42
|
+
except ImportError:
|
|
43
|
+
_WEBRTC_AVAILABLE = False
|
|
44
|
+
WebRTCManager = None
|
|
45
|
+
WebRTCOptions = None
|
|
46
|
+
is_webrtc_available = lambda: False
|
|
47
|
+
|
|
48
|
+
__version__ = "2.0.0"
|
|
49
|
+
__all__ = [
|
|
50
|
+
# WebSocket Client
|
|
51
|
+
"NoLag",
|
|
52
|
+
"NoLagOptions",
|
|
53
|
+
"ConnectionStatus",
|
|
54
|
+
"ActorType",
|
|
55
|
+
"QoS",
|
|
56
|
+
"SubscribeOptions",
|
|
57
|
+
"EmitOptions",
|
|
58
|
+
"MessageMeta",
|
|
59
|
+
"ActorPresence",
|
|
60
|
+
"LobbyPresenceEvent",
|
|
61
|
+
"LobbyPresenceState",
|
|
62
|
+
"LobbyPresenceHandler",
|
|
63
|
+
# REST API Client
|
|
64
|
+
"NoLagApi",
|
|
65
|
+
"NoLagApiError",
|
|
66
|
+
"NoLagApiOptions",
|
|
67
|
+
"ListOptions",
|
|
68
|
+
"PaginatedResult",
|
|
69
|
+
"ApiError",
|
|
70
|
+
"App",
|
|
71
|
+
"AppCreate",
|
|
72
|
+
"AppUpdate",
|
|
73
|
+
"Room",
|
|
74
|
+
"RoomCreate",
|
|
75
|
+
"RoomUpdate",
|
|
76
|
+
"Actor",
|
|
77
|
+
"ActorWithToken",
|
|
78
|
+
"ActorCreate",
|
|
79
|
+
"ActorUpdate",
|
|
80
|
+
# WebRTC (optional)
|
|
81
|
+
"WebRTCManager",
|
|
82
|
+
"WebRTCOptions",
|
|
83
|
+
"is_webrtc_available",
|
|
84
|
+
]
|
nolag/api.py
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NoLag REST API Client
|
|
3
|
+
|
|
4
|
+
Provides management operations for NoLag resources via REST API.
|
|
5
|
+
API keys are scoped to a specific project, so no organization or project IDs needed.
|
|
6
|
+
|
|
7
|
+
Use this for managing apps, rooms, and actors within your project.
|
|
8
|
+
For real-time messaging, use the main NoLag WebSocket client.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import aiohttp
|
|
12
|
+
from typing import Any, Optional
|
|
13
|
+
from urllib.parse import urljoin, urlencode
|
|
14
|
+
|
|
15
|
+
from .api_types import (
|
|
16
|
+
NoLagApiOptions,
|
|
17
|
+
ListOptions,
|
|
18
|
+
PaginatedResult,
|
|
19
|
+
ApiError,
|
|
20
|
+
App,
|
|
21
|
+
AppCreate,
|
|
22
|
+
AppUpdate,
|
|
23
|
+
Room,
|
|
24
|
+
RoomCreate,
|
|
25
|
+
RoomUpdate,
|
|
26
|
+
Actor,
|
|
27
|
+
ActorWithToken,
|
|
28
|
+
ActorCreate,
|
|
29
|
+
ActorUpdate,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
DEFAULT_BASE_URL = "https://api.nolag.app/v1"
|
|
33
|
+
DEFAULT_TIMEOUT = 30.0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class NoLagApiError(Exception):
|
|
37
|
+
"""NoLag API Error"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, message: str, status_code: int, details: Optional[ApiError] = None):
|
|
40
|
+
super().__init__(message)
|
|
41
|
+
self.status_code = status_code
|
|
42
|
+
self.details = details or ApiError(status_code=status_code, message=message)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class AppsApi:
|
|
46
|
+
"""Apps API - Manage apps in your project"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, api: "NoLagApi"):
|
|
49
|
+
self._api = api
|
|
50
|
+
|
|
51
|
+
async def list(self, options: Optional[ListOptions] = None) -> PaginatedResult:
|
|
52
|
+
"""List all apps in the project"""
|
|
53
|
+
params = options.to_params() if options else {}
|
|
54
|
+
data = await self._api._request("GET", "/apps", params=params)
|
|
55
|
+
return PaginatedResult(
|
|
56
|
+
data=[App.from_dict(item) for item in data.get("data", [])],
|
|
57
|
+
total=data.get("total", 0),
|
|
58
|
+
page=data.get("page", 1),
|
|
59
|
+
limit=data.get("limit", 10),
|
|
60
|
+
total_pages=data.get("totalPages", 1),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
async def get(self, app_id: str) -> App:
|
|
64
|
+
"""Get an app by ID"""
|
|
65
|
+
data = await self._api._request("GET", f"/apps/{app_id}")
|
|
66
|
+
return App.from_dict(data)
|
|
67
|
+
|
|
68
|
+
async def create(self, data: AppCreate) -> App:
|
|
69
|
+
"""Create a new app"""
|
|
70
|
+
result = await self._api._request("POST", "/apps", json=data.to_dict())
|
|
71
|
+
return App.from_dict(result)
|
|
72
|
+
|
|
73
|
+
async def update(self, app_id: str, data: AppUpdate) -> App:
|
|
74
|
+
"""Update an app"""
|
|
75
|
+
result = await self._api._request("PATCH", f"/apps/{app_id}", json=data.to_dict())
|
|
76
|
+
return App.from_dict(result)
|
|
77
|
+
|
|
78
|
+
async def delete(self, app_id: str) -> dict:
|
|
79
|
+
"""Delete an app (soft delete)"""
|
|
80
|
+
return await self._api._request("DELETE", f"/apps/{app_id}")
|
|
81
|
+
|
|
82
|
+
async def reset_to_blueprint(self, app_id: str) -> App:
|
|
83
|
+
"""Reset app to its blueprint configuration"""
|
|
84
|
+
result = await self._api._request("POST", f"/apps/{app_id}/reset-to-blueprint")
|
|
85
|
+
return App.from_dict(result)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class RoomsApi:
|
|
89
|
+
"""Rooms API - Manage rooms within apps"""
|
|
90
|
+
|
|
91
|
+
def __init__(self, api: "NoLagApi"):
|
|
92
|
+
self._api = api
|
|
93
|
+
|
|
94
|
+
async def list(self, app_id: str) -> list[Room]:
|
|
95
|
+
"""List all rooms in an app"""
|
|
96
|
+
data = await self._api._request("GET", f"/apps/{app_id}/rooms")
|
|
97
|
+
return [Room.from_dict(item) for item in data]
|
|
98
|
+
|
|
99
|
+
async def get(self, app_id: str, room_id: str) -> Room:
|
|
100
|
+
"""Get a room by ID"""
|
|
101
|
+
data = await self._api._request("GET", f"/apps/{app_id}/rooms/{room_id}")
|
|
102
|
+
return Room.from_dict(data)
|
|
103
|
+
|
|
104
|
+
async def create(self, app_id: str, data: RoomCreate) -> Room:
|
|
105
|
+
"""Create a new dynamic room"""
|
|
106
|
+
result = await self._api._request("POST", f"/apps/{app_id}/rooms", json=data.to_dict())
|
|
107
|
+
return Room.from_dict(result)
|
|
108
|
+
|
|
109
|
+
async def update(self, app_id: str, room_id: str, data: RoomUpdate) -> Room:
|
|
110
|
+
"""Update a room"""
|
|
111
|
+
result = await self._api._request(
|
|
112
|
+
"PATCH", f"/apps/{app_id}/rooms/{room_id}", json=data.to_dict()
|
|
113
|
+
)
|
|
114
|
+
return Room.from_dict(result)
|
|
115
|
+
|
|
116
|
+
async def delete(self, app_id: str, room_id: str) -> None:
|
|
117
|
+
"""Delete a dynamic room (static rooms cannot be deleted)"""
|
|
118
|
+
await self._api._request("DELETE", f"/apps/{app_id}/rooms/{room_id}")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class ActorsApi:
|
|
122
|
+
"""Actors API - Manage actors in your project"""
|
|
123
|
+
|
|
124
|
+
def __init__(self, api: "NoLagApi"):
|
|
125
|
+
self._api = api
|
|
126
|
+
|
|
127
|
+
async def list(self) -> list[Actor]:
|
|
128
|
+
"""List all actors in the project"""
|
|
129
|
+
data = await self._api._request("GET", "/actors")
|
|
130
|
+
return [Actor.from_dict(item) for item in data]
|
|
131
|
+
|
|
132
|
+
async def get(self, actor_id: str) -> Actor:
|
|
133
|
+
"""Get an actor by ID"""
|
|
134
|
+
data = await self._api._request("GET", f"/actors/{actor_id}")
|
|
135
|
+
return Actor.from_dict(data)
|
|
136
|
+
|
|
137
|
+
async def create(self, data: ActorCreate) -> ActorWithToken:
|
|
138
|
+
"""
|
|
139
|
+
Create a new actor
|
|
140
|
+
|
|
141
|
+
IMPORTANT: The access token is only returned once! Save it immediately.
|
|
142
|
+
"""
|
|
143
|
+
result = await self._api._request("POST", "/actors", json=data.to_dict())
|
|
144
|
+
return ActorWithToken.from_dict(result)
|
|
145
|
+
|
|
146
|
+
async def update(self, actor_id: str, data: ActorUpdate) -> Actor:
|
|
147
|
+
"""Update an actor"""
|
|
148
|
+
result = await self._api._request("PATCH", f"/actors/{actor_id}", json=data.to_dict())
|
|
149
|
+
return Actor.from_dict(result)
|
|
150
|
+
|
|
151
|
+
async def delete(self, actor_id: str) -> None:
|
|
152
|
+
"""Delete an actor"""
|
|
153
|
+
await self._api._request("DELETE", f"/actors/{actor_id}")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class NoLagApi:
|
|
157
|
+
"""
|
|
158
|
+
NoLag REST API Client
|
|
159
|
+
|
|
160
|
+
API keys are project-scoped, so you don't need to pass organization or project IDs.
|
|
161
|
+
|
|
162
|
+
Example:
|
|
163
|
+
```python
|
|
164
|
+
from nolag import NoLagApi, AppCreate, ActorCreate
|
|
165
|
+
|
|
166
|
+
async with NoLagApi('nlg_live_xxx.secret') as api:
|
|
167
|
+
# List apps in your project
|
|
168
|
+
apps = await api.apps.list()
|
|
169
|
+
|
|
170
|
+
# Create a room
|
|
171
|
+
room = await api.rooms.create(app_id, RoomCreate(
|
|
172
|
+
name='chat-room',
|
|
173
|
+
description='General chat'
|
|
174
|
+
))
|
|
175
|
+
|
|
176
|
+
# Create an actor and get the access token
|
|
177
|
+
actor = await api.actors.create(ActorCreate(
|
|
178
|
+
name='web-client',
|
|
179
|
+
actor_type='device'
|
|
180
|
+
))
|
|
181
|
+
print('Save this token:', actor.access_token)
|
|
182
|
+
```
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
def __init__(
|
|
186
|
+
self,
|
|
187
|
+
api_key: str,
|
|
188
|
+
options: Optional[NoLagApiOptions] = None,
|
|
189
|
+
):
|
|
190
|
+
self._api_key = api_key
|
|
191
|
+
self._options = options or NoLagApiOptions()
|
|
192
|
+
self._base_url = self._options.base_url.rstrip("/")
|
|
193
|
+
self._timeout = aiohttp.ClientTimeout(total=self._options.timeout)
|
|
194
|
+
self._session: Optional[aiohttp.ClientSession] = None
|
|
195
|
+
|
|
196
|
+
# Initialize sub-APIs
|
|
197
|
+
self.apps = AppsApi(self)
|
|
198
|
+
self.rooms = RoomsApi(self)
|
|
199
|
+
self.actors = ActorsApi(self)
|
|
200
|
+
|
|
201
|
+
async def __aenter__(self) -> "NoLagApi":
|
|
202
|
+
"""Async context manager entry"""
|
|
203
|
+
self._session = aiohttp.ClientSession(timeout=self._timeout)
|
|
204
|
+
return self
|
|
205
|
+
|
|
206
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
207
|
+
"""Async context manager exit"""
|
|
208
|
+
if self._session:
|
|
209
|
+
await self._session.close()
|
|
210
|
+
self._session = None
|
|
211
|
+
|
|
212
|
+
async def close(self) -> None:
|
|
213
|
+
"""Close the API client session"""
|
|
214
|
+
if self._session:
|
|
215
|
+
await self._session.close()
|
|
216
|
+
self._session = None
|
|
217
|
+
|
|
218
|
+
async def _request(
|
|
219
|
+
self,
|
|
220
|
+
method: str,
|
|
221
|
+
path: str,
|
|
222
|
+
params: Optional[dict] = None,
|
|
223
|
+
json: Optional[dict] = None,
|
|
224
|
+
) -> Any:
|
|
225
|
+
"""Make an authenticated request to the NoLag API"""
|
|
226
|
+
if not self._session:
|
|
227
|
+
self._session = aiohttp.ClientSession(timeout=self._timeout)
|
|
228
|
+
|
|
229
|
+
url = f"{self._base_url}{path}"
|
|
230
|
+
if params:
|
|
231
|
+
url = f"{url}?{urlencode(params)}"
|
|
232
|
+
|
|
233
|
+
headers = {
|
|
234
|
+
"Authorization": f"Bearer {self._api_key}",
|
|
235
|
+
"Content-Type": "application/json",
|
|
236
|
+
**self._options.headers,
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
async with self._session.request(
|
|
241
|
+
method, url, headers=headers, json=json
|
|
242
|
+
) as response:
|
|
243
|
+
if not response.ok:
|
|
244
|
+
try:
|
|
245
|
+
error_data = await response.json()
|
|
246
|
+
raise NoLagApiError(
|
|
247
|
+
error_data.get("message", "Request failed"),
|
|
248
|
+
response.status,
|
|
249
|
+
ApiError(
|
|
250
|
+
status_code=response.status,
|
|
251
|
+
message=error_data.get("message", "Request failed"),
|
|
252
|
+
error=error_data.get("error"),
|
|
253
|
+
),
|
|
254
|
+
)
|
|
255
|
+
except aiohttp.ContentTypeError:
|
|
256
|
+
raise NoLagApiError(
|
|
257
|
+
response.reason or "Request failed",
|
|
258
|
+
response.status,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Handle 204 No Content
|
|
262
|
+
if response.status == 204:
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
return await response.json()
|
|
266
|
+
|
|
267
|
+
except aiohttp.ClientError as e:
|
|
268
|
+
raise NoLagApiError(str(e), 0, ApiError(status_code=0, message=str(e)))
|
nolag/api_types.py
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NoLag REST API Types
|
|
3
|
+
|
|
4
|
+
These types are for the project-scoped API.
|
|
5
|
+
API keys are scoped to a specific project, so organization and project IDs
|
|
6
|
+
are implicit and not needed in API calls.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any, Optional, Literal
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# ============ Common Types ============
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class PaginatedResult:
|
|
18
|
+
"""Paginated API response"""
|
|
19
|
+
data: list
|
|
20
|
+
total: int
|
|
21
|
+
page: int
|
|
22
|
+
limit: int
|
|
23
|
+
total_pages: int
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class ApiError:
|
|
28
|
+
"""API error response"""
|
|
29
|
+
status_code: int
|
|
30
|
+
message: str
|
|
31
|
+
error: Optional[str] = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ============ App Types ============
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class App:
|
|
38
|
+
"""App resource"""
|
|
39
|
+
app_id: str
|
|
40
|
+
project_id: str
|
|
41
|
+
name: str
|
|
42
|
+
slug: Optional[str] = None
|
|
43
|
+
description: Optional[str] = None
|
|
44
|
+
blueprint_id: Optional[str] = None
|
|
45
|
+
config: Optional[dict[str, Any]] = None
|
|
46
|
+
created_at: Optional[str] = None
|
|
47
|
+
updated_at: Optional[str] = None
|
|
48
|
+
deleted_at: Optional[str] = None
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def from_dict(cls, data: dict) -> "App":
|
|
52
|
+
return cls(
|
|
53
|
+
app_id=data.get("appId", ""),
|
|
54
|
+
project_id=data.get("projectId", ""),
|
|
55
|
+
name=data.get("name", ""),
|
|
56
|
+
slug=data.get("slug"),
|
|
57
|
+
description=data.get("description"),
|
|
58
|
+
blueprint_id=data.get("blueprintId"),
|
|
59
|
+
config=data.get("config"),
|
|
60
|
+
created_at=data.get("createdAt"),
|
|
61
|
+
updated_at=data.get("updatedAt"),
|
|
62
|
+
deleted_at=data.get("deletedAt"),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class AppCreate:
|
|
68
|
+
"""Create app request"""
|
|
69
|
+
name: str
|
|
70
|
+
slug: Optional[str] = None
|
|
71
|
+
description: Optional[str] = None
|
|
72
|
+
blueprint_id: Optional[str] = None
|
|
73
|
+
config: Optional[dict[str, Any]] = None
|
|
74
|
+
|
|
75
|
+
def to_dict(self) -> dict:
|
|
76
|
+
result = {"name": self.name}
|
|
77
|
+
if self.slug:
|
|
78
|
+
result["slug"] = self.slug
|
|
79
|
+
if self.description:
|
|
80
|
+
result["description"] = self.description
|
|
81
|
+
if self.blueprint_id:
|
|
82
|
+
result["blueprintId"] = self.blueprint_id
|
|
83
|
+
if self.config:
|
|
84
|
+
result["config"] = self.config
|
|
85
|
+
return result
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class AppUpdate:
|
|
90
|
+
"""Update app request"""
|
|
91
|
+
name: Optional[str] = None
|
|
92
|
+
slug: Optional[str] = None
|
|
93
|
+
description: Optional[str] = None
|
|
94
|
+
config: Optional[dict[str, Any]] = None
|
|
95
|
+
|
|
96
|
+
def to_dict(self) -> dict:
|
|
97
|
+
result = {}
|
|
98
|
+
if self.name is not None:
|
|
99
|
+
result["name"] = self.name
|
|
100
|
+
if self.slug is not None:
|
|
101
|
+
result["slug"] = self.slug
|
|
102
|
+
if self.description is not None:
|
|
103
|
+
result["description"] = self.description
|
|
104
|
+
if self.config is not None:
|
|
105
|
+
result["config"] = self.config
|
|
106
|
+
return result
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ============ Room Types ============
|
|
110
|
+
|
|
111
|
+
RoomType = Literal["static", "dynamic"]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass
|
|
115
|
+
class Room:
|
|
116
|
+
"""Room resource"""
|
|
117
|
+
room_id: str
|
|
118
|
+
app_id: str
|
|
119
|
+
name: str
|
|
120
|
+
slug: str
|
|
121
|
+
description: Optional[str] = None
|
|
122
|
+
room_type: Optional[RoomType] = None
|
|
123
|
+
is_enabled: bool = True
|
|
124
|
+
topics: Optional[list[str]] = None
|
|
125
|
+
config: Optional[dict[str, Any]] = None
|
|
126
|
+
created_at: Optional[str] = None
|
|
127
|
+
updated_at: Optional[str] = None
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def from_dict(cls, data: dict) -> "Room":
|
|
131
|
+
return cls(
|
|
132
|
+
room_id=data.get("roomId", ""),
|
|
133
|
+
app_id=data.get("appId", ""),
|
|
134
|
+
name=data.get("name", ""),
|
|
135
|
+
slug=data.get("slug", ""),
|
|
136
|
+
description=data.get("description"),
|
|
137
|
+
room_type=data.get("roomType"),
|
|
138
|
+
is_enabled=data.get("isEnabled", True),
|
|
139
|
+
topics=data.get("topics"),
|
|
140
|
+
config=data.get("config"),
|
|
141
|
+
created_at=data.get("createdAt"),
|
|
142
|
+
updated_at=data.get("updatedAt"),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@dataclass
|
|
147
|
+
class RoomCreate:
|
|
148
|
+
"""Create room request"""
|
|
149
|
+
name: str
|
|
150
|
+
slug: Optional[str] = None
|
|
151
|
+
description: Optional[str] = None
|
|
152
|
+
topics: Optional[list[str]] = None
|
|
153
|
+
config: Optional[dict[str, Any]] = None
|
|
154
|
+
|
|
155
|
+
def to_dict(self) -> dict:
|
|
156
|
+
result = {"name": self.name}
|
|
157
|
+
if self.slug:
|
|
158
|
+
result["slug"] = self.slug
|
|
159
|
+
if self.description:
|
|
160
|
+
result["description"] = self.description
|
|
161
|
+
if self.topics:
|
|
162
|
+
result["topics"] = self.topics
|
|
163
|
+
if self.config:
|
|
164
|
+
result["config"] = self.config
|
|
165
|
+
return result
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dataclass
|
|
169
|
+
class RoomUpdate:
|
|
170
|
+
"""Update room request"""
|
|
171
|
+
name: Optional[str] = None
|
|
172
|
+
description: Optional[str] = None
|
|
173
|
+
is_enabled: Optional[bool] = None
|
|
174
|
+
topics: Optional[list[str]] = None
|
|
175
|
+
config: Optional[dict[str, Any]] = None
|
|
176
|
+
|
|
177
|
+
def to_dict(self) -> dict:
|
|
178
|
+
result = {}
|
|
179
|
+
if self.name is not None:
|
|
180
|
+
result["name"] = self.name
|
|
181
|
+
if self.description is not None:
|
|
182
|
+
result["description"] = self.description
|
|
183
|
+
if self.is_enabled is not None:
|
|
184
|
+
result["isEnabled"] = self.is_enabled
|
|
185
|
+
if self.topics is not None:
|
|
186
|
+
result["topics"] = self.topics
|
|
187
|
+
if self.config is not None:
|
|
188
|
+
result["config"] = self.config
|
|
189
|
+
return result
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ============ Actor Types ============
|
|
193
|
+
|
|
194
|
+
ActorTokenType = Literal["device", "user", "server"]
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@dataclass
|
|
198
|
+
class Actor:
|
|
199
|
+
"""Actor resource"""
|
|
200
|
+
actor_token_id: str
|
|
201
|
+
project_id: str
|
|
202
|
+
name: str
|
|
203
|
+
actor_type: ActorTokenType
|
|
204
|
+
description: Optional[str] = None
|
|
205
|
+
external_id: Optional[str] = None
|
|
206
|
+
metadata: Optional[dict[str, Any]] = None
|
|
207
|
+
is_active: bool = True
|
|
208
|
+
last_connected_at: Optional[str] = None
|
|
209
|
+
created_at: Optional[str] = None
|
|
210
|
+
updated_at: Optional[str] = None
|
|
211
|
+
|
|
212
|
+
@classmethod
|
|
213
|
+
def from_dict(cls, data: dict) -> "Actor":
|
|
214
|
+
return cls(
|
|
215
|
+
actor_token_id=data.get("actorTokenId", ""),
|
|
216
|
+
project_id=data.get("projectId", ""),
|
|
217
|
+
name=data.get("name", ""),
|
|
218
|
+
actor_type=data.get("actorType", "device"),
|
|
219
|
+
description=data.get("description"),
|
|
220
|
+
external_id=data.get("externalId"),
|
|
221
|
+
metadata=data.get("metadata"),
|
|
222
|
+
is_active=data.get("isActive", True),
|
|
223
|
+
last_connected_at=data.get("lastConnectedAt"),
|
|
224
|
+
created_at=data.get("createdAt"),
|
|
225
|
+
updated_at=data.get("updatedAt"),
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@dataclass
|
|
230
|
+
class ActorWithToken(Actor):
|
|
231
|
+
"""Actor with access token (returned on creation only)"""
|
|
232
|
+
access_token: str = ""
|
|
233
|
+
|
|
234
|
+
@classmethod
|
|
235
|
+
def from_dict(cls, data: dict) -> "ActorWithToken":
|
|
236
|
+
return cls(
|
|
237
|
+
actor_token_id=data.get("actorTokenId", ""),
|
|
238
|
+
project_id=data.get("projectId", ""),
|
|
239
|
+
name=data.get("name", ""),
|
|
240
|
+
actor_type=data.get("actorType", "device"),
|
|
241
|
+
description=data.get("description"),
|
|
242
|
+
external_id=data.get("externalId"),
|
|
243
|
+
metadata=data.get("metadata"),
|
|
244
|
+
is_active=data.get("isActive", True),
|
|
245
|
+
last_connected_at=data.get("lastConnectedAt"),
|
|
246
|
+
created_at=data.get("createdAt"),
|
|
247
|
+
updated_at=data.get("updatedAt"),
|
|
248
|
+
access_token=data.get("accessToken", ""),
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@dataclass
|
|
253
|
+
class ActorCreate:
|
|
254
|
+
"""Create actor request"""
|
|
255
|
+
name: str
|
|
256
|
+
actor_type: ActorTokenType
|
|
257
|
+
description: Optional[str] = None
|
|
258
|
+
external_id: Optional[str] = None
|
|
259
|
+
metadata: Optional[dict[str, Any]] = None
|
|
260
|
+
|
|
261
|
+
def to_dict(self) -> dict:
|
|
262
|
+
result = {
|
|
263
|
+
"name": self.name,
|
|
264
|
+
"actorType": self.actor_type,
|
|
265
|
+
}
|
|
266
|
+
if self.description:
|
|
267
|
+
result["description"] = self.description
|
|
268
|
+
if self.external_id:
|
|
269
|
+
result["externalId"] = self.external_id
|
|
270
|
+
if self.metadata:
|
|
271
|
+
result["metadata"] = self.metadata
|
|
272
|
+
return result
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@dataclass
|
|
276
|
+
class ActorUpdate:
|
|
277
|
+
"""Update actor request"""
|
|
278
|
+
name: Optional[str] = None
|
|
279
|
+
description: Optional[str] = None
|
|
280
|
+
external_id: Optional[str] = None
|
|
281
|
+
metadata: Optional[dict[str, Any]] = None
|
|
282
|
+
is_active: Optional[bool] = None
|
|
283
|
+
|
|
284
|
+
def to_dict(self) -> dict:
|
|
285
|
+
result = {}
|
|
286
|
+
if self.name is not None:
|
|
287
|
+
result["name"] = self.name
|
|
288
|
+
if self.description is not None:
|
|
289
|
+
result["description"] = self.description
|
|
290
|
+
if self.external_id is not None:
|
|
291
|
+
result["externalId"] = self.external_id
|
|
292
|
+
if self.metadata is not None:
|
|
293
|
+
result["metadata"] = self.metadata
|
|
294
|
+
if self.is_active is not None:
|
|
295
|
+
result["isActive"] = self.is_active
|
|
296
|
+
return result
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# ============ API Options ============
|
|
300
|
+
|
|
301
|
+
@dataclass
|
|
302
|
+
class NoLagApiOptions:
|
|
303
|
+
"""Options for the NoLag API client"""
|
|
304
|
+
base_url: str = "https://api.nolag.app/v1"
|
|
305
|
+
timeout: float = 30.0
|
|
306
|
+
headers: dict[str, str] = field(default_factory=dict)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@dataclass
|
|
310
|
+
class ListOptions:
|
|
311
|
+
"""Options for list endpoints"""
|
|
312
|
+
page: Optional[int] = None
|
|
313
|
+
limit: Optional[int] = None
|
|
314
|
+
sort_by: Optional[str] = None
|
|
315
|
+
sort_order: Optional[Literal["asc", "desc"]] = None
|
|
316
|
+
search: Optional[str] = None
|
|
317
|
+
|
|
318
|
+
def to_params(self) -> dict:
|
|
319
|
+
params = {}
|
|
320
|
+
if self.page is not None:
|
|
321
|
+
params["page"] = self.page
|
|
322
|
+
if self.limit is not None:
|
|
323
|
+
params["limit"] = self.limit
|
|
324
|
+
if self.sort_by is not None:
|
|
325
|
+
params["sortBy"] = self.sort_by
|
|
326
|
+
if self.sort_order is not None:
|
|
327
|
+
params["sortOrder"] = self.sort_order
|
|
328
|
+
if self.search is not None:
|
|
329
|
+
params["search"] = self.search
|
|
330
|
+
return params
|