stackshift 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.
- stackshift-1.1.0/PKG-INFO +252 -0
- {stackshift-1.0.0 → stackshift-1.1.0}/README.md +37 -0
- {stackshift-1.0.0 → stackshift-1.1.0}/pyproject.toml +2 -1
- {stackshift-1.0.0 → stackshift-1.1.0}/stackshift/__init__.py +2 -0
- {stackshift-1.0.0 → stackshift-1.1.0}/stackshift/assets.py +7 -1
- stackshift-1.1.0/stackshift/assets_dam.py +193 -0
- stackshift-1.1.0/stackshift/assets_media.py +48 -0
- stackshift-1.1.0/stackshift/assets_video.py +65 -0
- stackshift-1.1.0/stackshift/assets_workflows.py +176 -0
- {stackshift-1.0.0 → stackshift-1.1.0}/stackshift/client.py +2 -0
- {stackshift-1.0.0 → stackshift-1.1.0}/stackshift/mail.py +24 -1
- {stackshift-1.0.0 → stackshift-1.1.0}/stackshift/mail_advanced.py +4 -0
- stackshift-1.1.0/stackshift/mail_billing.py +9 -0
- stackshift-1.1.0/stackshift/mail_brands.py +35 -0
- stackshift-1.1.0/stackshift/mail_campaigns.py +108 -0
- stackshift-1.1.0/stackshift/mail_marketing.py +35 -0
- stackshift-1.1.0/stackshift/mail_migration.py +20 -0
- stackshift-1.1.0/stackshift/mail_tracking.py +33 -0
- stackshift-1.1.0/stackshift/workload_security.py +67 -0
- stackshift-1.1.0/stackshift.egg-info/PKG-INFO +252 -0
- {stackshift-1.0.0 → stackshift-1.1.0}/stackshift.egg-info/SOURCES.txt +17 -1
- stackshift-1.1.0/tests/test_assets_media.py +110 -0
- stackshift-1.1.0/tests/test_assets_workflows.py +34 -0
- stackshift-1.1.0/tests/test_mail_platform.py +29 -0
- stackshift-1.1.0/tests/test_mail_simulation.py +28 -0
- stackshift-1.1.0/tests/test_mail_subscribers.py +37 -0
- stackshift-1.1.0/tests/test_mail_tracking.py +37 -0
- stackshift-1.0.0/PKG-INFO +0 -6
- stackshift-1.0.0/stackshift/mail_campaigns.py +0 -61
- stackshift-1.0.0/stackshift.egg-info/PKG-INFO +0 -6
- {stackshift-1.0.0 → stackshift-1.1.0}/setup.cfg +0 -0
- {stackshift-1.0.0 → stackshift-1.1.0}/stackshift/mail_events_webhooks.py +0 -0
- {stackshift-1.0.0 → stackshift-1.1.0}/stackshift/mail_exports.py +0 -0
- {stackshift-1.0.0 → stackshift-1.1.0}/stackshift/mail_reputation.py +0 -0
- {stackshift-1.0.0 → stackshift-1.1.0}/stackshift/mail_streams.py +0 -0
- {stackshift-1.0.0 → stackshift-1.1.0}/stackshift/projects.py +0 -0
- {stackshift-1.0.0 → stackshift-1.1.0}/stackshift.egg-info/dependency_links.txt +0 -0
- {stackshift-1.0.0 → stackshift-1.1.0}/stackshift.egg-info/requires.txt +0 -0
- {stackshift-1.0.0 → stackshift-1.1.0}/stackshift.egg-info/top_level.txt +0 -0
- {stackshift-1.0.0 → stackshift-1.1.0}/tests/test_mail.py +0 -0
- {stackshift-1.0.0 → stackshift-1.1.0}/tests/test_mail_advanced.py +0 -0
- {stackshift-1.0.0 → stackshift-1.1.0}/tests/test_mail_diagnostics.py +0 -0
- {stackshift-1.0.0 → stackshift-1.1.0}/tests/test_mail_events_webhooks.py +0 -0
- {stackshift-1.0.0 → stackshift-1.1.0}/tests/test_mail_exports.py +0 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: stackshift
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Official Python SDK for StackShift.
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: requests>=2.31
|
|
8
|
+
|
|
9
|
+
# StackShift Python SDK
|
|
10
|
+
|
|
11
|
+
Official Python SDK for StackShift.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install stackshift
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Send email
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from stackshift import StackShift
|
|
23
|
+
|
|
24
|
+
stackshift = StackShift()
|
|
25
|
+
|
|
26
|
+
message = stackshift.mail.send(
|
|
27
|
+
from_="StackShift <noreply@mail.stackshift.cloud>",
|
|
28
|
+
to="ada@example.com",
|
|
29
|
+
subject="Welcome",
|
|
30
|
+
text="Welcome to StackShift.",
|
|
31
|
+
idempotency_key="welcome:user_123",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
print(message["id"], message["status"], message["idempotencyStatus"])
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Inspect message status:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
messages = stackshift.mail.messages.list(status="mta_accepted", limit=20)
|
|
41
|
+
detail = stackshift.mail.messages.get(messages["data"][0]["id"])
|
|
42
|
+
attempts = stackshift.mail.messages.attempts(detail["id"])
|
|
43
|
+
logs = stackshift.mail.messages.logs(detail["id"])
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`mta_accepted` means the message was accepted by StackShift's outbound MTA. It does not mean recipient-MX acceptance, inbox placement, opens, clicks, or spam placement. Recipient records later transition independently to `delayed`, `delivered`, `bounced`, `failed`, `suppressed`, or `complained`.
|
|
47
|
+
|
|
48
|
+
## Events and webhooks
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
events = stackshift.mail.events.list(
|
|
52
|
+
type="mail.message.bounced",
|
|
53
|
+
message_id="msg_123",
|
|
54
|
+
limit=20,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
event = stackshift.mail.events.get("evt_123")
|
|
58
|
+
timeline = stackshift.mail.messages.timeline("msg_123")
|
|
59
|
+
|
|
60
|
+
webhook = stackshift.mail.webhooks.create(
|
|
61
|
+
url="https://example.com/stackshift-mail",
|
|
62
|
+
event_types=["mail.message.bounced", "mail.otp.verified"],
|
|
63
|
+
)
|
|
64
|
+
print(webhook["id"], webhook["secret"]) # Secret is only returned on create/rotate.
|
|
65
|
+
|
|
66
|
+
deliveries = stackshift.mail.webhooks.deliveries(webhook["id"], status="failed")
|
|
67
|
+
retried = stackshift.mail.webhooks.retry_delivery(deliveries["data"][0]["id"])
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Verify a webhook signature before processing the payload:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
valid = stackshift.mail.webhooks.verify_signature(
|
|
74
|
+
raw_body=request.get_data(),
|
|
75
|
+
signature_header=request.headers.get("StackShift-Signature"),
|
|
76
|
+
timestamp_header=request.headers.get("StackShift-Timestamp"),
|
|
77
|
+
secret=os.environ["STACKSHIFT_WEBHOOK_SECRET"],
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Webhook handlers should be idempotent. StackShift retries non-2xx responses. Delivery events do not include full email bodies or OTP codes by default.
|
|
82
|
+
|
|
83
|
+
## Send a template
|
|
84
|
+
|
|
85
|
+
Templates are rendered by StackShift servers. The Python SDK only calls the REST API.
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
stackshift.mail.templates.create(
|
|
89
|
+
name="Welcome Email",
|
|
90
|
+
slug="welcome-email",
|
|
91
|
+
subject="Welcome, {{name}}",
|
|
92
|
+
text="Welcome, {{name}}",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
preview = stackshift.mail.templates.preview(
|
|
96
|
+
"welcome-email",
|
|
97
|
+
data={"name": "Ada"},
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
message = stackshift.mail.send_template(
|
|
101
|
+
template="welcome-email",
|
|
102
|
+
to="ada@example.com",
|
|
103
|
+
from_="Acme <noreply@acme.com>",
|
|
104
|
+
data={"name": "Ada"},
|
|
105
|
+
idempotency_key="welcome:user_123",
|
|
106
|
+
)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Missing variables fail before a message is queued. Template sends use the same sender-domain, suppression, idempotency, Durable Jobs, and Postfix handoff pipeline as `mail.send`.
|
|
110
|
+
|
|
111
|
+
## Bounces and suppressions
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
suppressions = stackshift.mail.suppressions.list()
|
|
115
|
+
|
|
116
|
+
manual = stackshift.mail.suppressions.create(
|
|
117
|
+
email="bad@example.com",
|
|
118
|
+
reason="manual",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
stackshift.mail.suppressions.delete(manual["id"])
|
|
122
|
+
|
|
123
|
+
bounces = stackshift.mail.bounces.list(type="hard")
|
|
124
|
+
message_bounces = stackshift.mail.messages.bounces("msg_123")
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Hard bounces are automatically suppressed by the backend. Suppressions are workspace scoped and checked before a message is queued.
|
|
128
|
+
|
|
129
|
+
## Verify a sending domain
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
domain = stackshift.mail.domains.create("acme.com")
|
|
133
|
+
print(domain["records"])
|
|
134
|
+
|
|
135
|
+
stackshift.mail.domains.verify(domain["id"])
|
|
136
|
+
|
|
137
|
+
message = stackshift.mail.send(
|
|
138
|
+
from_="Acme <noreply@acme.com>",
|
|
139
|
+
to="user@example.com",
|
|
140
|
+
subject="Welcome",
|
|
141
|
+
html="<h1>Welcome</h1>",
|
|
142
|
+
text="Welcome",
|
|
143
|
+
)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Add the returned SPF, DKIM, and return-path DNS records before verification. DMARC is recommended unless your environment sets `MAIL_DMARC_REQUIRED=true`. DNS propagation can take time.
|
|
147
|
+
|
|
148
|
+
## Upload an asset
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
from stackshift import StackShift
|
|
152
|
+
|
|
153
|
+
stackshift = StackShift()
|
|
154
|
+
|
|
155
|
+
asset = stackshift.assets.upload(
|
|
156
|
+
"avatar.png",
|
|
157
|
+
folder="avatars",
|
|
158
|
+
visibility="public",
|
|
159
|
+
metadata={"user_id": "user_123"},
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
print(asset["url"])
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
You do not pass a deployed StackShift project ID. The API key identifies the StackShift account, and StackShift resolves the default asset space internally.
|
|
166
|
+
|
|
167
|
+
## Private asset URL
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
signed = stackshift.assets.signed_url(
|
|
171
|
+
asset["id"],
|
|
172
|
+
expires_in="10m",
|
|
173
|
+
max_downloads=1,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
print(signed["url"])
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Image transformations
|
|
180
|
+
|
|
181
|
+
Use built-in presets or create your own named transformations.
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
from stackshift import StackShift, asset_transform_options, get_asset_transform_preset
|
|
185
|
+
|
|
186
|
+
stackshift = StackShift()
|
|
187
|
+
hero = get_asset_transform_preset("hero")
|
|
188
|
+
hero_options = asset_transform_options(hero)
|
|
189
|
+
|
|
190
|
+
stackshift.assets.create_transformation(hero["name"], **hero_options)
|
|
191
|
+
|
|
192
|
+
named = stackshift.assets.named_url("asset_123", hero["name"])
|
|
193
|
+
signed = stackshift.assets.signed_transform_url("asset_123", **hero_options, expiresIn="10m")
|
|
194
|
+
|
|
195
|
+
stackshift.assets.delete_transformation("old-preset")
|
|
196
|
+
print(named, signed["url"])
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Direct browser uploads
|
|
200
|
+
|
|
201
|
+
Create the upload session on your Python backend:
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
upload = stackshift.assets.signed_upload_url(
|
|
205
|
+
bucket="avatars",
|
|
206
|
+
key="users/user_123.png",
|
|
207
|
+
visibility="public",
|
|
208
|
+
expiresIn="10m",
|
|
209
|
+
maxBytes=5_000_000,
|
|
210
|
+
)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Then upload the browser `File` to `upload["url"]` from your frontend. Do not expose your StackShift API key to browser code.
|
|
214
|
+
|
|
215
|
+
The SDK only talks to the StackShift REST API. Storage placement, replication, disks, and repair are StackShift internals.
|
|
216
|
+
|
|
217
|
+
### Campaign drafts and engagement
|
|
218
|
+
|
|
219
|
+
`mail.campaigns.create(content={...})` accepts a private campaign content snapshot; template/version inputs still work. Save the complete draft with `update(id, revision=revision, **draft)` (draft keys use API camelCase). `preview`, `test`, `send(id, revision=revision)`, and `schedule(id, send_at, revision=revision)` protect the reviewed content. Stale revisions return 409.
|
|
220
|
+
|
|
221
|
+
Send methods accept `tracking={"opens": False, "clicks": True}`. Omitted fields inherit workspace defaults. `mail.tracking` exposes `settings`, `update_settings`, `message`, `campaign`, and branded-domain methods. Deployment capability flags control availability. Opens are approximate; detailed activity expires after 90 days. OTPs and tests never track.
|
|
222
|
+
|
|
223
|
+
### Isolated testing, brands, migration, and spending
|
|
224
|
+
|
|
225
|
+
Use a `sspat_test_` credential and `simulation={"scenario": "delayed"}` for a 60-second simulated delay. Delivered, bounced, and complained outcomes are also supported. Test resources are isolated; no real email is delivered.
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
brand = client.mail.brands.create(name="Product", editable_fields=["company"])
|
|
229
|
+
audience = client.mail.audiences.create(brand_id=brand["id"], name="Product news")
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Audience creation requires an explicit `brand_id`. Shared profile fields belong to the brand; consent stays per audience. `mail.automations`, `mail.forms`, and `mail.segments` expose create/list methods; their input dictionaries use API camelCase keys.
|
|
233
|
+
|
|
234
|
+
`mail.migration` supports public DNS audits/plans and authenticated checklists. `mail.billing.summary()` and `mail.campaigns.estimate(id)` expose allowance and credit. Pass `approved_max_kobo=0` (or another approved maximum) alongside `revision` when sending/scheduling campaigns. Billing remains inactive until a rate card is explicitly published and activated; funding and spending opt-in are separate owner-only dashboard actions.
|
|
235
|
+
|
|
236
|
+
## Native video, governed DAM and product rendering
|
|
237
|
+
|
|
238
|
+
Use the space-scoped DAM and video clients for collaborators, metadata schemas, bucket governance, reviewed publications, picker capabilities, model preparation, product rendering, galleries, version-pinned encoding, captions and playback sessions.
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
dam = sdk.assets.dam.for_space(space_id)
|
|
242
|
+
video = sdk.assets.video.for_space(space_id)
|
|
243
|
+
schema = dam.create_metadata_schema({"name": "Products", "fields": [{
|
|
244
|
+
"key": "product_name", "label": "Product name", "type": "text", "required": True,
|
|
245
|
+
}]})
|
|
246
|
+
published = dam.publish_metadata_schema(schema["id"], schema["revision"])
|
|
247
|
+
workspace = video.workspace(asset_id)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Mutations carry the supplied revision in `If-Match`. Model preparation and rendering carry an idempotency key; render submission also carries the accepted maximum units. Playback renewal and events authenticate with the session credential, not the account key. Keep these clients on your backend and return grants with `Cache-Control: private, no-store`.
|
|
251
|
+
|
|
252
|
+
See the [Assets SDK guide](https://docs.stackshift.cloud/assets/sdk-media-workflows) and its linked feature guides for complete language examples, inputs, permissions, review transitions and browser callbacks.
|
|
@@ -205,3 +205,40 @@ upload = stackshift.assets.signed_upload_url(
|
|
|
205
205
|
Then upload the browser `File` to `upload["url"]` from your frontend. Do not expose your StackShift API key to browser code.
|
|
206
206
|
|
|
207
207
|
The SDK only talks to the StackShift REST API. Storage placement, replication, disks, and repair are StackShift internals.
|
|
208
|
+
|
|
209
|
+
### Campaign drafts and engagement
|
|
210
|
+
|
|
211
|
+
`mail.campaigns.create(content={...})` accepts a private campaign content snapshot; template/version inputs still work. Save the complete draft with `update(id, revision=revision, **draft)` (draft keys use API camelCase). `preview`, `test`, `send(id, revision=revision)`, and `schedule(id, send_at, revision=revision)` protect the reviewed content. Stale revisions return 409.
|
|
212
|
+
|
|
213
|
+
Send methods accept `tracking={"opens": False, "clicks": True}`. Omitted fields inherit workspace defaults. `mail.tracking` exposes `settings`, `update_settings`, `message`, `campaign`, and branded-domain methods. Deployment capability flags control availability. Opens are approximate; detailed activity expires after 90 days. OTPs and tests never track.
|
|
214
|
+
|
|
215
|
+
### Isolated testing, brands, migration, and spending
|
|
216
|
+
|
|
217
|
+
Use a `sspat_test_` credential and `simulation={"scenario": "delayed"}` for a 60-second simulated delay. Delivered, bounced, and complained outcomes are also supported. Test resources are isolated; no real email is delivered.
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
brand = client.mail.brands.create(name="Product", editable_fields=["company"])
|
|
221
|
+
audience = client.mail.audiences.create(brand_id=brand["id"], name="Product news")
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Audience creation requires an explicit `brand_id`. Shared profile fields belong to the brand; consent stays per audience. `mail.automations`, `mail.forms`, and `mail.segments` expose create/list methods; their input dictionaries use API camelCase keys.
|
|
225
|
+
|
|
226
|
+
`mail.migration` supports public DNS audits/plans and authenticated checklists. `mail.billing.summary()` and `mail.campaigns.estimate(id)` expose allowance and credit. Pass `approved_max_kobo=0` (or another approved maximum) alongside `revision` when sending/scheduling campaigns. Billing remains inactive until a rate card is explicitly published and activated; funding and spending opt-in are separate owner-only dashboard actions.
|
|
227
|
+
|
|
228
|
+
## Native video, governed DAM and product rendering
|
|
229
|
+
|
|
230
|
+
Use the space-scoped DAM and video clients for collaborators, metadata schemas, bucket governance, reviewed publications, picker capabilities, model preparation, product rendering, galleries, version-pinned encoding, captions and playback sessions.
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
dam = sdk.assets.dam.for_space(space_id)
|
|
234
|
+
video = sdk.assets.video.for_space(space_id)
|
|
235
|
+
schema = dam.create_metadata_schema({"name": "Products", "fields": [{
|
|
236
|
+
"key": "product_name", "label": "Product name", "type": "text", "required": True,
|
|
237
|
+
}]})
|
|
238
|
+
published = dam.publish_metadata_schema(schema["id"], schema["revision"])
|
|
239
|
+
workspace = video.workspace(asset_id)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Mutations carry the supplied revision in `If-Match`. Model preparation and rendering carry an idempotency key; render submission also carries the accepted maximum units. Playback renewal and events authenticate with the session credential, not the account key. Keep these clients on your backend and return grants with `Cache-Control: private, no-store`.
|
|
243
|
+
|
|
244
|
+
See the [Assets SDK guide](https://docs.stackshift.cloud/assets/sdk-media-workflows) and its linked feature guides for complete language examples, inputs, permissions, review transitions and browser callbacks.
|
|
@@ -13,6 +13,7 @@ from .mail_streams import MailStreamsClient
|
|
|
13
13
|
from .mail_campaigns import MailAudiencesClient, MailCampaignsClient
|
|
14
14
|
from .mail_exports import MailExportsClient
|
|
15
15
|
from .projects import ProjectsClient
|
|
16
|
+
from .workload_security import WorkloadSecurityClient
|
|
16
17
|
|
|
17
18
|
__all__ = [
|
|
18
19
|
"StackShift",
|
|
@@ -33,6 +34,7 @@ __all__ = [
|
|
|
33
34
|
"MailCampaignsClient",
|
|
34
35
|
"MailExportsClient",
|
|
35
36
|
"ProjectsClient",
|
|
37
|
+
"WorkloadSecurityClient",
|
|
36
38
|
"asset_transform_options",
|
|
37
39
|
"asset_transform_spec",
|
|
38
40
|
"get_asset_transform_preset",
|
|
@@ -6,6 +6,10 @@ from pathlib import Path
|
|
|
6
6
|
from typing import Any, BinaryIO
|
|
7
7
|
from urllib.parse import quote
|
|
8
8
|
|
|
9
|
+
from .assets_workflows import AssetWorkflowsMixin
|
|
10
|
+
from .assets_dam import AssetDAMClient
|
|
11
|
+
from .assets_video import AssetVideoClient
|
|
12
|
+
|
|
9
13
|
|
|
10
14
|
ASSET_TRANSFORM_PRESETS: list[dict[str, Any]] = [
|
|
11
15
|
{"id": "web-thumbnail", "name": "web-thumbnail", "category": "responsive-web", "description": "Compact grid thumbnail for libraries and dashboards.", "width": 320, "height": 180, "crop": "fill", "format": "webp", "quality": 78},
|
|
@@ -47,9 +51,11 @@ def asset_transform_spec(**options: Any) -> str:
|
|
|
47
51
|
return _transform_spec(options)
|
|
48
52
|
|
|
49
53
|
|
|
50
|
-
class AssetsClient:
|
|
54
|
+
class AssetsClient(AssetWorkflowsMixin):
|
|
51
55
|
def __init__(self, client: Any) -> None:
|
|
52
56
|
self._client = client
|
|
57
|
+
self.dam = AssetDAMClient(client)
|
|
58
|
+
self.video = AssetVideoClient(client)
|
|
53
59
|
|
|
54
60
|
def upload(
|
|
55
61
|
self,
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from urllib.parse import quote
|
|
5
|
+
|
|
6
|
+
from .assets_media import AssetMediaClient
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AssetDAMClient(AssetMediaClient):
|
|
10
|
+
def spaces(self):
|
|
11
|
+
return self._request("GET", f"/assets/spaces")
|
|
12
|
+
|
|
13
|
+
def collaborators(self):
|
|
14
|
+
return self._request("GET", f"/assets/collaborators")
|
|
15
|
+
|
|
16
|
+
def invite(self, input: dict[str, Any]):
|
|
17
|
+
return self._request("POST", f"/assets/collaborators", body=input)
|
|
18
|
+
|
|
19
|
+
def accept_invitation(self, id: str):
|
|
20
|
+
return self._request("POST", f"/assets/invitations/{quote(id, safe='')}/accept")
|
|
21
|
+
|
|
22
|
+
def update_collaborator(self, id: str, revision: int, actions: list[str]):
|
|
23
|
+
return self._request(
|
|
24
|
+
"PUT",
|
|
25
|
+
f"/assets/collaborators/{quote(id, safe='')}",
|
|
26
|
+
body={"actions": actions},
|
|
27
|
+
revision=revision,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def revoke_collaborator(self, id: str, revision: int):
|
|
31
|
+
return self._request(
|
|
32
|
+
"DELETE", f"/assets/collaborators/{quote(id, safe='')}", revision=revision
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def metadata_schemas(self):
|
|
36
|
+
return self._request("GET", f"/assets/metadata-schemas")
|
|
37
|
+
|
|
38
|
+
def metadata_editor(self, asset_id: str):
|
|
39
|
+
return self._request(
|
|
40
|
+
"GET", f"/assets/{quote(asset_id, safe='')}/metadata-editor"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def create_metadata_schema(self, input: dict[str, Any]):
|
|
44
|
+
return self._request("POST", f"/assets/metadata-schemas", body=input)
|
|
45
|
+
|
|
46
|
+
def update_metadata_schema(self, id: str, revision: int, input: dict[str, Any]):
|
|
47
|
+
return self._request(
|
|
48
|
+
"PUT",
|
|
49
|
+
f"/assets/metadata-schemas/{quote(id, safe='')}",
|
|
50
|
+
body=input,
|
|
51
|
+
revision=revision,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def publish_metadata_schema(self, id: str, revision: int):
|
|
55
|
+
return self._request(
|
|
56
|
+
"POST",
|
|
57
|
+
f"/assets/metadata-schemas/{quote(id, safe='')}/publish",
|
|
58
|
+
revision=revision,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def bucket_governance(self, id: str):
|
|
62
|
+
return self._request("GET", f"/assets/buckets/{quote(id, safe='')}/governance")
|
|
63
|
+
|
|
64
|
+
def save_bucket_governance(self, id: str, input: dict[str, Any]):
|
|
65
|
+
return self._request(
|
|
66
|
+
"PUT", f"/assets/buckets/{quote(id, safe='')}/governance", body=input
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def migrate_bucket_governance(self, id: str, revision: int):
|
|
70
|
+
return self._request(
|
|
71
|
+
"POST",
|
|
72
|
+
f"/assets/buckets/{quote(id, safe='')}/governance/migrate",
|
|
73
|
+
revision=revision,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def publications(self, query: dict[str, Any] | None = None):
|
|
77
|
+
return self._request("GET", f"/assets/publications", query=query)
|
|
78
|
+
|
|
79
|
+
def publication_renditions(self, asset_id: str):
|
|
80
|
+
return self._request(
|
|
81
|
+
"GET", f"/assets/{quote(asset_id, safe='')}/publication-renditions"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def create_publication(self, input: dict[str, Any]):
|
|
85
|
+
return self._request("POST", f"/assets/publications", body=input)
|
|
86
|
+
|
|
87
|
+
def update_publication(self, id: str, revision: int, input: dict[str, Any]):
|
|
88
|
+
return self._request(
|
|
89
|
+
"PUT",
|
|
90
|
+
f"/assets/publications/{quote(id, safe='')}",
|
|
91
|
+
body=input,
|
|
92
|
+
revision=revision,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def transition_publication(self, id: str, revision: int, action: str, note: str):
|
|
96
|
+
return self._request(
|
|
97
|
+
"POST",
|
|
98
|
+
f"/assets/publications/{quote(id, safe='')}/{quote(action, safe='')}",
|
|
99
|
+
body={"note": note},
|
|
100
|
+
revision=revision,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def resolve_publication(self, id: str):
|
|
104
|
+
return self._request(
|
|
105
|
+
"GET", f"/assets/publications/{quote(id, safe='')}/resolve"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def preview_publication(self, id: str):
|
|
109
|
+
return self._request(
|
|
110
|
+
"GET", f"/assets/publications/{quote(id, safe='')}/preview"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def resolve_publication_ar(self, id: str):
|
|
114
|
+
return self._request("POST", f"/assets/publications/{quote(id, safe='')}/ar")
|
|
115
|
+
|
|
116
|
+
def create_picker_capability(self, input: dict[str, Any]):
|
|
117
|
+
return self._request("POST", f"/assets/picker-capabilities", body=input)
|
|
118
|
+
|
|
119
|
+
def revoke_picker_capability(self, id: str):
|
|
120
|
+
return self._request(
|
|
121
|
+
"DELETE", f"/assets/picker-capabilities/{quote(id, safe='')}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def models(self, asset_id: str):
|
|
125
|
+
return self._request("GET", f"/assets/{quote(asset_id, safe='')}/models")
|
|
126
|
+
|
|
127
|
+
def estimate_render(self, asset_id: str, revision: int, input: dict[str, Any]):
|
|
128
|
+
return self._request(
|
|
129
|
+
"POST",
|
|
130
|
+
f"/assets/{quote(asset_id, safe='')}/models",
|
|
131
|
+
body={**input, "operation": "estimate"},
|
|
132
|
+
revision=revision,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def render_product(
|
|
136
|
+
self,
|
|
137
|
+
asset_id: str,
|
|
138
|
+
revision: int,
|
|
139
|
+
idempotency_key: str,
|
|
140
|
+
input: dict[str, Any],
|
|
141
|
+
maximum_units: int,
|
|
142
|
+
):
|
|
143
|
+
return self._request(
|
|
144
|
+
"POST",
|
|
145
|
+
f"/assets/{quote(asset_id, safe='')}/models",
|
|
146
|
+
body={**input, "operation": "render", "maximum_units": maximum_units},
|
|
147
|
+
revision=revision,
|
|
148
|
+
idempotency_key=idempotency_key,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def process_model(self, asset_id: str, revision: int, idempotency_key: str):
|
|
152
|
+
return self._request(
|
|
153
|
+
"POST",
|
|
154
|
+
f"/assets/{quote(asset_id, safe='')}/models",
|
|
155
|
+
revision=revision,
|
|
156
|
+
idempotency_key=idempotency_key,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def galleries(self, query: dict[str, Any] | None = None):
|
|
160
|
+
return self._request("GET", f"/assets/galleries", query=query)
|
|
161
|
+
|
|
162
|
+
def create_gallery(self, input: dict[str, Any]):
|
|
163
|
+
return self._request("POST", f"/assets/galleries", body=input)
|
|
164
|
+
|
|
165
|
+
def update_gallery(self, id: str, revision: int, input: dict[str, Any]):
|
|
166
|
+
return self._request(
|
|
167
|
+
"PUT",
|
|
168
|
+
f"/assets/galleries/{quote(id, safe='')}",
|
|
169
|
+
body=input,
|
|
170
|
+
revision=revision,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def transition_gallery(self, id: str, revision: int, action: str):
|
|
174
|
+
return self._request(
|
|
175
|
+
"POST",
|
|
176
|
+
f"/assets/galleries/{quote(id, safe='')}/{quote(action, safe='')}",
|
|
177
|
+
revision=revision,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def resolve_gallery(self, id: str):
|
|
181
|
+
return self._request("GET", f"/assets/galleries/{quote(id, safe='')}/resolve")
|
|
182
|
+
|
|
183
|
+
def preview_gallery(self, id: str):
|
|
184
|
+
return self._request(
|
|
185
|
+
"GET", f"/assets/galleries/{quote(id, safe='')}/resolve?preview=true"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def resolve_gallery_ar(self, id: str, publication_id: str):
|
|
189
|
+
return self._request(
|
|
190
|
+
"POST",
|
|
191
|
+
f"/assets/galleries/{quote(id, safe='')}/ar",
|
|
192
|
+
body={"publication_id": publication_id},
|
|
193
|
+
)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from urllib.parse import urlsplit
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AssetMediaClient:
|
|
8
|
+
def __init__(self, client: Any, space_id: str = "") -> None:
|
|
9
|
+
self._client = client
|
|
10
|
+
self._space_id = space_id
|
|
11
|
+
|
|
12
|
+
def for_space(self, space_id: str):
|
|
13
|
+
return type(self)(self._client, space_id)
|
|
14
|
+
|
|
15
|
+
def _request(
|
|
16
|
+
self,
|
|
17
|
+
method: str,
|
|
18
|
+
path: str,
|
|
19
|
+
*,
|
|
20
|
+
body=None,
|
|
21
|
+
revision=None,
|
|
22
|
+
idempotency_key=None,
|
|
23
|
+
query=None,
|
|
24
|
+
):
|
|
25
|
+
headers = {}
|
|
26
|
+
if self._space_id:
|
|
27
|
+
headers["X-Asset-Space-ID"] = self._space_id
|
|
28
|
+
if revision is not None:
|
|
29
|
+
headers["If-Match"] = f'"{revision}"'
|
|
30
|
+
if idempotency_key is not None:
|
|
31
|
+
headers["Idempotency-Key"] = idempotency_key
|
|
32
|
+
return self._client.request(
|
|
33
|
+
method, path, json=body, headers=headers, params=query
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def _playback(self, path: str, credential: str, body: dict[str, Any]):
|
|
37
|
+
from .client import StackShift
|
|
38
|
+
|
|
39
|
+
if not credential.strip():
|
|
40
|
+
raise ValueError("Playback credential is required")
|
|
41
|
+
base = urlsplit(self._client.base_url)
|
|
42
|
+
# A separate client prevents account authentication from replacing the session credential.
|
|
43
|
+
playback = StackShift(
|
|
44
|
+
api_key=credential,
|
|
45
|
+
base_url=f"{base.scheme}://{base.netloc}",
|
|
46
|
+
session=self._client.session,
|
|
47
|
+
)
|
|
48
|
+
return playback.request("POST", "/playback" + path, json=body)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from urllib.parse import quote
|
|
5
|
+
|
|
6
|
+
from .assets_media import AssetMediaClient
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AssetVideoClient(AssetMediaClient):
|
|
10
|
+
def workspace(self, asset_id: str):
|
|
11
|
+
return self._request("GET", f"/assets/{quote(asset_id, safe='')}/video")
|
|
12
|
+
|
|
13
|
+
def process(self, asset_id: str, version_id: str):
|
|
14
|
+
return self._request(
|
|
15
|
+
"POST",
|
|
16
|
+
f"/assets/{quote(asset_id, safe='')}/versions/{quote(version_id, safe='')}/video",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
def create_session(self, asset_id: str):
|
|
20
|
+
return self._request(
|
|
21
|
+
"POST", f"/assets/{quote(asset_id, safe='')}/playback-sessions"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def revoke_session(self, asset_id: str, session_id: str):
|
|
25
|
+
return self._request(
|
|
26
|
+
"DELETE",
|
|
27
|
+
f"/assets/{quote(asset_id, safe='')}/playback-sessions/{quote(session_id, safe='')}",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def captions(self, asset_id: str, version_id: str):
|
|
31
|
+
return self._request(
|
|
32
|
+
"GET",
|
|
33
|
+
f"/assets/{quote(asset_id, safe='')}/versions/{quote(version_id, safe='')}/video/captions",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def add_caption(self, asset_id: str, version_id: str, input: dict[str, Any]):
|
|
37
|
+
return self._request(
|
|
38
|
+
"POST",
|
|
39
|
+
f"/assets/{quote(asset_id, safe='')}/versions/{quote(version_id, safe='')}/video/captions",
|
|
40
|
+
body=input,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def remove_caption(self, asset_id: str, version_id: str, caption_id: str):
|
|
44
|
+
return self._request(
|
|
45
|
+
"DELETE",
|
|
46
|
+
f"/assets/{quote(asset_id, safe='')}/versions/{quote(version_id, safe='')}/video/captions/{quote(caption_id, safe='')}",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def review(self, asset_id: str, version_id: str, decision: str, reason: str):
|
|
50
|
+
return self._request(
|
|
51
|
+
"POST",
|
|
52
|
+
f"/assets/{quote(asset_id, safe='')}/versions/{quote(version_id, safe='')}/video/review",
|
|
53
|
+
body={"decision": decision, "reason": reason},
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def analytics(self, asset_id: str):
|
|
57
|
+
return self._request(
|
|
58
|
+
"GET", f"/assets/{quote(asset_id, safe='')}/video/analytics"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def renew_session(self, credential: str):
|
|
62
|
+
return self._playback("/sessions/renew", credential, {})
|
|
63
|
+
|
|
64
|
+
def events(self, credential: str, events: list[dict[str, Any]]):
|
|
65
|
+
return self._playback("/events", credential, {"events": events})
|