waveium 0.2.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.
- waveium/__init__.py +204 -0
- waveium/_config.py +88 -0
- waveium/_http.py +362 -0
- waveium/_sse.py +497 -0
- waveium/async_client.py +92 -0
- waveium/client.py +90 -0
- waveium/exceptions.py +131 -0
- waveium/models/__init__.py +224 -0
- waveium/models/auth.py +58 -0
- waveium/models/common.py +130 -0
- waveium/models/environments.py +247 -0
- waveium/models/events.py +132 -0
- waveium/models/executions.py +145 -0
- waveium/models/files.py +158 -0
- waveium/models/organizations.py +51 -0
- waveium/models/projects.py +177 -0
- waveium/models/roles.py +48 -0
- waveium/models/scenes.py +222 -0
- waveium/models/shares.py +51 -0
- waveium/models/teams.py +90 -0
- waveium/models/users.py +88 -0
- waveium/resources/__init__.py +41 -0
- waveium/resources/_base.py +30 -0
- waveium/resources/_file_ops.py +371 -0
- waveium/resources/_share_ops.py +175 -0
- waveium/resources/_stream_ops.py +318 -0
- waveium/resources/_upload_ops.py +286 -0
- waveium/resources/auth.py +157 -0
- waveium/resources/environments.py +1047 -0
- waveium/resources/executions.py +1142 -0
- waveium/resources/organizations.py +281 -0
- waveium/resources/projects.py +559 -0
- waveium/resources/roles.py +104 -0
- waveium/resources/scenes.py +1314 -0
- waveium/resources/teams.py +351 -0
- waveium/resources/users.py +448 -0
- waveium-0.2.0.dist-info/METADATA +426 -0
- waveium-0.2.0.dist-info/RECORD +40 -0
- waveium-0.2.0.dist-info/WHEEL +4 -0
- waveium-0.2.0.dist-info/licenses/LICENSE +21 -0
waveium/_sse.py
ADDED
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
"""SSE (Server-Sent Events) transport layer for the Waveium SDK.
|
|
2
|
+
|
|
3
|
+
Implements the W3C SSE specification with auto-reconnection,
|
|
4
|
+
keepalive handling, and configurable backoff. Used internally
|
|
5
|
+
by StreamOperations to provide typed event streams.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import time
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import (
|
|
12
|
+
AsyncIterator,
|
|
13
|
+
Dict,
|
|
14
|
+
Iterator,
|
|
15
|
+
List,
|
|
16
|
+
Optional,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
import httpx
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger("waveium")
|
|
22
|
+
|
|
23
|
+
from .exceptions import (
|
|
24
|
+
AuthenticationError,
|
|
25
|
+
AuthorizationError,
|
|
26
|
+
NotFoundError,
|
|
27
|
+
StreamReconnectError,
|
|
28
|
+
StreamTimeoutError,
|
|
29
|
+
ValidationError,
|
|
30
|
+
)
|
|
31
|
+
from ._http import _handle_error_response
|
|
32
|
+
|
|
33
|
+
# SSE-specific timeout: long read (server keepalive every 15s,
|
|
34
|
+
# deadline 330s), normal connect/write/pool.
|
|
35
|
+
SSE_TIMEOUT = httpx.Timeout(
|
|
36
|
+
connect=10.0,
|
|
37
|
+
read=360.0,
|
|
38
|
+
write=10.0,
|
|
39
|
+
pool=10.0,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# HTTP status codes that should not be retried
|
|
43
|
+
_NON_RETRYABLE_STATUSES = {400, 401, 403, 404}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class RawSSEEvent:
|
|
48
|
+
"""A single parsed SSE event from the wire."""
|
|
49
|
+
|
|
50
|
+
event: str = "message"
|
|
51
|
+
data: str = ""
|
|
52
|
+
id: Optional[str] = None
|
|
53
|
+
retry: Optional[int] = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _parse_sse_lines(lines: Iterator[str]) -> Iterator[RawSSEEvent]:
|
|
57
|
+
"""Parse SSE lines per the W3C EventSource specification.
|
|
58
|
+
|
|
59
|
+
Handles event, data, id, retry fields, multi-line data
|
|
60
|
+
concatenation, and comment (keepalive) filtering.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
lines: Iterator of text lines from the SSE stream.
|
|
64
|
+
|
|
65
|
+
Yields:
|
|
66
|
+
Parsed RawSSEEvent instances.
|
|
67
|
+
"""
|
|
68
|
+
current_event = "message"
|
|
69
|
+
current_data: List[str] = []
|
|
70
|
+
current_id: Optional[str] = None
|
|
71
|
+
current_retry: Optional[int] = None
|
|
72
|
+
|
|
73
|
+
for raw_line in lines:
|
|
74
|
+
line = raw_line.rstrip("\n").rstrip("\r")
|
|
75
|
+
|
|
76
|
+
# Empty line: dispatch event if data is non-empty
|
|
77
|
+
if not line:
|
|
78
|
+
if current_data:
|
|
79
|
+
yield RawSSEEvent(
|
|
80
|
+
event=current_event,
|
|
81
|
+
data="\n".join(current_data),
|
|
82
|
+
id=current_id,
|
|
83
|
+
retry=current_retry,
|
|
84
|
+
)
|
|
85
|
+
# Reset for next event
|
|
86
|
+
current_event = "message"
|
|
87
|
+
current_data = []
|
|
88
|
+
current_id = None
|
|
89
|
+
current_retry = None
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
# Comment line (keepalive or debug)
|
|
93
|
+
if line.startswith(":"):
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
# Field parsing
|
|
97
|
+
if ":" in line:
|
|
98
|
+
field_name, _, value = line.partition(":")
|
|
99
|
+
# Strip single leading space from value per spec
|
|
100
|
+
if value.startswith(" "):
|
|
101
|
+
value = value[1:]
|
|
102
|
+
else:
|
|
103
|
+
field_name = line
|
|
104
|
+
value = ""
|
|
105
|
+
|
|
106
|
+
if field_name == "event":
|
|
107
|
+
current_event = value
|
|
108
|
+
elif field_name == "data":
|
|
109
|
+
current_data.append(value)
|
|
110
|
+
elif field_name == "id":
|
|
111
|
+
current_id = value
|
|
112
|
+
elif field_name == "retry":
|
|
113
|
+
try:
|
|
114
|
+
current_retry = int(value)
|
|
115
|
+
except ValueError:
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
async def _async_parse_sse_lines(
|
|
120
|
+
lines: AsyncIterator[str],
|
|
121
|
+
) -> AsyncIterator[RawSSEEvent]:
|
|
122
|
+
"""Async version of _parse_sse_lines.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
lines: Async iterator of text lines from the SSE stream.
|
|
126
|
+
|
|
127
|
+
Yields:
|
|
128
|
+
Parsed RawSSEEvent instances.
|
|
129
|
+
"""
|
|
130
|
+
current_event = "message"
|
|
131
|
+
current_data: List[str] = []
|
|
132
|
+
current_id: Optional[str] = None
|
|
133
|
+
current_retry: Optional[int] = None
|
|
134
|
+
|
|
135
|
+
async for raw_line in lines:
|
|
136
|
+
line = raw_line.rstrip("\n").rstrip("\r")
|
|
137
|
+
|
|
138
|
+
if not line:
|
|
139
|
+
if current_data:
|
|
140
|
+
yield RawSSEEvent(
|
|
141
|
+
event=current_event,
|
|
142
|
+
data="\n".join(current_data),
|
|
143
|
+
id=current_id,
|
|
144
|
+
retry=current_retry,
|
|
145
|
+
)
|
|
146
|
+
current_event = "message"
|
|
147
|
+
current_data = []
|
|
148
|
+
current_id = None
|
|
149
|
+
current_retry = None
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
if line.startswith(":"):
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
if ":" in line:
|
|
156
|
+
field_name, _, value = line.partition(":")
|
|
157
|
+
if value.startswith(" "):
|
|
158
|
+
value = value[1:]
|
|
159
|
+
else:
|
|
160
|
+
field_name = line
|
|
161
|
+
value = ""
|
|
162
|
+
|
|
163
|
+
if field_name == "event":
|
|
164
|
+
current_event = value
|
|
165
|
+
elif field_name == "data":
|
|
166
|
+
current_data.append(value)
|
|
167
|
+
elif field_name == "id":
|
|
168
|
+
current_id = value
|
|
169
|
+
elif field_name == "retry":
|
|
170
|
+
try:
|
|
171
|
+
current_retry = int(value)
|
|
172
|
+
except ValueError:
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@dataclass
|
|
177
|
+
class SSEConnection:
|
|
178
|
+
"""Synchronous SSE connection with auto-reconnection.
|
|
179
|
+
|
|
180
|
+
Handles the server's 5-minute connection timeout by
|
|
181
|
+
automatically reconnecting when a timeout event is received.
|
|
182
|
+
Network errors trigger exponential backoff.
|
|
183
|
+
|
|
184
|
+
Usage:
|
|
185
|
+
conn = SSEConnection(client, url, headers=headers)
|
|
186
|
+
with conn:
|
|
187
|
+
for event in conn:
|
|
188
|
+
process(event)
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
client: httpx.Client
|
|
192
|
+
url: str
|
|
193
|
+
headers: Dict[str, str] = field(default_factory=dict)
|
|
194
|
+
max_reconnects: int = 50
|
|
195
|
+
max_total_seconds: float = 7200.0
|
|
196
|
+
initial_retry_ms: int = 1000
|
|
197
|
+
max_retry_ms: int = 30000
|
|
198
|
+
|
|
199
|
+
_last_event_id: Optional[str] = field(default=None, init=False, repr=False)
|
|
200
|
+
_server_retry_ms: Optional[int] = field(
|
|
201
|
+
default=None, init=False, repr=False
|
|
202
|
+
)
|
|
203
|
+
_closed: bool = field(default=False, init=False, repr=False)
|
|
204
|
+
|
|
205
|
+
def __iter__(self) -> Iterator[RawSSEEvent]:
|
|
206
|
+
start_time = time.monotonic()
|
|
207
|
+
reconnect_count = 0
|
|
208
|
+
backoff_ms = self.initial_retry_ms
|
|
209
|
+
logger.debug("SSE connecting to %s", self.url)
|
|
210
|
+
|
|
211
|
+
while not self._closed:
|
|
212
|
+
# Check total time limit
|
|
213
|
+
elapsed = time.monotonic() - start_time
|
|
214
|
+
if elapsed >= self.max_total_seconds:
|
|
215
|
+
raise StreamTimeoutError(
|
|
216
|
+
f"SSE stream exceeded maximum duration "
|
|
217
|
+
f"of {self.max_total_seconds}s"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Check reconnect limit
|
|
221
|
+
if reconnect_count > self.max_reconnects:
|
|
222
|
+
raise StreamReconnectError(
|
|
223
|
+
f"SSE stream exceeded maximum reconnect "
|
|
224
|
+
f"count of {self.max_reconnects}"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Build request headers
|
|
228
|
+
request_headers = {
|
|
229
|
+
**self.headers,
|
|
230
|
+
"Accept": "text/event-stream",
|
|
231
|
+
"Cache-Control": "no-cache",
|
|
232
|
+
}
|
|
233
|
+
if self._last_event_id is not None:
|
|
234
|
+
request_headers["Last-Event-ID"] = self._last_event_id
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
with self.client.stream(
|
|
238
|
+
"GET",
|
|
239
|
+
self.url,
|
|
240
|
+
headers=request_headers,
|
|
241
|
+
timeout=SSE_TIMEOUT,
|
|
242
|
+
) as response:
|
|
243
|
+
# Handle non-success status codes
|
|
244
|
+
if response.status_code >= 400:
|
|
245
|
+
if response.status_code in _NON_RETRYABLE_STATUSES:
|
|
246
|
+
# Read body for error details
|
|
247
|
+
response.read()
|
|
248
|
+
raise _handle_error_response(response)
|
|
249
|
+
# 5xx: will retry after backoff
|
|
250
|
+
logger.warning(
|
|
251
|
+
"SSE server error %d, retrying in %dms",
|
|
252
|
+
response.status_code,
|
|
253
|
+
backoff_ms,
|
|
254
|
+
)
|
|
255
|
+
reconnect_count += 1
|
|
256
|
+
self._backoff_sleep(backoff_ms)
|
|
257
|
+
backoff_ms = min(backoff_ms * 2, self.max_retry_ms)
|
|
258
|
+
continue
|
|
259
|
+
|
|
260
|
+
# Reset backoff on successful connection
|
|
261
|
+
backoff_ms = self.initial_retry_ms
|
|
262
|
+
is_timeout_reconnect = False
|
|
263
|
+
logger.debug("SSE connected to %s", self.url)
|
|
264
|
+
|
|
265
|
+
for event in _parse_sse_lines(response.iter_lines()):
|
|
266
|
+
if self._closed:
|
|
267
|
+
return
|
|
268
|
+
|
|
269
|
+
# Track last event ID for reconnection
|
|
270
|
+
if event.id is not None:
|
|
271
|
+
self._last_event_id = event.id
|
|
272
|
+
|
|
273
|
+
# Track server-suggested retry interval
|
|
274
|
+
if event.retry is not None:
|
|
275
|
+
self._server_retry_ms = event.retry
|
|
276
|
+
|
|
277
|
+
# Timeout event: reconnect immediately
|
|
278
|
+
if event.event == "timeout":
|
|
279
|
+
logger.debug("SSE server timeout, reconnecting")
|
|
280
|
+
is_timeout_reconnect = True
|
|
281
|
+
reconnect_count += 1
|
|
282
|
+
break
|
|
283
|
+
|
|
284
|
+
yield event
|
|
285
|
+
|
|
286
|
+
# If we exited the loop normally (stream ended)
|
|
287
|
+
# without a timeout event, reconnect with backoff
|
|
288
|
+
if not is_timeout_reconnect:
|
|
289
|
+
if self._closed:
|
|
290
|
+
return
|
|
291
|
+
reconnect_count += 1
|
|
292
|
+
retry_ms = (
|
|
293
|
+
self._server_retry_ms
|
|
294
|
+
if self._server_retry_ms is not None
|
|
295
|
+
else backoff_ms
|
|
296
|
+
)
|
|
297
|
+
logger.debug(
|
|
298
|
+
"SSE stream ended, reconnecting in %dms",
|
|
299
|
+
retry_ms,
|
|
300
|
+
)
|
|
301
|
+
self._backoff_sleep(retry_ms)
|
|
302
|
+
backoff_ms = min(backoff_ms * 2, self.max_retry_ms)
|
|
303
|
+
|
|
304
|
+
except (
|
|
305
|
+
httpx.RequestError,
|
|
306
|
+
httpx.StreamError,
|
|
307
|
+
) as e:
|
|
308
|
+
if self._closed:
|
|
309
|
+
return
|
|
310
|
+
reconnect_count += 1
|
|
311
|
+
if reconnect_count > self.max_reconnects:
|
|
312
|
+
raise StreamReconnectError(
|
|
313
|
+
f"SSE stream exceeded maximum reconnect "
|
|
314
|
+
f"count of {self.max_reconnects}"
|
|
315
|
+
) from e
|
|
316
|
+
retry_ms = (
|
|
317
|
+
self._server_retry_ms
|
|
318
|
+
if self._server_retry_ms is not None
|
|
319
|
+
else backoff_ms
|
|
320
|
+
)
|
|
321
|
+
logger.warning(
|
|
322
|
+
"SSE connection error: %s, retrying in %dms",
|
|
323
|
+
e,
|
|
324
|
+
retry_ms,
|
|
325
|
+
)
|
|
326
|
+
self._backoff_sleep(retry_ms)
|
|
327
|
+
backoff_ms = min(backoff_ms * 2, self.max_retry_ms)
|
|
328
|
+
|
|
329
|
+
except (
|
|
330
|
+
AuthenticationError,
|
|
331
|
+
AuthorizationError,
|
|
332
|
+
NotFoundError,
|
|
333
|
+
ValidationError,
|
|
334
|
+
):
|
|
335
|
+
# Non-retryable errors: propagate immediately
|
|
336
|
+
raise
|
|
337
|
+
|
|
338
|
+
def _backoff_sleep(self, ms: int) -> None:
|
|
339
|
+
"""Sleep for backoff duration, respecting close signal."""
|
|
340
|
+
time.sleep(ms / 1000.0)
|
|
341
|
+
|
|
342
|
+
def close(self) -> None:
|
|
343
|
+
"""Signal the connection to stop iterating."""
|
|
344
|
+
self._closed = True
|
|
345
|
+
|
|
346
|
+
def __enter__(self) -> "SSEConnection":
|
|
347
|
+
return self
|
|
348
|
+
|
|
349
|
+
def __exit__(self, *args: object) -> None:
|
|
350
|
+
self.close()
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
@dataclass
|
|
354
|
+
class AsyncSSEConnection:
|
|
355
|
+
"""Asynchronous SSE connection with auto-reconnection.
|
|
356
|
+
|
|
357
|
+
Async counterpart of SSEConnection. Handles the server's
|
|
358
|
+
5-minute connection timeout and network errors with
|
|
359
|
+
automatic reconnection.
|
|
360
|
+
|
|
361
|
+
Usage:
|
|
362
|
+
conn = AsyncSSEConnection(client, url, headers=headers)
|
|
363
|
+
async with conn:
|
|
364
|
+
async for event in conn:
|
|
365
|
+
await process(event)
|
|
366
|
+
"""
|
|
367
|
+
|
|
368
|
+
client: httpx.AsyncClient
|
|
369
|
+
url: str
|
|
370
|
+
headers: Dict[str, str] = field(default_factory=dict)
|
|
371
|
+
max_reconnects: int = 50
|
|
372
|
+
max_total_seconds: float = 7200.0
|
|
373
|
+
initial_retry_ms: int = 1000
|
|
374
|
+
max_retry_ms: int = 30000
|
|
375
|
+
|
|
376
|
+
_last_event_id: Optional[str] = field(default=None, init=False, repr=False)
|
|
377
|
+
_server_retry_ms: Optional[int] = field(
|
|
378
|
+
default=None, init=False, repr=False
|
|
379
|
+
)
|
|
380
|
+
_closed: bool = field(default=False, init=False, repr=False)
|
|
381
|
+
|
|
382
|
+
async def __aiter__(self) -> AsyncIterator[RawSSEEvent]:
|
|
383
|
+
import asyncio
|
|
384
|
+
|
|
385
|
+
start_time = time.monotonic()
|
|
386
|
+
reconnect_count = 0
|
|
387
|
+
backoff_ms = self.initial_retry_ms
|
|
388
|
+
|
|
389
|
+
while not self._closed:
|
|
390
|
+
elapsed = time.monotonic() - start_time
|
|
391
|
+
if elapsed >= self.max_total_seconds:
|
|
392
|
+
raise StreamTimeoutError(
|
|
393
|
+
f"SSE stream exceeded maximum duration "
|
|
394
|
+
f"of {self.max_total_seconds}s"
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
if reconnect_count > self.max_reconnects:
|
|
398
|
+
raise StreamReconnectError(
|
|
399
|
+
f"SSE stream exceeded maximum reconnect "
|
|
400
|
+
f"count of {self.max_reconnects}"
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
request_headers = {
|
|
404
|
+
**self.headers,
|
|
405
|
+
"Accept": "text/event-stream",
|
|
406
|
+
"Cache-Control": "no-cache",
|
|
407
|
+
}
|
|
408
|
+
if self._last_event_id is not None:
|
|
409
|
+
request_headers["Last-Event-ID"] = self._last_event_id
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
async with self.client.stream(
|
|
413
|
+
"GET",
|
|
414
|
+
self.url,
|
|
415
|
+
headers=request_headers,
|
|
416
|
+
timeout=SSE_TIMEOUT,
|
|
417
|
+
) as response:
|
|
418
|
+
if response.status_code >= 400:
|
|
419
|
+
if response.status_code in _NON_RETRYABLE_STATUSES:
|
|
420
|
+
await response.aread()
|
|
421
|
+
raise _handle_error_response(response)
|
|
422
|
+
reconnect_count += 1
|
|
423
|
+
await asyncio.sleep(backoff_ms / 1000.0)
|
|
424
|
+
backoff_ms = min(backoff_ms * 2, self.max_retry_ms)
|
|
425
|
+
continue
|
|
426
|
+
|
|
427
|
+
backoff_ms = self.initial_retry_ms
|
|
428
|
+
is_timeout_reconnect = False
|
|
429
|
+
|
|
430
|
+
async for event in _async_parse_sse_lines(
|
|
431
|
+
response.aiter_lines()
|
|
432
|
+
):
|
|
433
|
+
if self._closed:
|
|
434
|
+
return
|
|
435
|
+
|
|
436
|
+
if event.id is not None:
|
|
437
|
+
self._last_event_id = event.id
|
|
438
|
+
|
|
439
|
+
if event.retry is not None:
|
|
440
|
+
self._server_retry_ms = event.retry
|
|
441
|
+
|
|
442
|
+
if event.event == "timeout":
|
|
443
|
+
is_timeout_reconnect = True
|
|
444
|
+
reconnect_count += 1
|
|
445
|
+
break
|
|
446
|
+
|
|
447
|
+
yield event
|
|
448
|
+
|
|
449
|
+
if not is_timeout_reconnect:
|
|
450
|
+
if self._closed:
|
|
451
|
+
return
|
|
452
|
+
reconnect_count += 1
|
|
453
|
+
retry_ms = (
|
|
454
|
+
self._server_retry_ms
|
|
455
|
+
if self._server_retry_ms is not None
|
|
456
|
+
else backoff_ms
|
|
457
|
+
)
|
|
458
|
+
await asyncio.sleep(retry_ms / 1000.0)
|
|
459
|
+
backoff_ms = min(backoff_ms * 2, self.max_retry_ms)
|
|
460
|
+
|
|
461
|
+
except (
|
|
462
|
+
httpx.RequestError,
|
|
463
|
+
httpx.StreamError,
|
|
464
|
+
) as e:
|
|
465
|
+
if self._closed:
|
|
466
|
+
return
|
|
467
|
+
reconnect_count += 1
|
|
468
|
+
if reconnect_count > self.max_reconnects:
|
|
469
|
+
raise StreamReconnectError(
|
|
470
|
+
f"SSE stream exceeded maximum reconnect "
|
|
471
|
+
f"count of {self.max_reconnects}"
|
|
472
|
+
) from e
|
|
473
|
+
retry_ms = (
|
|
474
|
+
self._server_retry_ms
|
|
475
|
+
if self._server_retry_ms is not None
|
|
476
|
+
else backoff_ms
|
|
477
|
+
)
|
|
478
|
+
await asyncio.sleep(retry_ms / 1000.0)
|
|
479
|
+
backoff_ms = min(backoff_ms * 2, self.max_retry_ms)
|
|
480
|
+
|
|
481
|
+
except (
|
|
482
|
+
AuthenticationError,
|
|
483
|
+
AuthorizationError,
|
|
484
|
+
NotFoundError,
|
|
485
|
+
ValidationError,
|
|
486
|
+
):
|
|
487
|
+
raise
|
|
488
|
+
|
|
489
|
+
async def close(self) -> None:
|
|
490
|
+
"""Signal the connection to stop iterating."""
|
|
491
|
+
self._closed = True
|
|
492
|
+
|
|
493
|
+
async def __aenter__(self) -> "AsyncSSEConnection":
|
|
494
|
+
return self
|
|
495
|
+
|
|
496
|
+
async def __aexit__(self, *args: object) -> None:
|
|
497
|
+
await self.close()
|
waveium/async_client.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Asynchronous Waveium API client."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from ._config import Config
|
|
6
|
+
from ._http import AsyncHTTPClient
|
|
7
|
+
from .resources import (
|
|
8
|
+
AsyncAuthResource,
|
|
9
|
+
AsyncEnvironmentsResource,
|
|
10
|
+
AsyncExecutionsResource,
|
|
11
|
+
AsyncOrganizationsResource,
|
|
12
|
+
AsyncProjectsResource,
|
|
13
|
+
AsyncRolesResource,
|
|
14
|
+
AsyncScenesResource,
|
|
15
|
+
AsyncTeamsResource,
|
|
16
|
+
AsyncUsersResource,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AsyncClient:
|
|
21
|
+
"""Asynchronous Waveium API client.
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
>>> import waveium
|
|
25
|
+
>>> async with waveium.AsyncClient(
|
|
26
|
+
... api_key="wvx_..."
|
|
27
|
+
... ) as client:
|
|
28
|
+
... resp = await client.environments.create(
|
|
29
|
+
... name="My Environment"
|
|
30
|
+
... )
|
|
31
|
+
... env = resp.environment
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
api_key: Optional[str] = None,
|
|
37
|
+
base_url: Optional[str] = None,
|
|
38
|
+
timeout: Optional[float] = None,
|
|
39
|
+
max_retries: Optional[int] = None,
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Initialize async Waveium client.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
api_key: API key for authentication. If not
|
|
45
|
+
provided, will try to load from
|
|
46
|
+
WAVEIUM_API_KEY environment variable.
|
|
47
|
+
base_url: Base URL for the Waveium API.
|
|
48
|
+
Defaults to production URL.
|
|
49
|
+
timeout: Request timeout in seconds.
|
|
50
|
+
Defaults to 60.0.
|
|
51
|
+
max_retries: Maximum number of retries for
|
|
52
|
+
failed requests. Defaults to 3.
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
ConfigurationError: If configuration is invalid.
|
|
56
|
+
"""
|
|
57
|
+
self._config = Config(
|
|
58
|
+
api_key=api_key,
|
|
59
|
+
base_url=base_url,
|
|
60
|
+
timeout=timeout,
|
|
61
|
+
max_retries=max_retries,
|
|
62
|
+
)
|
|
63
|
+
self._config.validate()
|
|
64
|
+
|
|
65
|
+
self._http = AsyncHTTPClient(self._config)
|
|
66
|
+
|
|
67
|
+
# Initialize resources
|
|
68
|
+
self.auth = AsyncAuthResource(self._http)
|
|
69
|
+
self.environments = AsyncEnvironmentsResource(self._http)
|
|
70
|
+
self.scenes = AsyncScenesResource(self._http)
|
|
71
|
+
self.executions = AsyncExecutionsResource(self._http)
|
|
72
|
+
self.users = AsyncUsersResource(self._http)
|
|
73
|
+
self.organizations = AsyncOrganizationsResource(self._http)
|
|
74
|
+
self.roles = AsyncRolesResource(self._http)
|
|
75
|
+
self.teams = AsyncTeamsResource(self._http)
|
|
76
|
+
self.projects = AsyncProjectsResource(self._http)
|
|
77
|
+
|
|
78
|
+
async def close(self) -> None:
|
|
79
|
+
"""Close the HTTP client and release resources."""
|
|
80
|
+
await self._http.close()
|
|
81
|
+
|
|
82
|
+
async def __aenter__(self) -> "AsyncClient":
|
|
83
|
+
"""Enter async context manager."""
|
|
84
|
+
return self
|
|
85
|
+
|
|
86
|
+
async def __aexit__(self, *args: object) -> None:
|
|
87
|
+
"""Exit async context manager."""
|
|
88
|
+
await self.close()
|
|
89
|
+
|
|
90
|
+
def __repr__(self) -> str:
|
|
91
|
+
"""Return string representation of client."""
|
|
92
|
+
return f"<Waveium AsyncClient base_url=" f"{self._config.base_url}>"
|
waveium/client.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Synchronous Waveium API client."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from ._config import Config
|
|
6
|
+
from ._http import HTTPClient
|
|
7
|
+
from .resources import (
|
|
8
|
+
AuthResource,
|
|
9
|
+
EnvironmentsResource,
|
|
10
|
+
ExecutionsResource,
|
|
11
|
+
OrganizationsResource,
|
|
12
|
+
ProjectsResource,
|
|
13
|
+
RolesResource,
|
|
14
|
+
ScenesResource,
|
|
15
|
+
TeamsResource,
|
|
16
|
+
UsersResource,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Client:
|
|
21
|
+
"""Synchronous Waveium API client.
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
>>> import waveium
|
|
25
|
+
>>> client = waveium.Client(api_key="wvx_...")
|
|
26
|
+
>>> resp = client.environments.create(
|
|
27
|
+
... name="My Environment"
|
|
28
|
+
... )
|
|
29
|
+
>>> env = resp.environment
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
api_key: Optional[str] = None,
|
|
35
|
+
base_url: Optional[str] = None,
|
|
36
|
+
timeout: Optional[float] = None,
|
|
37
|
+
max_retries: Optional[int] = None,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Initialize Waveium client.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
api_key: API key for authentication. If not
|
|
43
|
+
provided, will try to load from
|
|
44
|
+
WAVEIUM_API_KEY environment variable.
|
|
45
|
+
base_url: Base URL for the Waveium API.
|
|
46
|
+
Defaults to production URL.
|
|
47
|
+
timeout: Request timeout in seconds.
|
|
48
|
+
Defaults to 60.0.
|
|
49
|
+
max_retries: Maximum number of retries for
|
|
50
|
+
failed requests. Defaults to 3.
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
ConfigurationError: If configuration is invalid.
|
|
54
|
+
"""
|
|
55
|
+
self._config = Config(
|
|
56
|
+
api_key=api_key,
|
|
57
|
+
base_url=base_url,
|
|
58
|
+
timeout=timeout,
|
|
59
|
+
max_retries=max_retries,
|
|
60
|
+
)
|
|
61
|
+
self._config.validate()
|
|
62
|
+
|
|
63
|
+
self._http = HTTPClient(self._config)
|
|
64
|
+
|
|
65
|
+
# Initialize resources
|
|
66
|
+
self.auth = AuthResource(self._http)
|
|
67
|
+
self.environments = EnvironmentsResource(self._http)
|
|
68
|
+
self.scenes = ScenesResource(self._http)
|
|
69
|
+
self.executions = ExecutionsResource(self._http)
|
|
70
|
+
self.users = UsersResource(self._http)
|
|
71
|
+
self.organizations = OrganizationsResource(self._http)
|
|
72
|
+
self.roles = RolesResource(self._http)
|
|
73
|
+
self.teams = TeamsResource(self._http)
|
|
74
|
+
self.projects = ProjectsResource(self._http)
|
|
75
|
+
|
|
76
|
+
def close(self) -> None:
|
|
77
|
+
"""Close the HTTP client and release resources."""
|
|
78
|
+
self._http.close()
|
|
79
|
+
|
|
80
|
+
def __enter__(self) -> "Client":
|
|
81
|
+
"""Enter context manager."""
|
|
82
|
+
return self
|
|
83
|
+
|
|
84
|
+
def __exit__(self, *args: object) -> None:
|
|
85
|
+
"""Exit context manager."""
|
|
86
|
+
self.close()
|
|
87
|
+
|
|
88
|
+
def __repr__(self) -> str:
|
|
89
|
+
"""Return string representation of client."""
|
|
90
|
+
return f"<Waveium Client base_url=" f"{self._config.base_url}>"
|