ads_dev 1.0.0__tar.gz → 1.1.0__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: ads_dev
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: The ads.dev API for Python: image, video and audio generation on your brand, the brand kit and library, and competitor ads
5
5
  Keywords: ads.dev,sandstorm,ads,image generation,video generation,ai,api
6
6
  License-Expression: MIT
@@ -153,6 +153,7 @@ uv sync
153
153
  uv run pytest
154
154
  uv run python scripts/generate.py
155
155
  uv build
156
+ uv run python scripts/aliases.py
156
157
  ```
157
158
 
158
- `ads_dev/types.py` is generated from `../sdk/openapi.json`, the API's own document, and `ads_dev/_sync/client.py` from the async client with `unasync`; `scripts/generate.py` writes both, and the tests fail when either is stale. Build with uv 0.13 or newer.
159
+ `ads_dev/types.py` is generated from `../sdk/openapi.json`, the API's own document, and `ads_dev/_sync/client.py` from the async client with `unasync`; `scripts/generate.py` writes both, and the tests fail when either is stale. `scripts/aliases.py` builds `adsdotdev` and `sandstormapp`, which install `ads_dev` under those names, into `dist/` beside it, so `uv publish` uploads all three. Build with uv 0.13 or newer.
@@ -134,6 +134,7 @@ uv sync
134
134
  uv run pytest
135
135
  uv run python scripts/generate.py
136
136
  uv build
137
+ uv run python scripts/aliases.py
137
138
  ```
138
139
 
139
- `ads_dev/types.py` is generated from `../sdk/openapi.json`, the API's own document, and `ads_dev/_sync/client.py` from the async client with `unasync`; `scripts/generate.py` writes both, and the tests fail when either is stale. Build with uv 0.13 or newer.
140
+ `ads_dev/types.py` is generated from `../sdk/openapi.json`, the API's own document, and `ads_dev/_sync/client.py` from the async client with `unasync`; `scripts/generate.py` writes both, and the tests fail when either is stale. `scripts/aliases.py` builds `adsdotdev` and `sandstormapp`, which install `ads_dev` under those names, into `dist/` beside it, so `uv publish` uploads all three. Build with uv 0.13 or newer.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ads_dev"
3
- version = "1.0.0"
3
+ version = "1.1.0"
4
4
  description = "The ads.dev API for Python: image, video and audio generation on your brand, the brand kit and library, and competitor ads"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ads_dev"
3
- version = "1.0.0"
3
+ version = "1.1.0"
4
4
  description = "The ads.dev API for Python: image, video and audio generation on your brand, the brand kit and library, and competitor ads"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -15,6 +15,7 @@ from .._core import (
15
15
  RETRYABLE,
16
16
  RUN_TIMEOUT_S,
17
17
  GenerationFailed,
18
+ answer_of,
18
19
  delay_of,
19
20
  headers_of,
20
21
  is_finished,
@@ -328,8 +329,8 @@ class AsyncSandstorm:
328
329
  attempt += 1
329
330
  continue
330
331
  if response.is_success:
331
- return shape.model_validate(response.json())
332
+ return shape.model_validate(answer_of(response, self._base_url))
332
333
  if response.status_code not in RETRYABLE or attempt >= self._max_retries:
333
- raise refusal_of(response)
334
+ raise refusal_of(response, self._base_url)
334
335
  await anyio.sleep(delay_of(attempt, response))
335
336
  attempt += 1
@@ -24,12 +24,6 @@ RETRYABLE = frozenset({408, 429, 500, 502, 503, 504})
24
24
 
25
25
  FINISHED = frozenset({"succeeded", "failed", "canceled"})
26
26
 
27
- MISSING_KEY = (
28
- "Pass api_key or set SANDSTORM_API_KEY. To make a key: "
29
- "curl -fsSL https://ads.dev/login.mjs -o sandstorm-login.mjs && node sandstorm-login.mjs"
30
- )
31
-
32
-
33
27
  class SandstormError(Exception):
34
28
  def __init__(self, status: int, refusal: Mapping[str, Any]) -> None:
35
29
  super().__init__(refusal.get("message", ""))
@@ -51,10 +45,14 @@ def is_finished(generation: Generation) -> bool:
51
45
 
52
46
 
53
47
  def settings(api_key: str | None, base_url: str | None) -> tuple[str, str]:
48
+ url = (base_url or os.environ.get("SANDSTORM_API_URL") or BASE_URL).rstrip("/")
54
49
  key = api_key or os.environ.get("SANDSTORM_API_KEY")
55
50
  if not key:
56
- raise ValueError(MISSING_KEY)
57
- return key, (base_url or os.environ.get("SANDSTORM_API_URL") or BASE_URL).rstrip("/")
51
+ raise ValueError(
52
+ "Pass api_key or set SANDSTORM_API_KEY. To make a key: "
53
+ f"curl -fsSL {url}/login.mjs -o sandstorm-login.mjs && node sandstorm-login.mjs"
54
+ )
55
+ return key, url
58
56
 
59
57
 
60
58
  def headers_of(api_key: str, brand: str | None) -> dict[str, str]:
@@ -65,14 +63,32 @@ def query_of(params: Mapping[str, Any]) -> dict[str, str]:
65
63
  return {name: ("true" if value is True else "false" if value is False else str(value)) for name, value in params.items() if value is not None}
66
64
 
67
65
 
68
- def refusal_of(response: httpx.Response) -> SandstormError:
69
- try:
70
- return SandstormError(response.status_code, response.json()["error"])
71
- except (ValueError, KeyError, TypeError):
72
- return SandstormError(
73
- response.status_code,
74
- {"message": response.text or response.reason_phrase, "request_id": response.headers.get("sandstorm-request-id")},
75
- )
66
+ def is_json(response: httpx.Response) -> bool:
67
+ return "json" in response.headers.get("content-type", "")
68
+
69
+
70
+ def not_the_api(response: httpx.Response, base_url: str) -> SandstormError:
71
+ kind = response.headers.get("content-type", "no content type")
72
+ return SandstormError(
73
+ response.status_code,
74
+ {
75
+ "code": "not_the_api",
76
+ "message": f"{base_url} answered {response.status_code} with {kind} instead of the API's JSON. "
77
+ f"If {base_url} is not the Sandstorm API, pass base_url or set SANDSTORM_API_URL.",
78
+ },
79
+ )
80
+
81
+
82
+ def answer_of(response: httpx.Response, base_url: str) -> Any:
83
+ if not is_json(response):
84
+ raise not_the_api(response, base_url)
85
+ return response.json()
86
+
87
+
88
+ def refusal_of(response: httpx.Response, base_url: str) -> SandstormError:
89
+ if not is_json(response):
90
+ return not_the_api(response, base_url)
91
+ return SandstormError(response.status_code, response.json()["error"])
76
92
 
77
93
 
78
94
  def delay_of(attempt: int, response: httpx.Response | None) -> float:
@@ -15,6 +15,7 @@ from .._core import (
15
15
  RETRYABLE,
16
16
  RUN_TIMEOUT_S,
17
17
  GenerationFailed,
18
+ answer_of,
18
19
  delay_of,
19
20
  headers_of,
20
21
  is_finished,
@@ -328,8 +329,8 @@ class Sandstorm:
328
329
  attempt += 1
329
330
  continue
330
331
  if response.is_success:
331
- return shape.model_validate(response.json())
332
+ return shape.model_validate(answer_of(response, self._base_url))
332
333
  if response.status_code not in RETRYABLE or attempt >= self._max_retries:
333
- raise refusal_of(response)
334
+ raise refusal_of(response, self._base_url)
334
335
  time.sleep(delay_of(attempt, response))
335
336
  attempt += 1
@@ -4,7 +4,6 @@
4
4
  from __future__ import annotations
5
5
 
6
6
  from typing import Any, Literal
7
- from uuid import UUID
8
7
 
9
8
  from pydantic import (
10
9
  AnyUrl,
@@ -54,7 +53,7 @@ class CreateNanoBananaPro(BaseModel):
54
53
  class CreateNanoBananaProEditInput(BaseModel):
55
54
  prompt: constr(min_length=3, max_length=50000) = Field(
56
55
  ...,
57
- description="The change to make, or how the images come together, naming each image by its place in images.",
56
+ description="The change to make, or how the images come together, citing each image by its place in images as @image1, @image2.",
58
57
  )
59
58
  aspect_ratio: (
60
59
  Literal[
@@ -80,7 +79,7 @@ class CreateNanoBananaProEditInput(BaseModel):
80
79
  )
81
80
  images: list[str] = Field(
82
81
  ...,
83
- description="The 1 to 14 images to edit or combine, in the order the prompt names them, each an asset id (asset_…) or an https URL.",
82
+ description="The 1 to 14 images to edit or combine, in the order the prompt cites them, each an asset id (asset_…) or an https URL.",
84
83
  max_length=14,
85
84
  min_length=1,
86
85
  )
@@ -157,7 +156,7 @@ class CreateNanoBanana2(BaseModel):
157
156
  class CreateNanoBanana2EditInput(BaseModel):
158
157
  prompt: constr(min_length=3, max_length=50000) = Field(
159
158
  ...,
160
- description="The change to make, or how the images come together, naming each image by its place in images.",
159
+ description="The change to make, or how the images come together, citing each image by its place in images as @image1, @image2.",
161
160
  )
162
161
  aspect_ratio: (
163
162
  Literal[
@@ -187,7 +186,7 @@ class CreateNanoBanana2EditInput(BaseModel):
187
186
  )
188
187
  images: list[str] = Field(
189
188
  ...,
190
- description="The 1 to 14 images to edit or combine, in the order the prompt names them, each an asset id (asset_…) or an https URL.",
189
+ description="The 1 to 14 images to edit or combine, in the order the prompt cites them, each an asset id (asset_…) or an https URL.",
191
190
  max_length=14,
192
191
  min_length=1,
193
192
  )
@@ -261,7 +260,7 @@ class CreateNanoBanana2Lite(BaseModel):
261
260
  class CreateNanoBanana2LiteEditInput(BaseModel):
262
261
  prompt: constr(min_length=3, max_length=50000) = Field(
263
262
  ...,
264
- description="The change to make, or how the images come together, naming each image by its place in images.",
263
+ description="The change to make, or how the images come together, citing each image by its place in images as @image1, @image2.",
265
264
  )
266
265
  aspect_ratio: (
267
266
  Literal[
@@ -288,7 +287,7 @@ class CreateNanoBanana2LiteEditInput(BaseModel):
288
287
  )
289
288
  images: list[str] = Field(
290
289
  ...,
291
- description="The 1 to 14 images to edit or combine, in the order the prompt names them, each an asset id (asset_…) or an https URL.",
290
+ description="The 1 to 14 images to edit or combine, in the order the prompt cites them, each an asset id (asset_…) or an https URL.",
292
291
  max_length=14,
293
292
  min_length=1,
294
293
  )
@@ -363,7 +362,7 @@ class CreateGptImage25Flare(BaseModel):
363
362
  class CreateGptImage25FlareEditInput(BaseModel):
364
363
  prompt: constr(min_length=1, max_length=32000) = Field(
365
364
  ...,
366
- description="The change to make, or how the images come together, naming each image by its place in images and writing out exactly any words the image must show.",
365
+ description="The change to make, or how the images come together, citing each image by its place in images as @image1, @image2 and writing out exactly any words the image must show.",
367
366
  )
368
367
  aspect_ratio: (
369
368
  Literal[
@@ -403,7 +402,7 @@ class CreateGptImage25FlareEditInput(BaseModel):
403
402
  )
404
403
  images: list[str] = Field(
405
404
  ...,
406
- description="The 1 to 16 images to edit or combine, in the order the prompt names them, each an asset id (asset_…) or an https URL.",
405
+ description="The 1 to 16 images to edit or combine, in the order the prompt cites them, each an asset id (asset_…) or an https URL.",
407
406
  max_length=16,
408
407
  min_length=1,
409
408
  )
@@ -478,7 +477,7 @@ class CreateGptImage25Sunburst(BaseModel):
478
477
  class CreateGptImage25SunburstEditInput(BaseModel):
479
478
  prompt: constr(min_length=1, max_length=32000) = Field(
480
479
  ...,
481
- description="The change to make, or how the images come together, naming each image by its place in images and writing out exactly any words the image must show.",
480
+ description="The change to make, or how the images come together, citing each image by its place in images as @image1, @image2 and writing out exactly any words the image must show.",
482
481
  )
483
482
  aspect_ratio: (
484
483
  Literal[
@@ -518,7 +517,7 @@ class CreateGptImage25SunburstEditInput(BaseModel):
518
517
  )
519
518
  images: list[str] = Field(
520
519
  ...,
521
- description="The 1 to 16 images to edit or combine, in the order the prompt names them, each an asset id (asset_…) or an https URL.",
520
+ description="The 1 to 16 images to edit or combine, in the order the prompt cites them, each an asset id (asset_…) or an https URL.",
522
521
  max_length=16,
523
522
  min_length=1,
524
523
  )
@@ -581,7 +580,7 @@ class CreateSeedream5Pro(BaseModel):
581
580
  class CreateSeedream5ProEditInput(BaseModel):
582
581
  prompt: constr(min_length=3) = Field(
583
582
  ...,
584
- description="The change to make as one paragraph, at most 600 words, naming each image by its place in images (image 1, image 2), with the exact words the result must show written out.",
583
+ description="The change to make as one paragraph, at most 600 words, citing each image by its place in images as @image1, @image2, with the exact words the result must show written out.",
585
584
  )
586
585
  aspect_ratio: (
587
586
  Literal[
@@ -608,7 +607,7 @@ class CreateSeedream5ProEditInput(BaseModel):
608
607
  )
609
608
  images: list[str] = Field(
610
609
  ...,
611
- description="The images to edit or combine, in the order the prompt names them, at most 10, each an asset id (asset_…) or an https URL.",
610
+ description="The images to edit or combine, in the order the prompt cites them, at most 10, each an asset id (asset_…) or an https URL.",
612
611
  max_length=10,
613
612
  min_length=1,
614
613
  )
@@ -664,7 +663,7 @@ class CreateSeedream5Lite(BaseModel):
664
663
  class CreateSeedream5LiteEditInput(BaseModel):
665
664
  prompt: constr(min_length=3) = Field(
666
665
  ...,
667
- description="The change to make as one paragraph, at most 600 words, naming each image by its place in images (image 1, image 2), with the exact words the result must show written out.",
666
+ description="The change to make as one paragraph, at most 600 words, citing each image by its place in images as @image1, @image2, with the exact words the result must show written out.",
668
667
  )
669
668
  aspect_ratio: (
670
669
  Literal[
@@ -688,7 +687,7 @@ class CreateSeedream5LiteEditInput(BaseModel):
688
687
  )
689
688
  images: list[str] = Field(
690
689
  ...,
691
- description="The images to edit or combine, in the order the prompt names them, at most 14, each an asset id (asset_…) or an https URL.",
690
+ description="The images to edit or combine, in the order the prompt cites them, at most 14, each an asset id (asset_…) or an https URL.",
692
691
  max_length=14,
693
692
  min_length=1,
694
693
  )
@@ -898,7 +897,7 @@ class CreateSeedance25ImageToVideo(BaseModel):
898
897
  class CreateSeedance25ReferenceToVideoInput(BaseModel):
899
898
  prompt: constr(min_length=3, max_length=50000) = Field(
900
899
  ...,
901
- description="What happens in the clip, citing each image, video or audio by its place in its list as @Image1, @Video1 or @Audio1.",
900
+ description="What happens in the clip, citing each image, video or audio by its place in its list as @image1, @video1 or @audio1.",
902
901
  )
903
902
  task: Literal["auto", "reference", "editing", "extension"] | None = Field(
904
903
  "auto",
@@ -911,12 +910,12 @@ class CreateSeedance25ReferenceToVideoInput(BaseModel):
911
910
  )
912
911
  videos: list[str] | None = Field(
913
912
  [],
914
- description="Up to 10 videos, 30 seconds together at most, each an asset id (asset_…) or an https URL; none when left out.",
913
+ description="Up to 10 videos, each an asset id (asset_…) or an https URL; none when left out. Past 30 seconds together the longer ones are cut to their opening seconds, and beside audio their own sound is left out; editing and extension take them whole, so those must run 30 seconds or less together.",
915
914
  max_length=10,
916
915
  )
917
916
  audio: list[str] | None = Field(
918
917
  [],
919
- description="Up to 10 wav or mp3 files, 30 seconds together at most, each an asset id (asset_…) or an https URL; none when left out.",
918
+ description="Up to 10 audio files, each an asset id (asset_…) or an https URL; none when left out. Past 30 seconds together the longer ones are cut to their opening seconds.",
920
919
  max_length=10,
921
920
  )
922
921
  aspect_ratio: (
@@ -1095,7 +1094,7 @@ class CreateKlingO3ProReferenceToVideoInputElements1(BaseModel):
1095
1094
  )
1096
1095
  video: str = Field(
1097
1096
  ...,
1098
- description="A clip of the character or object, 3 to 10 seconds at 720 to 2160 px a side, as an asset id (asset_…) or an https URL, in place of images.",
1097
+ description="A clip of the character or object of at least 3 seconds, as an asset id (asset_…) or an https URL, in place of images; past 10 seconds it is cut to its opening 10, and outside 720 to 2160 px a side it is resized to fit.",
1099
1098
  )
1100
1099
 
1101
1100
 
@@ -1121,7 +1120,7 @@ class CreateKlingO3ProReferenceToVideoInput(BaseModel):
1121
1120
  )
1122
1121
  images: list[str] | None = Field(
1123
1122
  [],
1124
- description="Up to 4 images whose subject or look the clip keeps, each an asset id (asset_…) or an https URL, cited in the prompt as @Image1, @Image2 in this order; none when left out.",
1123
+ description="Up to 4 images whose subject or look the clip keeps, each an asset id (asset_…) or an https URL, cited in the prompt as @image1, @image2 in this order; none when left out.",
1125
1124
  max_length=4,
1126
1125
  )
1127
1126
  elements: (
@@ -1129,7 +1128,7 @@ class CreateKlingO3ProReferenceToVideoInput(BaseModel):
1129
1128
  CreateKlingO3ProReferenceToVideoInputElements | CreateKlingO3ProReferenceToVideoInputElements1
1130
1129
  ]
1131
1130
  | None
1132
- ) = Field([], description="Characters or objects held to one look across the clip, cited in the prompt as @Element1, @Element2 in this order; images and elements take 4 together at most, and none when left out.", max_length=4, validate_default=True)
1131
+ ) = Field([], description="Characters or objects held to one look across the clip, cited in the prompt as @element1, @element2 in this order; images and elements take 4 together at most, and none when left out.", max_length=4, validate_default=True)
1133
1132
  aspect_ratio: Literal["16:9", "9:16", "1:1"] | None = Field(
1134
1133
  "16:9", description="Shape of the clip as width:height; 16:9 when left out."
1135
1134
  )
@@ -1242,7 +1241,7 @@ class CreateGeminiOmniFlashImageToVideo(BaseModel):
1242
1241
  class CreateGeminiOmniFlashReferenceToVideoInput(BaseModel):
1243
1242
  prompt: constr(min_length=3, max_length=20000) = Field(
1244
1243
  ...,
1245
- description="What happens in the clip, citing each image or video inline by its place in its list from 0, as <IMAGE_REF_0> or <VIDEO_REF_0>, with any spoken line in double quotes.",
1244
+ description="What happens in the clip, citing each image or video by its place in its list as @image1 or @video1, with any spoken line in double quotes.",
1246
1245
  )
1247
1246
  images: list[str] | None = Field(
1248
1247
  [],
@@ -1251,7 +1250,7 @@ class CreateGeminiOmniFlashReferenceToVideoInput(BaseModel):
1251
1250
  )
1252
1251
  videos: list[str] | None = Field(
1253
1252
  [],
1254
- description="Up to 3 videos, each shorter than 3 seconds, for a likeness or a motion to copy, whose sound is ignored, each an asset id (asset_…) or an https URL; none when left out.",
1253
+ description="Up to 3 videos for a likeness or a motion to copy, whose sound is ignored, each an asset id (asset_…) or an https URL and cut to its opening 3 seconds when longer; none when left out.",
1255
1254
  max_length=3,
1256
1255
  )
1257
1256
  aspect_ratio: Literal["9:16", "16:9"] | None = Field(
@@ -1575,8 +1574,8 @@ class GenerationOutputItem(BaseModel):
1575
1574
  name: str | None
1576
1575
  url: str
1577
1576
  content_type: str
1578
- width: float | None = None
1579
- height: float | None = None
1577
+ width: int | None = None
1578
+ height: int | None = None
1580
1579
  duration: float | None = None
1581
1580
  role: Literal["main", "last_frame", "layer"]
1582
1581
  layer: GenerationOutputItemLayer | None = None
@@ -1619,7 +1618,11 @@ class ModelPricingRate(BaseModel):
1619
1618
  usd: float
1620
1619
  when: dict[str, Any] = Field(
1621
1620
  ...,
1622
- description="The input settings the rate applies to; empty when it applies to all",
1621
+ description="The inputs, by their names in input, that this rate applies to; a media input is true when given and false when not; empty when it applies to all. A rate charged on the input itself carries `input`: the tokens of text or image, or true for an input file it charges for",
1622
+ )
1623
+ default: bool = Field(
1624
+ ...,
1625
+ description="Whether a generation that leaves these inputs at their defaults pays this rate",
1623
1626
  )
1624
1627
 
1625
1628
 
@@ -1674,8 +1677,8 @@ class LibraryAsset(BaseModel):
1674
1677
  name: str | None
1675
1678
  url: str | None
1676
1679
  content_type: str | None
1677
- width: float | None = None
1678
- height: float | None = None
1680
+ width: int | None = None
1681
+ height: int | None = None
1679
1682
  duration: float | None = None
1680
1683
  caption: str | None = Field(
1681
1684
  ..., description="What the image shows, when it has been described"
@@ -1694,8 +1697,8 @@ class Asset(BaseModel):
1694
1697
  name: str | None
1695
1698
  url: str | None
1696
1699
  content_type: str | None
1697
- width: float | None = None
1698
- height: float | None = None
1700
+ width: int | None = None
1701
+ height: int | None = None
1699
1702
  duration: float | None = None
1700
1703
 
1701
1704
 
@@ -1751,14 +1754,14 @@ class BrandLogo(BaseModel):
1751
1754
  description="icon for a small mark, light for dark backgrounds, dark for light ones",
1752
1755
  )
1753
1756
  format: str | None
1754
- width: float | None
1755
- height: float | None
1757
+ width: int | None
1758
+ height: int | None
1756
1759
  url: str | None
1757
1760
 
1758
1761
 
1759
1762
  class Brand(BaseModel):
1760
1763
  object: Literal["brand"]
1761
- id: UUID = Field(
1764
+ id: str = Field(
1762
1765
  ...,
1763
1766
  description="Send it as `Sandstorm-Brand` to work on this brand with a key made on another",
1764
1767
  )
@@ -1797,8 +1800,8 @@ class OfferSummary(BaseModel):
1797
1800
  class OfferVariantPhoto(BaseModel):
1798
1801
  id: str = Field(..., description="An asset id, usable as a media input")
1799
1802
  url: str | None
1800
- width: float | None
1801
- height: float | None
1803
+ width: int | None
1804
+ height: int | None
1802
1805
  orientation: str | None
1803
1806
  shot_type: str | None
1804
1807
  caption: str | None
@@ -1819,8 +1822,8 @@ class OfferVariant(BaseModel):
1819
1822
  class OfferPhoto(BaseModel):
1820
1823
  id: str = Field(..., description="An asset id, usable as a media input")
1821
1824
  url: str | None
1822
- width: float | None
1823
- height: float | None
1825
+ width: int | None
1826
+ height: int | None
1824
1827
  orientation: str | None
1825
1828
  shot_type: str | None
1826
1829
  caption: str | None
File without changes
File without changes
File without changes