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 ADDED
@@ -0,0 +1,204 @@
1
+ """Waveium SDK for Python.
2
+
3
+ Official Python client library for the Waveium API.
4
+
5
+ Example:
6
+ >>> import waveium
7
+ >>> client = waveium.init(api_key="wvx_...")
8
+ >>> resp = client.environments.create(name="My Env")
9
+ >>> env = resp.environment
10
+ """
11
+
12
+ from typing import Optional
13
+
14
+ from ._config import get_firebase_token
15
+ from .async_client import AsyncClient
16
+ from .client import Client
17
+ from .exceptions import (
18
+ APIError,
19
+ AuthenticationError,
20
+ AuthorizationError,
21
+ ConfigurationError,
22
+ ConflictError,
23
+ ExecutionFailedError,
24
+ FileDownloadError,
25
+ FileUploadError,
26
+ NetworkError,
27
+ NotFoundError,
28
+ RateLimitError,
29
+ StreamError,
30
+ StreamReconnectError,
31
+ StreamTimeoutError,
32
+ ValidationError,
33
+ WaveiumError,
34
+ )
35
+
36
+ # Expose key models
37
+ from .models import (
38
+ CreateEnvironmentRequest,
39
+ CreateTokenRequest,
40
+ EnvironmentResponse,
41
+ ExecutionProgress,
42
+ ExecutionResponse,
43
+ SceneResponse,
44
+ ServerEvent,
45
+ StatusEvent,
46
+ TokenExchangeResponse,
47
+ )
48
+
49
+ __version__ = "0.2.0"
50
+
51
+ __all__ = [
52
+ # Main clients
53
+ "Client",
54
+ "AsyncClient",
55
+ # Convenience functions
56
+ "init",
57
+ "auth",
58
+ # Exceptions
59
+ "WaveiumError",
60
+ "AuthenticationError",
61
+ "AuthorizationError",
62
+ "ConflictError",
63
+ "NotFoundError",
64
+ "ValidationError",
65
+ "RateLimitError",
66
+ "APIError",
67
+ "NetworkError",
68
+ "FileUploadError",
69
+ "FileDownloadError",
70
+ "ConfigurationError",
71
+ "StreamError",
72
+ "StreamTimeoutError",
73
+ "StreamReconnectError",
74
+ "ExecutionFailedError",
75
+ # Common models
76
+ "CreateEnvironmentRequest",
77
+ "CreateTokenRequest",
78
+ "EnvironmentResponse",
79
+ "ExecutionProgress",
80
+ "ExecutionResponse",
81
+ "SceneResponse",
82
+ "ServerEvent",
83
+ "StatusEvent",
84
+ "TokenExchangeResponse",
85
+ # Version
86
+ "__version__",
87
+ ]
88
+
89
+
90
+ def init(
91
+ api_key: Optional[str] = None,
92
+ base_url: Optional[str] = None,
93
+ timeout: Optional[float] = None,
94
+ max_retries: Optional[int] = None,
95
+ ) -> Client:
96
+ """Initialize a Waveium client with an API key.
97
+
98
+ Args:
99
+ api_key: API key for authentication. If not
100
+ provided, will try to load from
101
+ WAVEIUM_API_KEY environment variable.
102
+ base_url: Base URL for the Waveium API.
103
+ Defaults to production URL.
104
+ timeout: Request timeout in seconds.
105
+ Defaults to 60.0.
106
+ max_retries: Maximum number of retries for
107
+ failed requests. Defaults to 3.
108
+
109
+ Returns:
110
+ Configured Waveium client
111
+
112
+ Raises:
113
+ ConfigurationError: If configuration is invalid
114
+
115
+ Example:
116
+ >>> import waveium
117
+ >>> client = waveium.init(api_key="wvx_...")
118
+ >>> resp = client.environments.create(
119
+ ... name="Test"
120
+ ... )
121
+ """
122
+ return Client(
123
+ api_key=api_key,
124
+ base_url=base_url,
125
+ timeout=timeout,
126
+ max_retries=max_retries,
127
+ )
128
+
129
+
130
+ def auth(
131
+ firebase_token: Optional[str] = None,
132
+ token_name: str = "Python SDK Token",
133
+ expires_days: int = 30,
134
+ base_url: Optional[str] = None,
135
+ timeout: Optional[float] = None,
136
+ max_retries: Optional[int] = None,
137
+ ) -> Client:
138
+ """Initialize a Waveium client by exchanging a
139
+ Firebase JWT for an API key.
140
+
141
+ Args:
142
+ firebase_token: Firebase JWT token. If not
143
+ provided, will try to load from
144
+ FIREBASE_TOKEN environment variable.
145
+ token_name: Name for the created API token.
146
+ Defaults to "Python SDK Token".
147
+ expires_days: Number of days until token
148
+ expires (1-365). Defaults to 30.
149
+ base_url: Base URL for the Waveium API.
150
+ Defaults to production URL.
151
+ timeout: Request timeout in seconds.
152
+ Defaults to 60.0.
153
+ max_retries: Maximum number of retries for
154
+ failed requests. Defaults to 3.
155
+
156
+ Returns:
157
+ Configured Waveium client with API key
158
+
159
+ Raises:
160
+ ConfigurationError: If no Firebase token
161
+ provided or found
162
+ AuthenticationError: If Firebase token is invalid
163
+
164
+ Example:
165
+ >>> import waveium
166
+ >>> client = waveium.auth(
167
+ ... firebase_token="eyJ..."
168
+ ... )
169
+ >>> resp = client.environments.create(
170
+ ... name="Test"
171
+ ... )
172
+ """
173
+ if firebase_token is None:
174
+ firebase_token = get_firebase_token()
175
+
176
+ if firebase_token is None:
177
+ raise ConfigurationError(
178
+ "Firebase token is required. Provide it via "
179
+ "firebase_token parameter or FIREBASE_TOKEN "
180
+ "environment variable."
181
+ )
182
+
183
+ temp_client = Client(
184
+ api_key=firebase_token,
185
+ base_url=base_url,
186
+ timeout=timeout,
187
+ max_retries=max_retries,
188
+ )
189
+
190
+ try:
191
+ response = temp_client.auth.create_token(
192
+ name=token_name,
193
+ expires_days=expires_days,
194
+ firebase_token=firebase_token,
195
+ )
196
+
197
+ return Client(
198
+ api_key=response.api_key,
199
+ base_url=base_url,
200
+ timeout=timeout,
201
+ max_retries=max_retries,
202
+ )
203
+ finally:
204
+ temp_client.close()
waveium/_config.py ADDED
@@ -0,0 +1,88 @@
1
+ """Configuration management for the Waveium SDK."""
2
+
3
+ import os
4
+ from typing import Optional
5
+
6
+ from dotenv import load_dotenv
7
+
8
+ from .exceptions import ConfigurationError
9
+
10
+ # Load environment variables from .env file
11
+ load_dotenv()
12
+
13
+ # Default configuration
14
+ DEFAULT_BASE_URL = "https://api.waveium.io"
15
+ DEFAULT_TIMEOUT = 60.0
16
+ DEFAULT_MAX_RETRIES = 3
17
+
18
+
19
+ class Config:
20
+ """Configuration for Waveium SDK."""
21
+
22
+ def __init__(
23
+ self,
24
+ api_key: Optional[str] = None,
25
+ base_url: Optional[str] = None,
26
+ timeout: Optional[float] = None,
27
+ max_retries: Optional[int] = None,
28
+ ) -> None:
29
+ """Initialize configuration.
30
+
31
+ Args:
32
+ api_key: API key for authentication. If not provided,
33
+ will try to load from WAVEIUM_API_KEY or WAVEIUM_SDK_TOKEN
34
+ environment variable.
35
+ base_url: Base URL for the Waveium API.
36
+ Defaults to production URL.
37
+ timeout: Request timeout in seconds. Defaults to 60.0.
38
+ max_retries: Maximum number of retries for failed requests.
39
+ Defaults to 3.
40
+
41
+ Raises:
42
+ ConfigurationError: If no API key is provided or found
43
+ in environment.
44
+ """
45
+ self.api_key = (
46
+ api_key
47
+ or os.getenv("WAVEIUM_API_KEY")
48
+ or os.getenv("WAVEIUM_SDK_TOKEN")
49
+ )
50
+ self.base_url = base_url or os.getenv(
51
+ "WAVEIUM_BASE_URL", DEFAULT_BASE_URL
52
+ ).rstrip("/")
53
+ self.timeout = timeout or float(
54
+ os.getenv("WAVEIUM_TIMEOUT", str(DEFAULT_TIMEOUT))
55
+ )
56
+ self.max_retries = max_retries or int(
57
+ os.getenv("WAVEIUM_MAX_RETRIES", str(DEFAULT_MAX_RETRIES))
58
+ )
59
+
60
+ def validate(self) -> None:
61
+ """Validate configuration.
62
+
63
+ Raises:
64
+ ConfigurationError: If configuration is invalid.
65
+ """
66
+ if not self.api_key:
67
+ raise ConfigurationError(
68
+ "API key is required. Provide it via api_key parameter or "
69
+ "WAVEIUM_API_KEY/WAVEIUM_SDK_TOKEN environment variable."
70
+ )
71
+
72
+ if not self.base_url:
73
+ raise ConfigurationError("Base URL cannot be empty")
74
+
75
+ if self.timeout <= 0:
76
+ raise ConfigurationError("Timeout must be positive")
77
+
78
+ if self.max_retries < 0:
79
+ raise ConfigurationError("Max retries cannot be negative")
80
+
81
+
82
+ def get_firebase_token() -> Optional[str]:
83
+ """Get Firebase token from environment variable.
84
+
85
+ Returns:
86
+ Firebase token if found, None otherwise.
87
+ """
88
+ return os.getenv("FIREBASE_TOKEN")
waveium/_http.py ADDED
@@ -0,0 +1,362 @@
1
+ """HTTP client wrappers for the Waveium SDK."""
2
+
3
+ import logging
4
+ from typing import (
5
+ Any,
6
+ Dict,
7
+ Optional,
8
+ Type,
9
+ TypeVar,
10
+ Union,
11
+ overload,
12
+ )
13
+
14
+ import httpx
15
+ from pydantic import BaseModel, ValidationError as PydanticValidationError
16
+
17
+ from ._config import Config
18
+ from .exceptions import (
19
+ APIError,
20
+ AuthenticationError,
21
+ AuthorizationError,
22
+ ConflictError,
23
+ NetworkError,
24
+ NotFoundError,
25
+ RateLimitError,
26
+ ValidationError,
27
+ WaveiumError,
28
+ )
29
+ from .models.common import ErrorResponse
30
+
31
+ T = TypeVar("T", bound=BaseModel)
32
+
33
+ logger = logging.getLogger("waveium")
34
+
35
+
36
+ def _build_headers(config: Config) -> Dict[str, str]:
37
+ """Build request headers from config.
38
+
39
+ Args:
40
+ config: Configuration object
41
+
42
+ Returns:
43
+ Dictionary of headers
44
+ """
45
+ headers = {
46
+ "User-Agent": "waveium-python/0.2.0",
47
+ "Content-Type": "application/json",
48
+ }
49
+ if config.api_key:
50
+ if config.api_key.startswith(("wv_", "wvx_")):
51
+ headers["X-API-Key"] = config.api_key
52
+ else:
53
+ headers["Authorization"] = f"Bearer {config.api_key}"
54
+ return headers
55
+
56
+
57
+ def _handle_error_response(response: httpx.Response) -> WaveiumError:
58
+ """Convert HTTP error response to appropriate exception.
59
+
60
+ Args:
61
+ response: HTTP response object
62
+
63
+ Returns:
64
+ Appropriate WaveiumError subclass
65
+ """
66
+ try:
67
+ error_data = response.json()
68
+ error_response = ErrorResponse.model_validate(error_data)
69
+ message = error_response.error.message
70
+ code = error_response.error.code
71
+ details = error_response.error.details
72
+ except Exception:
73
+ message = response.text or f"HTTP {response.status_code}"
74
+ code = None
75
+ details = None
76
+
77
+ if response.status_code == 400:
78
+ return ValidationError(message, code, details)
79
+ elif response.status_code == 401:
80
+ return AuthenticationError(message, code, details)
81
+ elif response.status_code == 403:
82
+ return AuthorizationError(message, code, details)
83
+ elif response.status_code == 404:
84
+ return NotFoundError(message, code, details)
85
+ elif response.status_code == 409:
86
+ return ConflictError(message, code, details)
87
+ elif response.status_code == 429:
88
+ return RateLimitError(message, code, details)
89
+ else:
90
+ return APIError(message, code, details)
91
+
92
+
93
+ class HTTPClient:
94
+ """Synchronous HTTP client for Waveium API."""
95
+
96
+ def __init__(self, config: Config) -> None:
97
+ """Initialize HTTP client.
98
+
99
+ Args:
100
+ config: Configuration object
101
+ """
102
+ self.config = config
103
+ self._client: Optional[httpx.Client] = None
104
+
105
+ def _get_client(self) -> httpx.Client:
106
+ """Get or create HTTP client instance.
107
+
108
+ Returns:
109
+ HTTP client instance
110
+ """
111
+ if self._client is None:
112
+ self._client = httpx.Client(
113
+ base_url=self.config.base_url,
114
+ timeout=self.config.timeout,
115
+ headers=_build_headers(self.config),
116
+ )
117
+ return self._client
118
+
119
+ @overload
120
+ def request(
121
+ self,
122
+ method: str,
123
+ path: str,
124
+ *,
125
+ json_data: Optional[Dict[str, Any]] = None,
126
+ params: Optional[Dict[str, Any]] = None,
127
+ headers: Optional[Dict[str, str]] = None,
128
+ response_model: Type[T],
129
+ ) -> T: ...
130
+
131
+ @overload
132
+ def request(
133
+ self,
134
+ method: str,
135
+ path: str,
136
+ *,
137
+ json_data: Optional[Dict[str, Any]] = None,
138
+ params: Optional[Dict[str, Any]] = None,
139
+ headers: Optional[Dict[str, str]] = None,
140
+ response_model: None = None,
141
+ ) -> Dict[str, Any]: ...
142
+
143
+ def request(
144
+ self,
145
+ method: str,
146
+ path: str,
147
+ *,
148
+ json_data: Optional[Dict[str, Any]] = None,
149
+ params: Optional[Dict[str, Any]] = None,
150
+ headers: Optional[Dict[str, str]] = None,
151
+ response_model: Optional[Type[T]] = None,
152
+ ) -> Union[T, Dict[str, Any]]:
153
+ """Make HTTP request.
154
+
155
+ Args:
156
+ method: HTTP method (GET, POST, PUT, DELETE, etc.)
157
+ path: API endpoint path
158
+ json_data: Optional JSON request body
159
+ params: Optional query parameters
160
+ headers: Optional additional headers
161
+ response_model: Optional Pydantic model to parse response
162
+
163
+ Returns:
164
+ Parsed response model or raw dictionary
165
+
166
+ Raises:
167
+ WaveiumError: If request fails
168
+ """
169
+ client = self._get_client()
170
+ logger.debug("%s %s", method, path)
171
+
172
+ try:
173
+ response = client.request(
174
+ method=method,
175
+ url=path,
176
+ json=json_data,
177
+ params=params,
178
+ headers=headers,
179
+ )
180
+ response.raise_for_status()
181
+
182
+ if response.status_code == 204:
183
+ return (
184
+ {}
185
+ if response_model is None
186
+ else response_model.model_validate({})
187
+ )
188
+
189
+ response_data: Dict[str, Any] = response.json()
190
+
191
+ if response_model:
192
+ try:
193
+ return response_model.model_validate(response_data)
194
+ except PydanticValidationError as e:
195
+ raise ValidationError(
196
+ f"Failed to parse response: {str(e)}",
197
+ code="VALIDATION_ERROR",
198
+ details={"errors": e.errors()},
199
+ )
200
+
201
+ return response_data
202
+
203
+ except httpx.HTTPStatusError as e:
204
+ logger.debug("%s %s -> %d", method, path, e.response.status_code)
205
+ raise _handle_error_response(e.response)
206
+ except httpx.RequestError as e:
207
+ logger.debug("%s %s -> network error: %s", method, path, e)
208
+ raise NetworkError(f"Network error: {str(e)}")
209
+ except Exception as e:
210
+ if isinstance(e, WaveiumError):
211
+ raise
212
+ raise APIError(f"Unexpected error: {str(e)}")
213
+
214
+ def close(self) -> None:
215
+ """Close HTTP client."""
216
+ if self._client:
217
+ self._client.close()
218
+ self._client = None
219
+
220
+ def __enter__(self) -> "HTTPClient":
221
+ """Enter context manager."""
222
+ return self
223
+
224
+ def __exit__(self, *args: Any) -> None:
225
+ """Exit context manager."""
226
+ self.close()
227
+
228
+
229
+ class AsyncHTTPClient:
230
+ """Asynchronous HTTP client for Waveium API."""
231
+
232
+ def __init__(self, config: Config) -> None:
233
+ """Initialize async HTTP client.
234
+
235
+ Args:
236
+ config: Configuration object
237
+ """
238
+ self.config = config
239
+ self._client: Optional[httpx.AsyncClient] = None
240
+
241
+ def _get_client(self) -> httpx.AsyncClient:
242
+ """Get or create async HTTP client instance.
243
+
244
+ Returns:
245
+ Async HTTP client instance
246
+ """
247
+ if self._client is None:
248
+ self._client = httpx.AsyncClient(
249
+ base_url=self.config.base_url,
250
+ timeout=self.config.timeout,
251
+ headers=_build_headers(self.config),
252
+ )
253
+ return self._client
254
+
255
+ @overload
256
+ async def request(
257
+ self,
258
+ method: str,
259
+ path: str,
260
+ *,
261
+ json_data: Optional[Dict[str, Any]] = None,
262
+ params: Optional[Dict[str, Any]] = None,
263
+ headers: Optional[Dict[str, str]] = None,
264
+ response_model: Type[T],
265
+ ) -> T: ...
266
+
267
+ @overload
268
+ async def request(
269
+ self,
270
+ method: str,
271
+ path: str,
272
+ *,
273
+ json_data: Optional[Dict[str, Any]] = None,
274
+ params: Optional[Dict[str, Any]] = None,
275
+ headers: Optional[Dict[str, str]] = None,
276
+ response_model: None = None,
277
+ ) -> Dict[str, Any]: ...
278
+
279
+ async def request(
280
+ self,
281
+ method: str,
282
+ path: str,
283
+ *,
284
+ json_data: Optional[Dict[str, Any]] = None,
285
+ params: Optional[Dict[str, Any]] = None,
286
+ headers: Optional[Dict[str, str]] = None,
287
+ response_model: Optional[Type[T]] = None,
288
+ ) -> Union[T, Dict[str, Any]]:
289
+ """Make async HTTP request.
290
+
291
+ Args:
292
+ method: HTTP method (GET, POST, PUT, DELETE, etc.)
293
+ path: API endpoint path
294
+ json_data: Optional JSON request body
295
+ params: Optional query parameters
296
+ headers: Optional additional headers
297
+ response_model: Optional Pydantic model to parse response
298
+
299
+ Returns:
300
+ Parsed response model or raw dictionary
301
+
302
+ Raises:
303
+ WaveiumError: If request fails
304
+ """
305
+ client = self._get_client()
306
+ logger.debug("%s %s", method, path)
307
+
308
+ try:
309
+ response = await client.request(
310
+ method=method,
311
+ url=path,
312
+ json=json_data,
313
+ params=params,
314
+ headers=headers,
315
+ )
316
+ response.raise_for_status()
317
+
318
+ if response.status_code == 204:
319
+ return (
320
+ {}
321
+ if response_model is None
322
+ else response_model.model_validate({})
323
+ )
324
+
325
+ response_data: Dict[str, Any] = response.json()
326
+
327
+ if response_model:
328
+ try:
329
+ return response_model.model_validate(response_data)
330
+ except PydanticValidationError as e:
331
+ raise ValidationError(
332
+ f"Failed to parse response: {str(e)}",
333
+ code="VALIDATION_ERROR",
334
+ details={"errors": e.errors()},
335
+ )
336
+
337
+ return response_data
338
+
339
+ except httpx.HTTPStatusError as e:
340
+ logger.debug("%s %s -> %d", method, path, e.response.status_code)
341
+ raise _handle_error_response(e.response)
342
+ except httpx.RequestError as e:
343
+ logger.debug("%s %s -> network error: %s", method, path, e)
344
+ raise NetworkError(f"Network error: {str(e)}")
345
+ except Exception as e:
346
+ if isinstance(e, WaveiumError):
347
+ raise
348
+ raise APIError(f"Unexpected error: {str(e)}")
349
+
350
+ async def close(self) -> None:
351
+ """Close async HTTP client."""
352
+ if self._client:
353
+ await self._client.aclose()
354
+ self._client = None
355
+
356
+ async def __aenter__(self) -> "AsyncHTTPClient":
357
+ """Enter async context manager."""
358
+ return self
359
+
360
+ async def __aexit__(self, *args: Any) -> None:
361
+ """Exit async context manager."""
362
+ await self.close()