ads_dev 1.0.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.
- ads_dev/__init__.py +40 -0
- ads_dev/_async/client.py +335 -0
- ads_dev/_core.py +101 -0
- ads_dev/_sync/client.py +335 -0
- ads_dev/py.typed +0 -0
- ads_dev/types.py +2126 -0
- ads_dev-1.0.0.dist-info/METADATA +158 -0
- ads_dev-1.0.0.dist-info/RECORD +10 -0
- ads_dev-1.0.0.dist-info/WHEEL +4 -0
- ads_dev-1.0.0.dist-info/licenses/LICENSE +21 -0
ads_dev/_sync/client.py
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
import uuid
|
|
5
|
+
from collections.abc import Iterator, Mapping
|
|
6
|
+
from typing import Any, Generic, TypeVar
|
|
7
|
+
from urllib.parse import quote
|
|
8
|
+
|
|
9
|
+
import time
|
|
10
|
+
import httpx
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
from .._core import (
|
|
14
|
+
LONGEST_WAIT_S,
|
|
15
|
+
RETRYABLE,
|
|
16
|
+
RUN_TIMEOUT_S,
|
|
17
|
+
GenerationFailed,
|
|
18
|
+
delay_of,
|
|
19
|
+
headers_of,
|
|
20
|
+
is_finished,
|
|
21
|
+
query_of,
|
|
22
|
+
refusal_of,
|
|
23
|
+
settings,
|
|
24
|
+
)
|
|
25
|
+
from ..types import (
|
|
26
|
+
Ad,
|
|
27
|
+
AdList,
|
|
28
|
+
AdSummary,
|
|
29
|
+
Advertiser,
|
|
30
|
+
AdvertiserList,
|
|
31
|
+
Asset,
|
|
32
|
+
Brand,
|
|
33
|
+
Character,
|
|
34
|
+
CharacterList,
|
|
35
|
+
Generation,
|
|
36
|
+
GenerationList,
|
|
37
|
+
LibraryAsset,
|
|
38
|
+
LibraryAssetList,
|
|
39
|
+
Model,
|
|
40
|
+
ModelList,
|
|
41
|
+
Offer,
|
|
42
|
+
OfferList,
|
|
43
|
+
OfferSummary,
|
|
44
|
+
Voice,
|
|
45
|
+
VoiceList,
|
|
46
|
+
WebhookSecret,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
Item = TypeVar("Item", bound=BaseModel)
|
|
50
|
+
|
|
51
|
+
Answer = TypeVar("Answer", bound=BaseModel)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Page(Generic[Item]):
|
|
55
|
+
def __init__(self, client: Sandstorm, path: str, params: Mapping[str, Any], shape: type[BaseModel]) -> None:
|
|
56
|
+
self._client = client
|
|
57
|
+
self._path = path
|
|
58
|
+
self._params = params
|
|
59
|
+
self._shape = shape
|
|
60
|
+
|
|
61
|
+
def first_page(self) -> Any:
|
|
62
|
+
return self._client._send("GET", self._path, self._shape, query=self._params)
|
|
63
|
+
|
|
64
|
+
def __iter__(self) -> Iterator[Item]:
|
|
65
|
+
return self._items()
|
|
66
|
+
|
|
67
|
+
def _items(self) -> Iterator[Item]:
|
|
68
|
+
cursor: str | None = None
|
|
69
|
+
while True:
|
|
70
|
+
page = self._client._send("GET", self._path, self._shape, query={**self._params, "cursor": cursor})
|
|
71
|
+
for item in page.data:
|
|
72
|
+
yield item
|
|
73
|
+
if not page.has_more or page.next_cursor is None:
|
|
74
|
+
return
|
|
75
|
+
cursor = page.next_cursor
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class Generations:
|
|
79
|
+
def __init__(self, client: Sandstorm) -> None:
|
|
80
|
+
self._client = client
|
|
81
|
+
|
|
82
|
+
def create(
|
|
83
|
+
self,
|
|
84
|
+
model: str,
|
|
85
|
+
input: Mapping[str, Any],
|
|
86
|
+
*,
|
|
87
|
+
webhook_url: str | None = None,
|
|
88
|
+
wait: int | None = None,
|
|
89
|
+
idempotency_key: str | None = None,
|
|
90
|
+
) -> Generation:
|
|
91
|
+
body = {"model": model, "input": dict(input), **({"webhook_url": webhook_url} if webhook_url else {})}
|
|
92
|
+
return self._client._send(
|
|
93
|
+
"POST", "/v1/generations", Generation, body=body, wait=wait, idempotency_key=idempotency_key or str(uuid.uuid4())
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def get(self, id: str, *, wait: int | None = None) -> Generation:
|
|
97
|
+
return self._client._send("GET", f"/v1/generations/{quote(id, safe='')}", Generation, wait=wait)
|
|
98
|
+
|
|
99
|
+
def wait(self, id: str, *, timeout: float = RUN_TIMEOUT_S) -> Generation:
|
|
100
|
+
deadline = time.monotonic() + timeout
|
|
101
|
+
while True:
|
|
102
|
+
left = math.ceil(deadline - time.monotonic())
|
|
103
|
+
generation = self.get(id, wait=max(1, min(LONGEST_WAIT_S, left)))
|
|
104
|
+
if is_finished(generation):
|
|
105
|
+
return generation
|
|
106
|
+
if left <= 0:
|
|
107
|
+
raise TimeoutError(f"{id} did not finish within {timeout} s; it is still {generation.status}")
|
|
108
|
+
|
|
109
|
+
def run(
|
|
110
|
+
self,
|
|
111
|
+
model: str,
|
|
112
|
+
input: Mapping[str, Any],
|
|
113
|
+
*,
|
|
114
|
+
webhook_url: str | None = None,
|
|
115
|
+
idempotency_key: str | None = None,
|
|
116
|
+
timeout: float = RUN_TIMEOUT_S,
|
|
117
|
+
) -> Generation:
|
|
118
|
+
created = self.create(model, input, webhook_url=webhook_url, wait=LONGEST_WAIT_S, idempotency_key=idempotency_key)
|
|
119
|
+
generation = created if is_finished(created) else self.wait(created.id, timeout=timeout)
|
|
120
|
+
if generation.status != "succeeded":
|
|
121
|
+
raise GenerationFailed(generation)
|
|
122
|
+
return generation
|
|
123
|
+
|
|
124
|
+
def cancel(self, id: str) -> Generation:
|
|
125
|
+
return self._client._send("POST", f"/v1/generations/{quote(id, safe='')}/cancel", Generation)
|
|
126
|
+
|
|
127
|
+
def list(self, *, status: str | None = None, model: str | None = None, limit: int | None = None) -> Page[Generation]:
|
|
128
|
+
return Page(self._client, "/v1/generations", {"status": status, "model": model, "limit": limit}, GenerationList)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class Models:
|
|
132
|
+
def __init__(self, client: Sandstorm) -> None:
|
|
133
|
+
self._client = client
|
|
134
|
+
|
|
135
|
+
def list(self, *, type: str | None = None) -> Page[Model]:
|
|
136
|
+
return Page(self._client, "/v1/models", {"type": type}, ModelList)
|
|
137
|
+
|
|
138
|
+
def get(self, id: str) -> Model:
|
|
139
|
+
return self._client._send("GET", f"/v1/models/{quote(id, safe='')}", Model)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class Assets:
|
|
143
|
+
def __init__(self, client: Sandstorm) -> None:
|
|
144
|
+
self._client = client
|
|
145
|
+
|
|
146
|
+
def list(
|
|
147
|
+
self,
|
|
148
|
+
*,
|
|
149
|
+
type: str | None = None,
|
|
150
|
+
offer: str | None = None,
|
|
151
|
+
liked: bool | None = None,
|
|
152
|
+
archived: bool | None = None,
|
|
153
|
+
limit: int | None = None,
|
|
154
|
+
) -> Page[LibraryAsset]:
|
|
155
|
+
return Page(self._client, "/v1/assets", {"type": type, "offer": offer, "liked": liked, "archived": archived, "limit": limit}, LibraryAssetList)
|
|
156
|
+
|
|
157
|
+
def get(self, id: str) -> Asset:
|
|
158
|
+
return self._client._send("GET", f"/v1/assets/{quote(id, safe='')}", Asset)
|
|
159
|
+
|
|
160
|
+
def import_url(self, url: str, *, name: str | None = None, idempotency_key: str | None = None) -> Asset:
|
|
161
|
+
body = {"url": url, **({"name": name} if name else {})}
|
|
162
|
+
return self._client._send("POST", "/v1/assets", Asset, body=body, idempotency_key=idempotency_key or str(uuid.uuid4()))
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class Brands:
|
|
166
|
+
def __init__(self, client: Sandstorm) -> None:
|
|
167
|
+
self._client = client
|
|
168
|
+
|
|
169
|
+
def get(self) -> Brand:
|
|
170
|
+
return self._client._send("GET", "/v1/brand", Brand)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class Offers:
|
|
174
|
+
def __init__(self, client: Sandstorm) -> None:
|
|
175
|
+
self._client = client
|
|
176
|
+
|
|
177
|
+
def list(self, *, query: str | None = None, limit: int | None = None) -> Page[OfferSummary]:
|
|
178
|
+
return Page(self._client, "/v1/offers", {"query": query, "limit": limit}, OfferList)
|
|
179
|
+
|
|
180
|
+
def get(self, id: str) -> Offer:
|
|
181
|
+
return self._client._send("GET", f"/v1/offers/{quote(id, safe='')}", Offer)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class Characters:
|
|
185
|
+
def __init__(self, client: Sandstorm) -> None:
|
|
186
|
+
self._client = client
|
|
187
|
+
|
|
188
|
+
def list(self, *, query: str | None = None, kind: str | None = None) -> Page[Character]:
|
|
189
|
+
return Page(self._client, "/v1/characters", {"query": query, "kind": kind}, CharacterList)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class Voices:
|
|
193
|
+
def __init__(self, client: Sandstorm) -> None:
|
|
194
|
+
self._client = client
|
|
195
|
+
|
|
196
|
+
def list(
|
|
197
|
+
self,
|
|
198
|
+
*,
|
|
199
|
+
query: str | None = None,
|
|
200
|
+
gender: str | None = None,
|
|
201
|
+
age: str | None = None,
|
|
202
|
+
accent: str | None = None,
|
|
203
|
+
use_case: str | None = None,
|
|
204
|
+
limit: int | None = None,
|
|
205
|
+
) -> Page[Voice]:
|
|
206
|
+
params = {"query": query, "gender": gender, "age": age, "accent": accent, "use_case": use_case, "limit": limit}
|
|
207
|
+
return Page(self._client, "/v1/voices", params, VoiceList)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class Advertisers:
|
|
211
|
+
def __init__(self, client: Sandstorm) -> None:
|
|
212
|
+
self._client = client
|
|
213
|
+
|
|
214
|
+
def list(self, *, query: str, limit: int | None = None) -> Page[Advertiser]:
|
|
215
|
+
return Page(self._client, "/v1/advertisers", {"query": query, "limit": limit}, AdvertiserList)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class Ads:
|
|
219
|
+
def __init__(self, client: Sandstorm) -> None:
|
|
220
|
+
self._client = client
|
|
221
|
+
|
|
222
|
+
def list(
|
|
223
|
+
self,
|
|
224
|
+
*,
|
|
225
|
+
query: str | None = None,
|
|
226
|
+
advertiser: str | None = None,
|
|
227
|
+
platform: str | None = None,
|
|
228
|
+
format: str | None = None,
|
|
229
|
+
status: str | None = None,
|
|
230
|
+
min_active_days: int | None = None,
|
|
231
|
+
started_after: str | None = None,
|
|
232
|
+
started_before: str | None = None,
|
|
233
|
+
limit: int | None = None,
|
|
234
|
+
) -> Page[AdSummary]:
|
|
235
|
+
params = {
|
|
236
|
+
"query": query,
|
|
237
|
+
"advertiser": advertiser,
|
|
238
|
+
"platform": platform,
|
|
239
|
+
"format": format,
|
|
240
|
+
"status": status,
|
|
241
|
+
"min_active_days": min_active_days,
|
|
242
|
+
"started_after": started_after,
|
|
243
|
+
"started_before": started_before,
|
|
244
|
+
"limit": limit,
|
|
245
|
+
}
|
|
246
|
+
return Page(self._client, "/v1/ads", params, AdList)
|
|
247
|
+
|
|
248
|
+
def get(self, id: str) -> Ad:
|
|
249
|
+
return self._client._send("GET", f"/v1/ads/{quote(id, safe='')}", Ad)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class Webhooks:
|
|
253
|
+
def __init__(self, client: Sandstorm) -> None:
|
|
254
|
+
self._client = client
|
|
255
|
+
|
|
256
|
+
def secret(self) -> str:
|
|
257
|
+
return (self._client._send("GET", "/v1/webhooks/secret", WebhookSecret)).secret
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class Sandstorm:
|
|
261
|
+
def __init__(
|
|
262
|
+
self,
|
|
263
|
+
*,
|
|
264
|
+
api_key: str | None = None,
|
|
265
|
+
base_url: str | None = None,
|
|
266
|
+
brand: str | None = None,
|
|
267
|
+
max_retries: int = 2,
|
|
268
|
+
timeout: float = 60,
|
|
269
|
+
http: httpx.Client | None = None,
|
|
270
|
+
) -> None:
|
|
271
|
+
key, url = settings(api_key, base_url)
|
|
272
|
+
self._base_url = url
|
|
273
|
+
self._headers = headers_of(key, brand)
|
|
274
|
+
self._max_retries = max_retries
|
|
275
|
+
self._timeout = timeout
|
|
276
|
+
self._http = http or httpx.Client()
|
|
277
|
+
self.generations = Generations(self)
|
|
278
|
+
self.models = Models(self)
|
|
279
|
+
self.assets = Assets(self)
|
|
280
|
+
self.brand = Brands(self)
|
|
281
|
+
self.offers = Offers(self)
|
|
282
|
+
self.characters = Characters(self)
|
|
283
|
+
self.voices = Voices(self)
|
|
284
|
+
self.advertisers = Advertisers(self)
|
|
285
|
+
self.ads = Ads(self)
|
|
286
|
+
self.webhooks = Webhooks(self)
|
|
287
|
+
|
|
288
|
+
def __enter__(self) -> Sandstorm:
|
|
289
|
+
return self
|
|
290
|
+
|
|
291
|
+
def __exit__(self, *_: object) -> None:
|
|
292
|
+
self.close()
|
|
293
|
+
|
|
294
|
+
def close(self) -> None:
|
|
295
|
+
self._http.close()
|
|
296
|
+
|
|
297
|
+
def _send(
|
|
298
|
+
self,
|
|
299
|
+
method: str,
|
|
300
|
+
path: str,
|
|
301
|
+
shape: type[Answer],
|
|
302
|
+
*,
|
|
303
|
+
query: Mapping[str, Any] | None = None,
|
|
304
|
+
body: Mapping[str, Any] | None = None,
|
|
305
|
+
wait: int | None = None,
|
|
306
|
+
idempotency_key: str | None = None,
|
|
307
|
+
) -> Answer:
|
|
308
|
+
headers = {
|
|
309
|
+
**self._headers,
|
|
310
|
+
**({"prefer": f"wait={wait}"} if wait is not None else {}),
|
|
311
|
+
**({"idempotency-key": idempotency_key} if idempotency_key is not None else {}),
|
|
312
|
+
}
|
|
313
|
+
attempt = 0
|
|
314
|
+
while True:
|
|
315
|
+
try:
|
|
316
|
+
response = self._http.request(
|
|
317
|
+
method,
|
|
318
|
+
self._base_url + path,
|
|
319
|
+
params=query_of(query or {}),
|
|
320
|
+
json=body,
|
|
321
|
+
headers=headers,
|
|
322
|
+
timeout=self._timeout + (wait or 0),
|
|
323
|
+
)
|
|
324
|
+
except httpx.TransportError:
|
|
325
|
+
if attempt >= self._max_retries:
|
|
326
|
+
raise
|
|
327
|
+
time.sleep(delay_of(attempt, None))
|
|
328
|
+
attempt += 1
|
|
329
|
+
continue
|
|
330
|
+
if response.is_success:
|
|
331
|
+
return shape.model_validate(response.json())
|
|
332
|
+
if response.status_code not in RETRYABLE or attempt >= self._max_retries:
|
|
333
|
+
raise refusal_of(response)
|
|
334
|
+
time.sleep(delay_of(attempt, response))
|
|
335
|
+
attempt += 1
|
ads_dev/py.typed
ADDED
|
File without changes
|