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/__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()
|