wavespeed 0.0.3__tar.gz → 0.0.5__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavespeed
3
- Version: 0.0.3
3
+ Version: 0.0.5
4
4
  Summary: Python client for WaveSpeed AI
5
5
  Author: WaveSpeed AI
6
6
  License: MIT License
@@ -264,8 +264,7 @@ await prediction.async_reload() -> Prediction
264
264
  ## Environment Variables
265
265
 
266
266
  - `WAVESPEED_API_KEY`: Your WaveSpeed API key
267
- - `WAVESPEED_POLL_INTERVAL`: Interval in seconds for polling prediction status (default: 1)
268
- - `WAVESPEED_TIMEOUT`: Timeout in seconds for API requests (default: 60)
267
+ - `WAVESPEED_POLL_INTERVAL`: Interval in seconds for polling prediction status (default: 0.5)
269
268
 
270
269
  ## License
271
270
 
@@ -227,8 +227,7 @@ await prediction.async_reload() -> Prediction
227
227
  ## Environment Variables
228
228
 
229
229
  - `WAVESPEED_API_KEY`: Your WaveSpeed API key
230
- - `WAVESPEED_POLL_INTERVAL`: Interval in seconds for polling prediction status (default: 1)
231
- - `WAVESPEED_TIMEOUT`: Timeout in seconds for API requests (default: 60)
230
+ - `WAVESPEED_POLL_INTERVAL`: Interval in seconds for polling prediction status (default: 0.5)
232
231
 
233
232
  ## License
234
233
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "wavespeed"
7
- version = "0.0.3"
7
+ version = "0.0.5"
8
8
  description = "Python client for WaveSpeed AI"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -2,13 +2,19 @@
2
2
  Tests for the Wavespeed client.
3
3
  """
4
4
 
5
+ import unittest
5
6
  import pytest
6
7
  import respx
7
8
  from httpx import Response
9
+ import httpx
8
10
  from datetime import datetime
11
+ import unittest.mock
9
12
 
10
- from wavespeed.client import WaveSpeed
13
+ from wavespeed.client import WaveSpeed, RetryTransport
11
14
  from wavespeed.schemas.prediction import Prediction, PredictionUrls
15
+ import logging
16
+
17
+ logging.basicConfig(level=logging.INFO)
12
18
 
13
19
 
14
20
  @pytest.fixture
@@ -112,6 +118,86 @@ def mock_prediction_completed_response():
112
118
  }
113
119
 
114
120
 
121
+ @pytest.fixture
122
+ def mock_transport():
123
+ """Create a mock transport for testing retry functionality."""
124
+ class MockTransport(httpx.BaseTransport):
125
+ def __init__(self):
126
+ self.request_count = 0
127
+ self.requests = []
128
+
129
+ def handle_request(self, request):
130
+ self.request_count += 1
131
+ self.requests.append(request)
132
+
133
+ # Return 503 for the first two requests, then 200
134
+ if self.request_count <= 2:
135
+ response = Response(503, json={"error": "Service Unavailable"})
136
+ response._request = request # Set the request on the response
137
+ return response
138
+ else:
139
+ response = Response(200, json={
140
+ "code": 200,
141
+ "message": "Success",
142
+ "data": {
143
+ "id": "test_prediction_id",
144
+ "model": "wavespeed-ai/flux-dev",
145
+ "input": {"prompt": "A test prompt"},
146
+ "outputs": ["https://example.com/generated_image.jpg"],
147
+ "urls": {"get": "https://api.wavespeed.ai/api/v2/predictions/test_prediction_id/result"},
148
+ "has_nsfw_contents": [False],
149
+ "status": "completed",
150
+ "created_at": datetime.now().isoformat(),
151
+ "error": "",
152
+ "executionTime": 1000
153
+ }
154
+ })
155
+ response._request = request # Set the request on the response
156
+ return response
157
+
158
+ return MockTransport()
159
+
160
+
161
+ @pytest.fixture
162
+ def mock_async_transport():
163
+ """Create a mock async transport for testing retry functionality."""
164
+ class MockAsyncTransport(httpx.AsyncBaseTransport):
165
+ def __init__(self):
166
+ self.request_count = 0
167
+ self.requests = []
168
+
169
+ async def handle_async_request(self, request):
170
+ self.request_count += 1
171
+ self.requests.append(request)
172
+
173
+ # Return 503 for the first two requests, then 200
174
+ if self.request_count <= 2:
175
+ response = Response(503, json={"error": "Service Unavailable"})
176
+ response._request = request # Set the request on the response
177
+ return response
178
+ else:
179
+ response = Response(200, json={
180
+ "code": 200,
181
+ "message": "Success",
182
+ "data": {
183
+ "id": "test_prediction_id",
184
+ "model": "wavespeed-ai/flux-dev",
185
+ "input": {"prompt": "A test prompt"},
186
+ "outputs": ["https://example.com/generated_image.jpg"],
187
+ "urls": {"get": "https://api.wavespeed.ai/api/v2/predictions/test_prediction_id/result"},
188
+ "has_nsfw_contents": [False],
189
+ "status": "completed",
190
+ "created_at": datetime.now().isoformat(),
191
+ "error": "",
192
+ "executionTime": 1000
193
+ }
194
+ })
195
+ response._request = request # Set the request on the response
196
+ return response
197
+
198
+ return MockAsyncTransport()
199
+
200
+
115
201
  @respx.mock
116
202
  def test_run(client, mock_prediction_response, mock_prediction_completed_response):
117
203
  """Test the run method."""
@@ -274,6 +360,7 @@ def test_prediction_wait(client, mock_prediction_in_progress_response, mock_pred
274
360
  assert result.status == "completed"
275
361
  assert len(result.outputs) == 1
276
362
  assert result.outputs[0] == "https://example.com/generated_image.jpg"
363
+ assert result.has_nsfw_contents == [False]
277
364
 
278
365
 
279
366
  @respx.mock
@@ -313,3 +400,98 @@ async def test_prediction_async_wait(async_client, mock_prediction_in_progress_r
313
400
  assert result.status == "completed"
314
401
  assert len(result.outputs) == 1
315
402
  assert result.outputs[0] == "https://example.com/generated_image.jpg"
403
+ assert result.has_nsfw_contents == [False]
404
+
405
+ @unittest.mock.patch("httpx.HTTPTransport.handle_request")
406
+ def test_client_retry_integration(mock_send):
407
+ """Test that the client's retry logic works correctly when integrated with httpx."""
408
+ # Create a counter to track the number of requests
409
+ request_count = {"count": 0}
410
+
411
+ # Configure the mock to return 502 for the first request, 503 for the second, then 200
412
+ def side_effect(request, **kwargs):
413
+ request_count["count"] += 1
414
+ if request_count["count"] == 1:
415
+ response = Response(502, json={"error": "Bad Gateway"})
416
+ elif request_count["count"] == 2:
417
+ response = Response(503, json={"error": "Service Unavailable"})
418
+ else:
419
+ response = Response(200, json={
420
+ "code": 200,
421
+ "message": "Success",
422
+ "data": {
423
+ "id": "test_prediction_id",
424
+ "model": "wavespeed-ai/flux-dev",
425
+ "input": {"prompt": "A test prompt"},
426
+ "outputs": ["https://example.com/generated_image.jpg"],
427
+ "urls": {"get": "https://api.wavespeed.ai/api/v2/predictions/test_prediction_id/result"},
428
+ "has_nsfw_contents": [False],
429
+ "status": "completed",
430
+ "created_at": datetime.now().isoformat(),
431
+ "error": "",
432
+ "executionTime": 1000
433
+ }
434
+ })
435
+ response._request = request
436
+ return response
437
+ mock_send.side_effect = side_effect
438
+
439
+ # Create a test client
440
+ client = WaveSpeed(api_key="test_api_key")
441
+
442
+ prediction = client.get_prediction("test_prediction_id")
443
+ assert prediction.status == "completed"
444
+ # Verify that the transport was called the expected number of times (3 total: 2 failures + 1 success)
445
+ assert request_count["count"] == 3
446
+ assert mock_send.call_count == 3
447
+
448
+
449
+ @pytest.mark.asyncio
450
+ @unittest.mock.patch("httpx.AsyncHTTPTransport.handle_async_request")
451
+ async def test_client_async_retry_integration(mock_send):
452
+ """Test that the client's async retry logic works correctly when integrated with httpx."""
453
+ # Create a counter to track the number of requests
454
+ request_count = {"count": 0}
455
+
456
+ # Configure the mock to return 502 for the first request, 503 for the second, then 200
457
+ async def side_effect(request, **kwargs):
458
+ request_count["count"] += 1
459
+ if request_count["count"] == 1:
460
+ response = Response(502, json={"error": "Bad Gateway"})
461
+ elif request_count["count"] == 2:
462
+ response = Response(503, json={"error": "Service Unavailable"})
463
+ else:
464
+ response = Response(200, json={
465
+ "code": 200,
466
+ "message": "Success",
467
+ "data": {
468
+ "id": "test_prediction_id",
469
+ "model": "wavespeed-ai/flux-dev",
470
+ "input": {"prompt": "A test prompt"},
471
+ "outputs": ["https://example.com/generated_image.jpg"],
472
+ "urls": {"get": "https://api.wavespeed.ai/api/v2/predictions/test_prediction_id/result"},
473
+ "has_nsfw_contents": [False],
474
+ "status": "completed",
475
+ "created_at": datetime.now().isoformat(),
476
+ "error": "",
477
+ "executionTime": 1000
478
+ }
479
+ })
480
+ response._request = request
481
+ return response
482
+ mock_send.side_effect = side_effect
483
+
484
+ # Create a test client
485
+ client = WaveSpeed(api_key="test_api_key")
486
+
487
+ # Send the request through the client
488
+ prediction = await client.async_get_prediction(
489
+ predictionId="test_prediction_id",
490
+ )
491
+
492
+ # Verify the response is successful
493
+ assert prediction.status == "completed"
494
+
495
+ # Verify that the transport was called the expected number of times (3 total: 2 failures + 1 success)
496
+ assert request_count["count"] == 3
497
+ assert mock_send.call_count == 3
@@ -0,0 +1,334 @@
1
+ import os
2
+ import time
3
+ import httpx
4
+ from typing import Dict, Any
5
+ from urllib.parse import urljoin
6
+ import asyncio
7
+ import random
8
+ from datetime import datetime
9
+ from typing import Iterable, Optional, Mapping, Union
10
+ import logging
11
+
12
+ from wavespeed.schemas.prediction import Prediction
13
+
14
+ class RetryTransport(httpx.AsyncBaseTransport, httpx.BaseTransport):
15
+ """A custom HTTP transport that automatically retries requests using an exponential backoff strategy
16
+ for specific HTTP status codes and request methods.
17
+ """
18
+
19
+ RETRYABLE_METHODS = frozenset(["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE"])
20
+ RETRYABLE_STATUS_CODES = frozenset(
21
+ [
22
+ 429, # Too Many Requests
23
+ 502, # Bad Gateway
24
+ 503, # Service Unavailable
25
+ 504, # Gateway Timeout
26
+ ]
27
+ )
28
+ MAX_BACKOFF_WAIT = 60
29
+
30
+
31
+ def __init__( # pylint: disable=too-many-arguments
32
+ self,
33
+ wrapped_transport: Union[httpx.BaseTransport, httpx.AsyncBaseTransport],
34
+ *,
35
+ max_attempts: int = 10,
36
+ max_backoff_wait: float = MAX_BACKOFF_WAIT,
37
+ backoff_factor: float = 0.1,
38
+ jitter_ratio: float = 0.1,
39
+ retryable_methods: Optional[Iterable[str]] = None,
40
+ retry_status_codes: Optional[Iterable[int]] = None,
41
+ ) -> None:
42
+ self._wrapped_transport = wrapped_transport
43
+
44
+ if jitter_ratio < 0 or jitter_ratio > 0.5:
45
+ raise ValueError(
46
+ f"jitter ratio should be between 0 and 0.5, actual {jitter_ratio}"
47
+ )
48
+
49
+ self.max_attempts = max_attempts
50
+ self.backoff_factor = backoff_factor
51
+ self.retryable_methods = (
52
+ frozenset(retryable_methods)
53
+ if retryable_methods
54
+ else self.RETRYABLE_METHODS
55
+ )
56
+ self.retry_status_codes = (
57
+ frozenset(retry_status_codes)
58
+ if retry_status_codes
59
+ else self.RETRYABLE_STATUS_CODES
60
+ )
61
+ self.jitter_ratio = jitter_ratio
62
+ self.max_backoff_wait = max_backoff_wait
63
+
64
+ def _calculate_sleep(
65
+ self, attempts_made: int, headers: Union[httpx.Headers, Mapping[str, str]]
66
+ ) -> float:
67
+ retry_after_header = (headers.get("Retry-After") or "").strip()
68
+ if retry_after_header:
69
+ if retry_after_header.isdigit():
70
+ return float(retry_after_header)
71
+
72
+ try:
73
+ parsed_date = datetime.fromisoformat(retry_after_header).astimezone()
74
+ diff = (parsed_date - datetime.now().astimezone()).total_seconds()
75
+ if diff > 0:
76
+ return min(diff, self.max_backoff_wait)
77
+ except ValueError:
78
+ pass
79
+
80
+ backoff = self.backoff_factor * (2 ** (attempts_made - 1))
81
+ jitter = (backoff * self.jitter_ratio) * random.choice([1, -1]) # noqa: S311
82
+ total_backoff = backoff + jitter
83
+ return min(total_backoff, self.max_backoff_wait)
84
+
85
+ def handle_request(self, request: httpx.Request) -> httpx.Response:
86
+ response = self._wrapped_transport.handle_request(request) # type: ignore
87
+
88
+ if request.method not in self.retryable_methods:
89
+ return response
90
+
91
+ remaining_attempts = self.max_attempts - 1
92
+ attempts_made = 1
93
+
94
+ while True:
95
+ if (
96
+ remaining_attempts < 1
97
+ or response.status_code not in self.retry_status_codes
98
+ ):
99
+ return response
100
+
101
+ response.close()
102
+
103
+ sleep_for = self._calculate_sleep(attempts_made, response.headers)
104
+ logging.info("Got %s, Retrying request after %s seconds", response.status_code, sleep_for)
105
+ time.sleep(sleep_for)
106
+
107
+ response = self._wrapped_transport.handle_request(request) # type: ignore
108
+
109
+ attempts_made += 1
110
+ remaining_attempts -= 1
111
+
112
+ async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
113
+ response = await self._wrapped_transport.handle_async_request(request) # type: ignore
114
+
115
+ if request.method not in self.retryable_methods:
116
+ return response
117
+
118
+ remaining_attempts = self.max_attempts - 1
119
+ attempts_made = 1
120
+
121
+ while True:
122
+ if (
123
+ remaining_attempts < 1
124
+ or response.status_code not in self.retry_status_codes
125
+ ):
126
+ return response
127
+
128
+ await response.aclose()
129
+
130
+ sleep_for = self._calculate_sleep(attempts_made, response.headers)
131
+ logging.info("Got %s, Retrying request after %s seconds", response.status_code, sleep_for)
132
+ await asyncio.sleep(sleep_for)
133
+
134
+ response = await self._wrapped_transport.handle_async_request(request) # type: ignore
135
+
136
+ attempts_made += 1
137
+ remaining_attempts -= 1
138
+
139
+ async def aclose(self) -> None:
140
+ await self._wrapped_transport.aclose() # type: ignore
141
+
142
+ def close(self) -> None:
143
+ self._wrapped_transport.close() # type: ignore
144
+
145
+ class WaveSpeed:
146
+ """
147
+ A client for interacting with the Wavespeed AI API.
148
+ """
149
+ _async_client: Optional[httpx.AsyncClient] = None
150
+ _client: Optional[httpx.Client] = None
151
+
152
+ def __init__(self, api_key: str="", base_url: str = "https://api.wavespeed.ai/api/v2/", timeout: int | None = 120, **kwargs):
153
+ """
154
+ Initialize the WaveSpeed client.
155
+
156
+ Args:
157
+ api_key: Your WaveSpeed API key
158
+ base_url: Base URL for the API
159
+ timeout: Timeout in seconds for http client send request
160
+ """
161
+ self.api_key = api_key
162
+ if not self.api_key:
163
+ self.api_key = os.environ.get("WAVESPEED_API_KEY", '')
164
+ if not self.api_key:
165
+ raise ValueError("API key is required.")
166
+ self.headers = {
167
+ "Content-Type": "application/json",
168
+ "Authorization": f"Bearer {self.api_key}"
169
+ }
170
+ self.base_url = base_url
171
+ self.timeout = timeout
172
+ self.poll_interval = float(os.environ.get("WAVESPEED_POLL_INTERVAL", 0.5))
173
+ self._client_kwargs = kwargs
174
+
175
+ @property
176
+ def async_client(self) -> httpx.AsyncClient:
177
+ if self._async_client:
178
+ return self._async_client
179
+ transport = self._client_kwargs.get("async_transport", None) or httpx.AsyncHTTPTransport()
180
+ self._async_client = httpx.AsyncClient(headers=self.headers, timeout=self.timeout, transport=RetryTransport(transport))
181
+ return self._async_client
182
+
183
+ @property
184
+ def client(self) -> httpx.Client:
185
+ if self._client:
186
+ return self._client
187
+ transport = self._client_kwargs.get("transport", None) or httpx.HTTPTransport()
188
+ self._client = httpx.Client(headers=self.headers, timeout=self.timeout, transport=RetryTransport(transport))
189
+ return self._client
190
+
191
+ async def async_run(
192
+ self,
193
+ modelId: str,
194
+ input: Dict[str, Any],
195
+ **kwargs
196
+ ) -> Prediction:
197
+ """
198
+ Generate an image using the Wavespeed AI API.
199
+
200
+ Args:
201
+ modelId: The ID of the model to use
202
+ input: Input parameters for the model
203
+ **kwargs: Additional parameters to pass to the API
204
+
205
+ Returns:
206
+ The API response as a dictionary
207
+ """
208
+ url = urljoin(self.base_url, modelId)
209
+
210
+ payload = input
211
+
212
+ response = await self.async_client.post(
213
+ url,
214
+ headers=self.headers,
215
+ json=payload,
216
+ )
217
+
218
+ # Raise an exception for HTTP errors
219
+ response.raise_for_status()
220
+ data = response.json()
221
+ if data.get('code') != 200:
222
+ raise ValueError(f"Unexpected code: {data.get('code')}, data: {data}")
223
+ prediction = Prediction(**data['data'])
224
+ prediction._client = self
225
+ return await prediction.async_wait()
226
+
227
+ def run(
228
+ self,
229
+ modelId: str,
230
+ input: Dict[str, Any],
231
+ **kwargs
232
+ ) -> Prediction:
233
+ """
234
+ Generate an image using the Wavespeed AI API.
235
+
236
+ Args:
237
+ modelId: The ID of the model to use
238
+ input: Input parameters for the model
239
+ **kwargs: Additional parameters to pass to the API
240
+
241
+ Returns:
242
+ The API response as a dictionary
243
+ """
244
+ url = urljoin(self.base_url, modelId)
245
+
246
+ payload = input
247
+
248
+ response = self.client.post(
249
+ url,
250
+ headers=self.headers,
251
+ json=payload,
252
+ )
253
+
254
+ # Raise an exception for HTTP errors
255
+ response.raise_for_status()
256
+ data = response.json()
257
+ if data.get('code') != 200:
258
+ raise ValueError(f"Unexpected code: {data.get('code')}, data: {data}")
259
+ prediction = Prediction(**data['data'])
260
+ prediction._client = self
261
+ return prediction.wait()
262
+
263
+ async def async_create(self, modelId: str, input: Dict[str, Any], **kwargs) -> Prediction:
264
+ url = urljoin(self.base_url, modelId)
265
+ payload = input
266
+ response = await self.async_client.post(
267
+ url,
268
+ headers=self.headers,
269
+ json=payload,
270
+ )
271
+ # Raise an exception for HTTP errors
272
+ response.raise_for_status()
273
+ data = response.json()
274
+ if data.get('code') != 200:
275
+ raise ValueError(f"Unexpected code: {data.get('code')}, data: {data}")
276
+ prediction = Prediction(**data['data'])
277
+ prediction._client = self
278
+ return prediction
279
+
280
+ def create(self, modelId: str, input: Dict[str, Any], **kwargs) -> Prediction:
281
+ url = urljoin(self.base_url, modelId)
282
+ payload = input
283
+ response = self.client.post(
284
+ url,
285
+ headers=self.headers,
286
+ json=payload,
287
+ )
288
+ # Raise an exception for HTTP errors
289
+ response.raise_for_status()
290
+ data = response.json()
291
+ if data.get('code') != 200:
292
+ raise ValueError(f"Unexpected code: {data.get('code')}, data: {data}")
293
+ prediction = Prediction(**data['data'])
294
+ prediction._client = self
295
+ return prediction
296
+
297
+ def get_prediction(self, predictionId: str) -> Prediction:
298
+ url = urljoin(self.base_url, f"predictions/{predictionId}/result")
299
+ response = self.client.get(
300
+ url,
301
+ headers=self.headers,
302
+ )
303
+ # Raise an exception for HTTP errors
304
+ response.raise_for_status()
305
+ data = response.json()
306
+ if data.get('code') != 200:
307
+ raise ValueError(f"Unexpected code: {data.get('code')}, data: {data}")
308
+ prediction = Prediction(**data['data'])
309
+ prediction._client = self
310
+ return prediction
311
+
312
+ async def async_get_prediction(self, predictionId: str) -> Prediction:
313
+ url = urljoin(self.base_url, f"predictions/{predictionId}/result")
314
+ response = await self.async_client.get(
315
+ url,
316
+ headers=self.headers,
317
+ )
318
+ # Raise an exception for HTTP errors
319
+ response.raise_for_status()
320
+ data = response.json()
321
+ if data.get('code') != 200:
322
+ raise ValueError(f"Unexpected code: {data.get('code')}, data: {data}")
323
+ prediction = Prediction(**data['data'])
324
+ prediction._client = self
325
+ return prediction
326
+
327
+ async def close(self):
328
+ """Close the httpx client session."""
329
+ self.client.close()
330
+ await self.async_client.aclose()
331
+
332
+ def __str__(self) -> str:
333
+ """String representation of the client."""
334
+ return f"WaveSpeed()"
@@ -35,8 +35,10 @@ class Prediction(BaseModel):
35
35
  print('Waiting for prediction to complete: ', self.urls.get, type(self.urls.get))
36
36
  response = self._client.client.get(self.urls.get)
37
37
  response.raise_for_status()
38
- data = response.json()['data']
39
- self._update_from_dict(data)
38
+ data = response.json()
39
+ if data.get('code') != 200:
40
+ raise ValueError(f"Unexpected code: {data.get('code')}, data: {data}")
41
+ self._update_from_dict(data['data'])
40
42
  return self
41
43
 
42
44
  async def async_wait(self) -> "Prediction":
@@ -44,22 +46,28 @@ class Prediction(BaseModel):
44
46
  await asyncio.sleep(self._client.poll_interval)
45
47
  response = await self._client.async_client.get(self.urls.get)
46
48
  response.raise_for_status()
47
- data = response.json()['data']
48
- self._update_from_dict(data)
49
+ data = response.json()
50
+ if data.get('code') != 200:
51
+ raise ValueError(f"Unexpected code: {data.get('code')}, data: {data}")
52
+ self._update_from_dict(data['data'])
49
53
  return self
50
54
 
51
55
  async def async_reload(self) -> "Prediction":
52
56
  response = await self._client.async_client.get(self.urls.get)
53
57
  response.raise_for_status()
54
- data = response.json()['data']
55
- self._update_from_dict(data)
58
+ data = response.json()
59
+ if data.get('code') != 200:
60
+ raise ValueError(f"Unexpected code: {data.get('code')}, data: {data}")
61
+ self._update_from_dict(data['data'])
56
62
  return self
57
63
 
58
64
  def reload(self) -> "Prediction":
59
65
  response = self._client.client.get(self.urls.get)
60
66
  response.raise_for_status()
61
- data = response.json()['data']
62
- self._update_from_dict(data)
67
+ data = response.json()
68
+ if data.get('code') != 200:
69
+ raise ValueError(f"Unexpected code: {data.get('code')}, data: {data}")
70
+ self._update_from_dict(data['data'])
63
71
  return self
64
72
 
65
73
  def _update_from_dict(self, data: Dict[str, Any]) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavespeed
3
- Version: 0.0.3
3
+ Version: 0.0.5
4
4
  Summary: Python client for WaveSpeed AI
5
5
  Author: WaveSpeed AI
6
6
  License: MIT License
@@ -264,8 +264,7 @@ await prediction.async_reload() -> Prediction
264
264
  ## Environment Variables
265
265
 
266
266
  - `WAVESPEED_API_KEY`: Your WaveSpeed API key
267
- - `WAVESPEED_POLL_INTERVAL`: Interval in seconds for polling prediction status (default: 1)
268
- - `WAVESPEED_TIMEOUT`: Timeout in seconds for API requests (default: 60)
267
+ - `WAVESPEED_POLL_INTERVAL`: Interval in seconds for polling prediction status (default: 0.5)
269
268
 
270
269
  ## License
271
270
 
@@ -1,150 +0,0 @@
1
- import os
2
- import httpx
3
- from typing import Dict, Any
4
- from urllib.parse import urljoin
5
-
6
- from wavespeed.schemas.prediction import Prediction
7
-
8
-
9
- class WaveSpeed:
10
- """
11
- A client for interacting with the Wavespeed AI API.
12
- """
13
-
14
- BASE_URL = "https://api.wavespeed.ai/api/v2/"
15
-
16
- def __init__(self, api_key: str):
17
- """
18
- Initialize the Wavespeed client.
19
-
20
- Args:
21
- api_key: Your Wavespeed API key
22
- """
23
- self.api_key = api_key
24
- if not api_key:
25
- api_key = os.environ.get("WAVESPEED_API_KEY", '')
26
- if not api_key:
27
- raise ValueError("API key is required.")
28
- self.headers = {
29
- "Content-Type": "application/json",
30
- "Authorization": f"Bearer {api_key}"
31
- }
32
- self.async_client = httpx.AsyncClient(headers=self.headers)
33
- self.client = httpx.Client(headers=self.headers)
34
- self.poll_interval = float(os.environ.get("WAVESPEED_POLL_INTERVAL", 1)) # seconds
35
- self.timeout = int(os.environ.get("WAVESPEED_TIMEOUT", 60)) # seconds
36
-
37
- async def async_run(
38
- self,
39
- modelId: str,
40
- input: Dict[str, Any],
41
- **kwargs
42
- ) -> Prediction:
43
- """
44
- Generate an image using the Wavespeed AI API.
45
-
46
- Args:
47
- modelId: The ID of the model to use
48
- input: Input parameters for the model
49
- **kwargs: Additional parameters to pass to the API
50
-
51
- Returns:
52
- The API response as a dictionary
53
- """
54
- url = urljoin(self.BASE_URL, modelId)
55
-
56
- payload = input
57
-
58
- # Reset client if it's closed
59
- if self.async_client.is_closed:
60
- self.async_client = httpx.AsyncClient()
61
-
62
- response = await self.async_client.post(
63
- url,
64
- headers=self.headers,
65
- json=payload,
66
- timeout=self.timeout
67
- )
68
-
69
- # Raise an exception for HTTP errors
70
- response.raise_for_status()
71
- data = response.json()
72
- prediction = Prediction(**data['data'])
73
- prediction._client = self
74
- return await prediction.async_wait()
75
-
76
- def run(
77
- self,
78
- modelId: str,
79
- input: Dict[str, Any],
80
- **kwargs
81
- ) -> Prediction:
82
- """
83
- Generate an image using the Wavespeed AI API.
84
-
85
- Args:
86
- modelId: The ID of the model to use
87
- input: Input parameters for the model
88
- **kwargs: Additional parameters to pass to the API
89
-
90
- Returns:
91
- The API response as a dictionary
92
- """
93
- url = urljoin(self.BASE_URL, modelId)
94
-
95
- payload = input
96
-
97
- response = self.client.post(
98
- url,
99
- headers=self.headers,
100
- json=payload,
101
- timeout=self.timeout
102
- )
103
-
104
- # Raise an exception for HTTP errors
105
- response.raise_for_status()
106
- data = response.json()
107
- prediction = Prediction(**data['data'])
108
- prediction._client = self
109
- return prediction.wait()
110
-
111
- async def async_create(self, modelId: str, input: Dict[str, Any], **kwargs) -> Prediction:
112
- url = urljoin(self.BASE_URL, modelId)
113
- payload = input
114
- response = await self.async_client.post(
115
- url,
116
- headers=self.headers,
117
- json=payload,
118
- timeout=self.timeout
119
- )
120
- # Raise an exception for HTTP errors
121
- response.raise_for_status()
122
- data = response.json()
123
- prediction = Prediction(**data['data'])
124
- prediction._client = self
125
- return prediction
126
-
127
- def create(self, modelId: str, input: Dict[str, Any], **kwargs) -> Prediction:
128
- url = urljoin(self.BASE_URL, modelId)
129
- payload = input
130
- response = self.client.post(
131
- url,
132
- headers=self.headers,
133
- json=payload,
134
- timeout=self.timeout
135
- )
136
- # Raise an exception for HTTP errors
137
- response.raise_for_status()
138
- data = response.json()
139
- prediction = Prediction(**data['data'])
140
- prediction._client = self
141
- return prediction
142
-
143
- async def close(self):
144
- """Close the httpx client session."""
145
- self.client.close()
146
- await self.async_client.aclose()
147
-
148
- def __str__(self) -> str:
149
- """String representation of the client."""
150
- return f"WaveSpeed()"
File without changes
File without changes