wavespeed 0.0.4__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.
- {wavespeed-0.0.4 → wavespeed-0.0.5}/PKG-INFO +2 -3
- {wavespeed-0.0.4 → wavespeed-0.0.5}/README.md +1 -2
- {wavespeed-0.0.4 → wavespeed-0.0.5}/pyproject.toml +1 -1
- {wavespeed-0.0.4 → wavespeed-0.0.5}/tests/test_client.py +183 -1
- wavespeed-0.0.5/wavespeed/client.py +334 -0
- {wavespeed-0.0.4 → wavespeed-0.0.5}/wavespeed/schemas/prediction.py +16 -8
- {wavespeed-0.0.4 → wavespeed-0.0.5}/wavespeed.egg-info/PKG-INFO +2 -3
- wavespeed-0.0.4/wavespeed/client.py +0 -147
- {wavespeed-0.0.4 → wavespeed-0.0.5}/LICENSE +0 -0
- {wavespeed-0.0.4 → wavespeed-0.0.5}/setup.cfg +0 -0
- {wavespeed-0.0.4 → wavespeed-0.0.5}/wavespeed/__init__.py +0 -0
- {wavespeed-0.0.4 → wavespeed-0.0.5}/wavespeed/schemas/__init__.py +0 -0
- {wavespeed-0.0.4 → wavespeed-0.0.5}/wavespeed.egg-info/SOURCES.txt +0 -0
- {wavespeed-0.0.4 → wavespeed-0.0.5}/wavespeed.egg-info/dependency_links.txt +0 -0
- {wavespeed-0.0.4 → wavespeed-0.0.5}/wavespeed.egg-info/requires.txt +0 -0
- {wavespeed-0.0.4 → wavespeed-0.0.5}/wavespeed.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wavespeed
|
|
3
|
-
Version: 0.0.
|
|
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:
|
|
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:
|
|
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
|
|
|
@@ -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()
|
|
39
|
-
|
|
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()
|
|
48
|
-
|
|
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()
|
|
55
|
-
|
|
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()
|
|
62
|
-
|
|
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
|
+
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:
|
|
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,147 +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
|
-
def __init__(self, api_key: str="", base_url: str = "https://api.wavespeed.ai/api/v2/", timeout: int | None = None):
|
|
15
|
-
"""
|
|
16
|
-
Initialize the WaveSpeed client.
|
|
17
|
-
|
|
18
|
-
Args:
|
|
19
|
-
api_key: Your WaveSpeed API key
|
|
20
|
-
base_url: Base URL for the API
|
|
21
|
-
timeout: Timeout in seconds for http client send request
|
|
22
|
-
"""
|
|
23
|
-
self.api_key = api_key
|
|
24
|
-
if not self.api_key:
|
|
25
|
-
self.api_key = os.environ.get("WAVESPEED_API_KEY", '')
|
|
26
|
-
if not self.api_key:
|
|
27
|
-
raise ValueError("API key is required.")
|
|
28
|
-
self.headers = {
|
|
29
|
-
"Content-Type": "application/json",
|
|
30
|
-
"Authorization": f"Bearer {self.api_key}"
|
|
31
|
-
}
|
|
32
|
-
self.async_client = httpx.AsyncClient(headers=self.headers, timeout=timeout)
|
|
33
|
-
self.client = httpx.Client(headers=self.headers, timeout=timeout)
|
|
34
|
-
self.base_url = base_url
|
|
35
|
-
self.timeout = timeout
|
|
36
|
-
self.poll_interval = float(os.environ.get("WAVESPEED_POLL_INTERVAL", 1))
|
|
37
|
-
|
|
38
|
-
async def async_run(
|
|
39
|
-
self,
|
|
40
|
-
modelId: str,
|
|
41
|
-
input: Dict[str, Any],
|
|
42
|
-
**kwargs
|
|
43
|
-
) -> Prediction:
|
|
44
|
-
"""
|
|
45
|
-
Generate an image using the Wavespeed AI API.
|
|
46
|
-
|
|
47
|
-
Args:
|
|
48
|
-
modelId: The ID of the model to use
|
|
49
|
-
input: Input parameters for the model
|
|
50
|
-
**kwargs: Additional parameters to pass to the API
|
|
51
|
-
|
|
52
|
-
Returns:
|
|
53
|
-
The API response as a dictionary
|
|
54
|
-
"""
|
|
55
|
-
url = urljoin(self.base_url, modelId)
|
|
56
|
-
|
|
57
|
-
payload = input
|
|
58
|
-
|
|
59
|
-
# Reset client if it's closed
|
|
60
|
-
if self.async_client.is_closed:
|
|
61
|
-
self.async_client = httpx.AsyncClient()
|
|
62
|
-
|
|
63
|
-
response = await self.async_client.post(
|
|
64
|
-
url,
|
|
65
|
-
headers=self.headers,
|
|
66
|
-
json=payload,
|
|
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
|
-
)
|
|
102
|
-
|
|
103
|
-
# Raise an exception for HTTP errors
|
|
104
|
-
response.raise_for_status()
|
|
105
|
-
data = response.json()
|
|
106
|
-
prediction = Prediction(**data['data'])
|
|
107
|
-
prediction._client = self
|
|
108
|
-
return prediction.wait()
|
|
109
|
-
|
|
110
|
-
async def async_create(self, modelId: str, input: Dict[str, Any], **kwargs) -> Prediction:
|
|
111
|
-
url = urljoin(self.base_url, modelId)
|
|
112
|
-
payload = input
|
|
113
|
-
response = await self.async_client.post(
|
|
114
|
-
url,
|
|
115
|
-
headers=self.headers,
|
|
116
|
-
json=payload,
|
|
117
|
-
)
|
|
118
|
-
# Raise an exception for HTTP errors
|
|
119
|
-
response.raise_for_status()
|
|
120
|
-
data = response.json()
|
|
121
|
-
prediction = Prediction(**data['data'])
|
|
122
|
-
prediction._client = self
|
|
123
|
-
return prediction
|
|
124
|
-
|
|
125
|
-
def create(self, modelId: str, input: Dict[str, Any], **kwargs) -> Prediction:
|
|
126
|
-
url = urljoin(self.base_url, modelId)
|
|
127
|
-
payload = input
|
|
128
|
-
response = self.client.post(
|
|
129
|
-
url,
|
|
130
|
-
headers=self.headers,
|
|
131
|
-
json=payload,
|
|
132
|
-
)
|
|
133
|
-
# Raise an exception for HTTP errors
|
|
134
|
-
response.raise_for_status()
|
|
135
|
-
data = response.json()
|
|
136
|
-
prediction = Prediction(**data['data'])
|
|
137
|
-
prediction._client = self
|
|
138
|
-
return prediction
|
|
139
|
-
|
|
140
|
-
async def close(self):
|
|
141
|
-
"""Close the httpx client session."""
|
|
142
|
-
self.client.close()
|
|
143
|
-
await self.async_client.aclose()
|
|
144
|
-
|
|
145
|
-
def __str__(self) -> str:
|
|
146
|
-
"""String representation of the client."""
|
|
147
|
-
return f"WaveSpeed()"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|