stackshift 0.1.1__py3-none-any.whl → 0.1.2__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.
stackshift/assets.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
+ import hashlib
4
5
  from pathlib import Path
5
6
  from typing import Any, BinaryIO
6
7
  from urllib.parse import quote
@@ -66,7 +67,7 @@ class AssetsClient:
66
67
  "key": key,
67
68
  "folder": folder,
68
69
  "visibility": visibility,
69
- "cacheControl": cache_control,
70
+ "cache_control": cache_control,
70
71
  "metadata": json.dumps(metadata) if metadata else None,
71
72
  })
72
73
 
@@ -77,21 +78,25 @@ class AssetsClient:
77
78
  def get(self, asset_id: str) -> dict[str, Any]:
78
79
  return self._client.request("GET", f"/assets/{asset_id}")
79
80
 
80
- def delete(self, asset_id: str) -> dict[str, Any]:
81
- return self._client.request("DELETE", f"/assets/{asset_id}")
81
+ def delete(self, asset_id: str, revision: int, *, idempotency_key: str | None = None) -> dict[str, Any]:
82
+ headers = {"If-Match": f'"{revision}"'}
83
+ if idempotency_key:
84
+ headers["Idempotency-Key"] = idempotency_key
85
+ return self._client.request("DELETE", f"/assets/{asset_id}", headers=headers)
82
86
 
83
87
  def replace(
84
88
  self,
85
89
  asset_id: str,
90
+ revision: int,
86
91
  file: str | Path | BinaryIO,
87
92
  *,
88
93
  cache_control: str | None = None,
89
94
  metadata: dict[str, Any] | None = None,
90
95
  ) -> dict[str, Any]:
91
96
  return self._multipart("PUT", f"/assets/{asset_id}/replace", file, {
92
- "cacheControl": cache_control,
97
+ "cache_control": cache_control,
93
98
  "metadata": json.dumps(metadata) if metadata else None,
94
- })
99
+ }, headers={"If-Match": f'"{revision}"'})
95
100
 
96
101
  def signed_url(
97
102
  self,
@@ -101,16 +106,18 @@ class AssetsClient:
101
106
  max_downloads: int | None = None,
102
107
  ) -> dict[str, Any]:
103
108
  return self._client.request("POST", f"/assets/{asset_id}/signed-url", json={
104
- "expiresIn": expires_in,
105
- "maxDownloads": max_downloads,
109
+ "expires_in": expires_in,
110
+ "max_downloads": max_downloads,
106
111
  })
107
112
 
108
113
  def signed_upload_url(self, **payload: Any) -> dict[str, Any]:
109
114
  return self.create_upload_session(**payload)
110
115
 
111
116
  def create_upload_session(self, **payload: Any) -> dict[str, Any]:
117
+ idempotency_key = payload.pop("idempotency_key", None)
112
118
  clean = {key: value for key, value in payload.items() if value not in (None, "")}
113
- return self._client.request("POST", "/assets/upload-sessions", json=clean)
119
+ headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
120
+ return self._client.request("POST", "/assets/upload-sessions", json=clean, headers=headers)
114
121
 
115
122
  def create_chunked_upload_session(self, **payload: Any) -> dict[str, Any]:
116
123
  return self.create_upload_session(mode="chunked", **payload)
@@ -126,10 +133,11 @@ class AssetsClient:
126
133
  origin = self._client.base_url.removesuffix("/api/v1")
127
134
  target = f"{origin}/uploads/{quote(upload_url_or_token)}"
128
135
 
136
+ raw = chunk if isinstance(chunk, bytes) else chunk.read()
129
137
  response = self._client.session.put(
130
138
  f"{target}/parts/{part_number}",
131
- data=chunk,
132
- headers={"User-Agent": "StackShift-Python/0.1"},
139
+ data=raw,
140
+ headers={"User-Agent": "StackShift-Python/0.1", "X-Content-SHA256": hashlib.sha256(raw).hexdigest()},
133
141
  )
134
142
  payload = response.json() if response.content else None
135
143
  if response.status_code < 200 or response.status_code >= 300:
@@ -146,6 +154,27 @@ class AssetsClient:
146
154
  def cancel_upload_session(self, session_id: str) -> dict[str, Any]:
147
155
  return self._client.request("DELETE", f"/assets/upload-sessions/{session_id}")
148
156
 
157
+ def list_buckets(self) -> dict[str, Any]:
158
+ return self._client.request("GET", "/assets/buckets")
159
+
160
+ def create_bucket(self, **payload: Any) -> dict[str, Any]:
161
+ return self._client.request("POST", "/assets/buckets", json=payload)
162
+
163
+ def update_bucket(self, bucket_id: str, revision: int, **payload: Any) -> dict[str, Any]:
164
+ return self._client.request("PUT", f"/assets/buckets/{bucket_id}", json=payload, headers={"If-Match": f'"{revision}"'})
165
+
166
+ def delete_bucket(self, bucket_id: str, revision: int) -> dict[str, Any]:
167
+ return self._client.request("DELETE", f"/assets/buckets/{bucket_id}", headers={"If-Match": f'"{revision}"'})
168
+
169
+ def set_tags(self, asset_id: str, revision: int, tags: list[str]) -> dict[str, Any]:
170
+ return self._client.request("PUT", f"/assets/{asset_id}/tags", json={"tags": tags}, headers={"If-Match": f'"{revision}"'})
171
+
172
+ def patch_metadata(self, asset_id: str, revision: int, metadata: dict[str, Any]) -> dict[str, Any]:
173
+ return self._client.request("PATCH", f"/assets/{asset_id}/metadata", json={"metadata": metadata}, headers={"If-Match": f'"{revision}"'})
174
+
175
+ def patch(self, asset_id: str, revision: int, **payload: Any) -> dict[str, Any]:
176
+ return self._client.request("PATCH", f"/assets/{asset_id}", json=payload, headers={"If-Match": f'"{revision}"'})
177
+
149
178
  def url(self, asset_id: str, **options: Any) -> str:
150
179
  spec = _transform_spec(options)
151
180
  return f"{self._client.cdn_base_url}/assets/{quote(asset_id)}/tr/{quote(spec)}"
@@ -153,18 +182,6 @@ class AssetsClient:
153
182
  def named_url(self, asset_id: str, name: str) -> str:
154
183
  return f"{self._client.cdn_base_url}/t/{quote(name)}/assets/{quote(asset_id)}"
155
184
 
156
- def video_url(
157
- self,
158
- asset_id: str,
159
- kind: str,
160
- profile: str = "default",
161
- token: str | None = None,
162
- ) -> str:
163
- prefix = "/private/assets" if token else "/assets"
164
- base = f"{self._client.cdn_base_url}{prefix}/{quote(asset_id)}"
165
- result = f"{base}/video/{quote(kind)}/{quote(profile)}"
166
- return f"{result}?token={quote(token)}" if token else result
167
-
168
185
  def version_url(self, asset_id: str, version_id: str, token: str | None = None) -> str:
169
186
  prefix = "/private/assets" if token else "/assets"
170
187
  result = f"{self._client.cdn_base_url}{prefix}/{quote(asset_id)}/versions/{quote(version_id)}"
@@ -175,26 +192,35 @@ class AssetsClient:
175
192
  result = f"{self._client.cdn_base_url}{prefix}/{quote(asset_id)}/branches/{quote(branch)}"
176
193
  return f"{result}?token={quote(token)}" if token else result
177
194
 
195
+ def video_url(self, asset_id: str, kind: str, profile: str = "default", token: str | None = None) -> str:
196
+ prefix = "/private/assets" if token else "/assets"
197
+ result = f"{self._client.cdn_base_url}{prefix}/{quote(asset_id)}/video/{quote(kind)}/{quote(profile)}"
198
+ return f"{result}?token={quote(token)}" if token else result
199
+
178
200
  def ai(self, asset_id: str) -> dict[str, Any]:
179
201
  return self._client.request("GET", f"/assets/{asset_id}/ai")
180
202
 
181
- def request_ai_analyze(self, asset_id: str) -> dict[str, Any]:
182
- return self._client.request("POST", f"/assets/{asset_id}/ai/analyze")
203
+ def request_ai_analyze(self, asset_id: str, *, idempotency_key: str | None = None) -> dict[str, Any]:
204
+ return self._request_ai_job(f"/assets/{asset_id}/ai/analyze", idempotency_key)
183
205
 
184
- def request_moderation(self, asset_id: str) -> dict[str, Any]:
185
- return self._client.request("POST", f"/assets/{asset_id}/moderation")
206
+ def request_moderation(self, asset_id: str, *, idempotency_key: str | None = None) -> dict[str, Any]:
207
+ return self._request_ai_job(f"/assets/{asset_id}/moderation", idempotency_key)
186
208
 
187
209
  def transcript(self, asset_id: str) -> dict[str, Any]:
188
210
  return self._client.request("GET", f"/assets/{asset_id}/transcript")
189
211
 
190
- def request_transcript(self, asset_id: str) -> dict[str, Any]:
191
- return self._client.request("POST", f"/assets/{asset_id}/transcript")
212
+ def request_transcript(self, asset_id: str, *, idempotency_key: str | None = None) -> dict[str, Any]:
213
+ return self._request_ai_job(f"/assets/{asset_id}/transcript", idempotency_key)
214
+
215
+ def request_smart_crop(self, asset_id: str, *, idempotency_key: str | None = None) -> dict[str, Any]:
216
+ return self._request_ai_job(f"/assets/{asset_id}/smart-crop", idempotency_key)
192
217
 
193
- def request_smart_crop(self, asset_id: str) -> dict[str, Any]:
194
- return self._client.request("POST", f"/assets/{asset_id}/smart-crop")
218
+ def request_background_removal(self, asset_id: str, *, idempotency_key: str | None = None) -> dict[str, Any]:
219
+ return self._request_ai_job(f"/assets/{asset_id}/background-remove", idempotency_key)
195
220
 
196
- def request_background_removal(self, asset_id: str) -> dict[str, Any]:
197
- return self._client.request("POST", f"/assets/{asset_id}/background-remove")
221
+ def _request_ai_job(self, path: str, idempotency_key: str | None) -> dict[str, Any]:
222
+ headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
223
+ return self._client.request("POST", path, headers=headers)
198
224
 
199
225
  def versions(self, asset_id: str) -> dict[str, Any]:
200
226
  return self._client.request("GET", f"/assets/{asset_id}/versions")
@@ -265,7 +291,74 @@ class AssetsClient:
265
291
  clean = {key: value for key, value in options.items() if value not in (None, "")}
266
292
  return self._client.request("POST", f"/assets/{asset_id}/transform-url", json=clean)
267
293
 
268
- def _multipart(self, method: str, suffix: str, file: str | Path | BinaryIO, fields: dict[str, Any]) -> dict[str, Any]:
294
+ def get_ai_config(self) -> dict[str, Any]:
295
+ return self._client.request("GET", "/assets/ai/config")
296
+
297
+ def update_ai_config(self, revision: int, *, enabled_actions: list[str], monthly_spend_cap_micros: int) -> dict[str, Any]:
298
+ return self._client.request("PUT", "/assets/ai/config", headers={"If-Match": f'"{revision}"'}, json={
299
+ "enabled_actions": enabled_actions,
300
+ "monthly_spend_cap_micros": monthly_spend_cap_micros,
301
+ })
302
+
303
+ def job(self, job_id: str) -> dict[str, Any]:
304
+ return self._client.request("GET", f"/assets/jobs/{quote(job_id)}")
305
+
306
+ def cancel_job(self, job_id: str) -> dict[str, Any]:
307
+ return self._client.request("POST", f"/assets/jobs/{quote(job_id)}/cancel")
308
+
309
+ def create_lifecycle_rule(self, **payload: Any) -> dict[str, Any]:
310
+ return self._client.request("POST", "/assets/lifecycle-rules", json=payload)
311
+
312
+ def list_lifecycle_rules(self) -> dict[str, Any]:
313
+ return self._client.request("GET", "/assets/lifecycle-rules")
314
+
315
+ def delete_lifecycle_rule(self, rule_id: str) -> dict[str, Any]:
316
+ return self._client.request("DELETE", f"/assets/lifecycle-rules/{quote(rule_id)}")
317
+
318
+ def create_domain(self, domain: str) -> dict[str, Any]:
319
+ return self._client.request("POST", "/assets/domains", json={"domain": domain})
320
+
321
+ def list_domains(self) -> dict[str, Any]:
322
+ return self._client.request("GET", "/assets/domains")
323
+
324
+ def verify_domain(self, domain_id: str) -> dict[str, Any]:
325
+ return self._client.request("POST", f"/assets/domains/{quote(domain_id)}/verify")
326
+
327
+ def delete_domain(self, domain_id: str) -> dict[str, Any]:
328
+ return self._client.request("DELETE", f"/assets/domains/{quote(domain_id)}")
329
+
330
+ def analytics(self) -> dict[str, Any]:
331
+ return self._client.request("GET", "/assets/analytics")
332
+
333
+ def usage_summary(self) -> dict[str, Any]:
334
+ return self._client.request("GET", "/assets/summary")
335
+
336
+ def events(self, limit: int = 50) -> dict[str, Any]:
337
+ return self._client.request("GET", "/assets/events", params={"limit": limit})
338
+
339
+ def create_webhook(self, url: str, event_types: list[str] | None = None) -> dict[str, Any]:
340
+ return self._client.request("POST", "/assets/webhooks", json={"url": url, "event_types": event_types or ["asset.*"]})
341
+
342
+ def list_webhooks(self) -> dict[str, Any]:
343
+ return self._client.request("GET", "/assets/webhooks")
344
+
345
+ def delete_webhook(self, webhook_id: str) -> dict[str, Any]:
346
+ return self._client.request("DELETE", f"/assets/webhooks/{quote(webhook_id)}")
347
+
348
+ def list_webhook_deliveries(self, webhook_id: str, limit: int = 50) -> dict[str, Any]:
349
+ return self._client.request("GET", f"/assets/webhooks/{quote(webhook_id)}/deliveries", params={"limit": limit})
350
+
351
+ def retry_webhook_delivery(self, delivery_id: str) -> dict[str, Any]:
352
+ return self._client.request("POST", f"/assets/webhook-deliveries/{quote(delivery_id)}/retry")
353
+
354
+ def purge(self, asset_id: str) -> dict[str, Any]:
355
+ return self._client.request("POST", f"/assets/{quote(asset_id)}/purge")
356
+
357
+ def bulk(self, *, action: str, items: list[dict[str, Any]], idempotency_key: str | None = None, **payload: Any) -> dict[str, Any]:
358
+ headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
359
+ return self._client.request("POST", "/assets/bulk", json={"action": action, "items": items, **payload}, headers=headers)
360
+
361
+ def _multipart(self, method: str, suffix: str, file: str | Path | BinaryIO, fields: dict[str, Any], headers: dict[str, str] | None = None) -> dict[str, Any]:
269
362
  should_close = False
270
363
  if isinstance(file, (str, Path)):
271
364
  handle = Path(file).open("rb")
@@ -277,7 +370,7 @@ class AssetsClient:
277
370
 
278
371
  try:
279
372
  data = {key: value for key, value in fields.items() if value not in (None, "")}
280
- return self._client.request(method, suffix, data=data, files={"file": (filename, handle)})
373
+ return self._client.request(method, suffix, data=data, files={"file": (filename, handle)}, headers=headers)
281
374
  finally:
282
375
  if should_close:
283
376
  handle.close()
@@ -292,4 +385,6 @@ def _transform_spec(options: dict[str, Any]) -> str:
292
385
  parts.append(f"c_{options.get('crop') or 'fit'}")
293
386
  parts.append(f"f_{options.get('format') or 'auto'}")
294
387
  parts.append(f"q_{options.get('quality') or 'auto'}")
388
+ parts.append(f"g_{options.get('gravity') or 'centre'}")
389
+ parts.append(f"a_{options.get('animation') or 'first'}")
295
390
  return ",".join(parts)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackshift
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Official Python SDK for StackShift.
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: requests>=2.31
@@ -1,12 +1,12 @@
1
1
  stackshift/__init__.py,sha256=e5QZUzE6m_195Cvvawyv8p_M18eNkQzW6sBgBn3SlRo,1008
2
- stackshift/assets.py,sha256=Q_YHycCrqynWF6VWiQLfURG8ZndOwyhDudsg1t47nB0,16980
2
+ stackshift/assets.py,sha256=qRzL9BNZSneNBc3es_vLsY4_hh_r3zOfdO_rAKQq5LU,22949
3
3
  stackshift/client.py,sha256=AcJivv4VRCQ756MRrxCrLi0Q4in-4np0Re8ceQSTF1Y,3000
4
4
  stackshift/mail.py,sha256=vegCniMcWBvHtz_UGAfNfVxFYTety8H3KKFQsWfKZSs,13462
5
5
  stackshift/mail_advanced.py,sha256=-lzajXqlrkR8qA4YpRX5Sv3VwcEQITtKfIXbWwjRRQE,3533
6
6
  stackshift/mail_phase7.py,sha256=Ud2fyKwt-vpaw7vgEayLw5q9tiDIF-fkfTBaoGJMJBI,5377
7
7
  stackshift/mail_reputation.py,sha256=mPvPkZeqcQc5808PhBK7gYsgf4Fk_U19nRxSsJ60MGU,1112
8
8
  stackshift/projects.py,sha256=oYWaE7xNRR6OXuPEwJH-oFFeOmDA9ozq6jJY09ANIHA,498
9
- stackshift-0.1.1.dist-info/METADATA,sha256=HUXITWCwNLSzCl8fs5t8ly_ARJYZ1SpV9jfKC5B7y2A,152
10
- stackshift-0.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
- stackshift-0.1.1.dist-info/top_level.txt,sha256=qe3uWsu_O9mNtq7bIck9Y8rjTIMrH0kzgAlGFDoRGqs,11
12
- stackshift-0.1.1.dist-info/RECORD,,
9
+ stackshift-0.1.2.dist-info/METADATA,sha256=GKbdBmBmZWOUs8DTe-amYxZNbplrh7pAlkaRtkBWQ14,152
10
+ stackshift-0.1.2.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
11
+ stackshift-0.1.2.dist-info/top_level.txt,sha256=qe3uWsu_O9mNtq7bIck9Y8rjTIMrH0kzgAlGFDoRGqs,11
12
+ stackshift-0.1.2.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (83.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5