stackshift 1.0.1__py3-none-any.whl → 1.2.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.
- stackshift/__init__.py +4 -0
- stackshift/ai_credits.py +24 -0
- stackshift/assets.py +6 -0
- stackshift/assets_dam.py +193 -0
- stackshift/assets_media.py +48 -0
- stackshift/assets_platform.py +99 -0
- stackshift/assets_video.py +65 -0
- stackshift/client.py +4 -0
- stackshift/mail.py +24 -1
- stackshift/mail_advanced.py +4 -0
- stackshift/mail_billing.py +9 -0
- stackshift/mail_brands.py +35 -0
- stackshift/mail_campaigns.py +56 -9
- stackshift/mail_marketing.py +35 -0
- stackshift/mail_migration.py +20 -0
- stackshift/mail_tracking.py +33 -0
- stackshift/workload_security.py +67 -0
- {stackshift-1.0.1.dist-info → stackshift-1.2.0.dist-info}/METADATA +70 -1
- stackshift-1.2.0.dist-info/RECORD +27 -0
- stackshift-1.0.1.dist-info/RECORD +0 -16
- {stackshift-1.0.1.dist-info → stackshift-1.2.0.dist-info}/WHEEL +0 -0
- {stackshift-1.0.1.dist-info → stackshift-1.2.0.dist-info}/top_level.txt +0 -0
stackshift/__init__.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from .client import StackShift
|
|
2
|
+
from .ai_credits import AICreditsClient
|
|
2
3
|
from .assets import ASSET_TRANSFORM_PRESETS, AssetsClient, asset_transform_options, asset_transform_spec, get_asset_transform_preset
|
|
3
4
|
from .mail import (
|
|
4
5
|
MailClient,
|
|
@@ -13,9 +14,11 @@ from .mail_streams import MailStreamsClient
|
|
|
13
14
|
from .mail_campaigns import MailAudiencesClient, MailCampaignsClient
|
|
14
15
|
from .mail_exports import MailExportsClient
|
|
15
16
|
from .projects import ProjectsClient
|
|
17
|
+
from .workload_security import WorkloadSecurityClient
|
|
16
18
|
|
|
17
19
|
__all__ = [
|
|
18
20
|
"StackShift",
|
|
21
|
+
"AICreditsClient",
|
|
19
22
|
"ASSET_TRANSFORM_PRESETS",
|
|
20
23
|
"AssetsClient",
|
|
21
24
|
"MailClient",
|
|
@@ -33,6 +36,7 @@ __all__ = [
|
|
|
33
36
|
"MailCampaignsClient",
|
|
34
37
|
"MailExportsClient",
|
|
35
38
|
"ProjectsClient",
|
|
39
|
+
"WorkloadSecurityClient",
|
|
36
40
|
"asset_transform_options",
|
|
37
41
|
"asset_transform_spec",
|
|
38
42
|
"get_asset_transform_preset",
|
stackshift/ai_credits.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AICreditsClient:
|
|
5
|
+
def __init__(self, client: Any) -> None:
|
|
6
|
+
self._client = client
|
|
7
|
+
|
|
8
|
+
def balance(self) -> dict[str, Any]:
|
|
9
|
+
return self._client.request("GET", "/billing/ai-credits")
|
|
10
|
+
|
|
11
|
+
def usage(self, **filters: Any) -> dict[str, Any]:
|
|
12
|
+
return self._client.request("GET", "/billing/ai-credits/usage", params=filters)
|
|
13
|
+
|
|
14
|
+
def quote(self, credits: int, currency: str) -> dict[str, Any]:
|
|
15
|
+
return self._client.request("POST", "/billing/ai-credits/top-ups/quote", json={"credits": credits, "currency": currency})
|
|
16
|
+
|
|
17
|
+
def checkout(self, quote_id: str, idempotency_key: str, provider: str) -> dict[str, Any]:
|
|
18
|
+
return self._client.request("POST", "/billing/ai-credits/top-ups/checkout", json={"quote_id": quote_id, "idempotency_key": idempotency_key, "provider": provider})
|
|
19
|
+
|
|
20
|
+
def funding(self) -> dict[str, str]:
|
|
21
|
+
return self._client.request("GET", "/billing/ai-credits/funding")
|
|
22
|
+
|
|
23
|
+
def set_funding(self, product: str, mode: str) -> dict[str, str]:
|
|
24
|
+
return self._client.request("PUT", "/billing/ai-credits/funding", json={"product": product, "mode": mode})
|
stackshift/assets.py
CHANGED
|
@@ -7,6 +7,9 @@ from typing import Any, BinaryIO
|
|
|
7
7
|
from urllib.parse import quote
|
|
8
8
|
|
|
9
9
|
from .assets_workflows import AssetWorkflowsMixin
|
|
10
|
+
from .assets_dam import AssetDAMClient
|
|
11
|
+
from .assets_video import AssetVideoClient
|
|
12
|
+
from .assets_platform import AssetPlatformClient
|
|
10
13
|
|
|
11
14
|
|
|
12
15
|
ASSET_TRANSFORM_PRESETS: list[dict[str, Any]] = [
|
|
@@ -52,6 +55,9 @@ def asset_transform_spec(**options: Any) -> str:
|
|
|
52
55
|
class AssetsClient(AssetWorkflowsMixin):
|
|
53
56
|
def __init__(self, client: Any) -> None:
|
|
54
57
|
self._client = client
|
|
58
|
+
self.dam = AssetDAMClient(client)
|
|
59
|
+
self.video = AssetVideoClient(client)
|
|
60
|
+
self.platform = AssetPlatformClient(client)
|
|
55
61
|
|
|
56
62
|
def upload(
|
|
57
63
|
self,
|
stackshift/assets_dam.py
ADDED
|
@@ -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,99 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from urllib.parse import quote
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _id(value: str) -> str:
|
|
8
|
+
return quote(value, safe="")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class _ScopedTransport:
|
|
12
|
+
def __init__(self, client: Any, space_id: str) -> None:
|
|
13
|
+
self._client = client
|
|
14
|
+
self._space_id = space_id
|
|
15
|
+
|
|
16
|
+
def request(self, method: str, path: str, **kwargs: Any) -> dict[str, Any]:
|
|
17
|
+
headers = dict(kwargs.pop("headers", None) or {})
|
|
18
|
+
if self._space_id:
|
|
19
|
+
headers["X-Asset-Space-ID"] = self._space_id
|
|
20
|
+
return self._client.request(method, path, headers=headers, **kwargs)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AssetConnectionsClient:
|
|
24
|
+
def __init__(self, client: Any) -> None:
|
|
25
|
+
self._client = client
|
|
26
|
+
|
|
27
|
+
def list(self) -> dict[str, Any]:
|
|
28
|
+
return self._client.request("GET", "/assets/connections")
|
|
29
|
+
|
|
30
|
+
def get(self, connection_id: str) -> dict[str, Any]:
|
|
31
|
+
return self._client.request("GET", f"/assets/connections/{_id(connection_id)}")
|
|
32
|
+
|
|
33
|
+
def create(self, input: dict[str, Any]) -> dict[str, Any]:
|
|
34
|
+
return self._client.request("POST", "/assets/connections", json=input)
|
|
35
|
+
|
|
36
|
+
def update(self, connection_id: str, input: dict[str, Any]) -> dict[str, Any]:
|
|
37
|
+
return self._client.request("PATCH", f"/assets/connections/{_id(connection_id)}", json=input)
|
|
38
|
+
|
|
39
|
+
def verify(self, connection_id: str) -> dict[str, Any]:
|
|
40
|
+
return self._client.request("POST", f"/assets/connections/{_id(connection_id)}/verify")
|
|
41
|
+
|
|
42
|
+
def delete(self, connection_id: str) -> dict[str, Any]:
|
|
43
|
+
return self._client.request("DELETE", f"/assets/connections/{_id(connection_id)}")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AssetStorageClient:
|
|
47
|
+
def __init__(self, client: Any) -> None:
|
|
48
|
+
self._client = client
|
|
49
|
+
|
|
50
|
+
def configure(self, bucket_id: str, revision: int, input: dict[str, Any]) -> dict[str, Any]:
|
|
51
|
+
return self._client.request("PUT", f"/assets/buckets/{_id(bucket_id)}/storage",
|
|
52
|
+
json=input, headers={"If-Match": f'"{revision}"'})
|
|
53
|
+
|
|
54
|
+
def list_s2_buckets(self) -> dict[str, Any]:
|
|
55
|
+
return self._client.request("GET", "/assets/s2-buckets")
|
|
56
|
+
|
|
57
|
+
def create_s2_bucket(self, input: dict[str, Any]) -> dict[str, Any]:
|
|
58
|
+
return self._client.request("POST", "/assets/s2-buckets", json=input)
|
|
59
|
+
|
|
60
|
+
def relocate(self, bucket_id: str, input: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
61
|
+
return self._client.request("POST", f"/assets/buckets/{_id(bucket_id)}/relocations",
|
|
62
|
+
json=input or {})
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class AssetMigrationsClient:
|
|
66
|
+
def __init__(self, client: Any) -> None:
|
|
67
|
+
self._client = client
|
|
68
|
+
|
|
69
|
+
def start(self, input: dict[str, Any]) -> dict[str, Any]:
|
|
70
|
+
return self._client.request("POST", "/assets/migrations", json=input)
|
|
71
|
+
|
|
72
|
+
def list(self, limit: int | None = None) -> dict[str, Any]:
|
|
73
|
+
return self._client.request("GET", "/assets/migrations",
|
|
74
|
+
params={"limit": limit} if limit is not None else None)
|
|
75
|
+
|
|
76
|
+
def get(self, migration_id: str) -> dict[str, Any]:
|
|
77
|
+
return self._client.request("GET", f"/assets/migrations/{_id(migration_id)}")
|
|
78
|
+
|
|
79
|
+
def cancel(self, migration_id: str) -> dict[str, Any]:
|
|
80
|
+
return self._client.request("POST", f"/assets/migrations/{_id(migration_id)}/cancel")
|
|
81
|
+
|
|
82
|
+
def retry_item(self, migration_id: str, item_id: str) -> dict[str, Any]:
|
|
83
|
+
return self._client.request("POST", f"/assets/migrations/{_id(migration_id)}/items/{_id(item_id)}/retry")
|
|
84
|
+
|
|
85
|
+
def report(self, migration_id: str) -> dict[str, Any]:
|
|
86
|
+
return self._client.request("GET", f"/assets/migrations/{_id(migration_id)}/report",
|
|
87
|
+
params={"format": "json"})
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class AssetPlatformClient:
|
|
91
|
+
def __init__(self, client: Any, space_id: str = "") -> None:
|
|
92
|
+
self._client = client
|
|
93
|
+
transport = _ScopedTransport(client, space_id)
|
|
94
|
+
self.connections = AssetConnectionsClient(transport)
|
|
95
|
+
self.storage = AssetStorageClient(transport)
|
|
96
|
+
self.migrations = AssetMigrationsClient(transport)
|
|
97
|
+
|
|
98
|
+
def for_space(self, space_id: str) -> AssetPlatformClient:
|
|
99
|
+
return type(self)(self._client, space_id)
|
|
@@ -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})
|
stackshift/client.py
CHANGED
|
@@ -26,13 +26,17 @@ class StackShift:
|
|
|
26
26
|
self.cdn_base_url = cdn_base_url.rstrip("/")
|
|
27
27
|
self.session = session or requests.Session()
|
|
28
28
|
|
|
29
|
+
from .ai_credits import AICreditsClient
|
|
29
30
|
from .assets import AssetsClient
|
|
30
31
|
from .mail import MailClient
|
|
31
32
|
from .projects import ProjectsClient
|
|
33
|
+
from .workload_security import WorkloadSecurityClient
|
|
32
34
|
|
|
35
|
+
self.ai_credits = AICreditsClient(self)
|
|
33
36
|
self.projects = ProjectsClient(self)
|
|
34
37
|
self.assets = AssetsClient(self)
|
|
35
38
|
self.mail = MailClient(self)
|
|
39
|
+
self.workload_security = WorkloadSecurityClient(self)
|
|
36
40
|
|
|
37
41
|
def request(self, method: str, path: str, **kwargs: Any) -> Any:
|
|
38
42
|
return self._request(self.base_url, method, path, **kwargs)
|
stackshift/mail.py
CHANGED
|
@@ -7,8 +7,13 @@ from .mail_advanced import MailAnalyticsClient, MailBatchClient, MailInboundClie
|
|
|
7
7
|
from .mail_events_webhooks import MailEventsClient, MailWebhooksClient
|
|
8
8
|
from .mail_reputation import MailLimitsClient, MailReputationClient
|
|
9
9
|
from .mail_streams import MailStreamsClient
|
|
10
|
+
from .mail_brands import MailBrandsClient
|
|
11
|
+
from .mail_billing import MailBillingClient
|
|
12
|
+
from .mail_marketing import MailAutomationsClient, MailFormsClient, MailSegmentsClient
|
|
13
|
+
from .mail_migration import MailMigrationClient
|
|
10
14
|
from .mail_campaigns import MailAudiencesClient, MailCampaignsClient
|
|
11
15
|
from .mail_exports import MailExportsClient
|
|
16
|
+
from .mail_tracking import MailTrackingClient
|
|
12
17
|
|
|
13
18
|
|
|
14
19
|
class MailClient:
|
|
@@ -23,8 +28,15 @@ class MailClient:
|
|
|
23
28
|
self.events = MailEventsClient(client)
|
|
24
29
|
self.webhooks = MailWebhooksClient(client)
|
|
25
30
|
self.streams = MailStreamsClient(client)
|
|
31
|
+
self.brands = MailBrandsClient(client)
|
|
32
|
+
self.billing = MailBillingClient(client)
|
|
33
|
+
self.automations = MailAutomationsClient(client)
|
|
34
|
+
self.forms = MailFormsClient(client)
|
|
35
|
+
self.segments = MailSegmentsClient(client)
|
|
36
|
+
self.migration = MailMigrationClient(client)
|
|
26
37
|
self.audiences = MailAudiencesClient(client)
|
|
27
38
|
self.campaigns = MailCampaignsClient(client)
|
|
39
|
+
self.tracking = MailTrackingClient(client)
|
|
28
40
|
self.exports = MailExportsClient(client)
|
|
29
41
|
self.limits = MailLimitsClient(client)
|
|
30
42
|
self.reputation = MailReputationClient(client)
|
|
@@ -49,6 +61,8 @@ class MailClient:
|
|
|
49
61
|
headers: dict[str, str] | None = None,
|
|
50
62
|
tags: dict[str, str] | None = None,
|
|
51
63
|
stream: str | None = None,
|
|
64
|
+
tracking: dict[str, bool] | None = None,
|
|
65
|
+
simulation: dict[str, str] | None = None,
|
|
52
66
|
) -> dict[str, Any]:
|
|
53
67
|
payload = {
|
|
54
68
|
"from": from_,
|
|
@@ -64,6 +78,8 @@ class MailClient:
|
|
|
64
78
|
"headers": headers,
|
|
65
79
|
"tags": tags,
|
|
66
80
|
"stream": stream,
|
|
81
|
+
"tracking": tracking,
|
|
82
|
+
"simulation": simulation,
|
|
67
83
|
}
|
|
68
84
|
clean = {key: value for key, value in payload.items() if value is not None}
|
|
69
85
|
return self._client.mail_request("POST", "/mail/send", json=clean)
|
|
@@ -84,6 +100,8 @@ class MailClient:
|
|
|
84
100
|
headers: dict[str, str] | None = None,
|
|
85
101
|
tags: dict[str, str] | None = None,
|
|
86
102
|
stream: str | None = None,
|
|
103
|
+
tracking: dict[str, bool] | None = None,
|
|
104
|
+
simulation: dict[str, str] | None = None,
|
|
87
105
|
) -> dict[str, Any]:
|
|
88
106
|
payload = {
|
|
89
107
|
"template": template,
|
|
@@ -99,6 +117,8 @@ class MailClient:
|
|
|
99
117
|
"headers": headers,
|
|
100
118
|
"tags": tags,
|
|
101
119
|
"stream": stream,
|
|
120
|
+
"tracking": tracking,
|
|
121
|
+
"simulation": simulation,
|
|
102
122
|
}
|
|
103
123
|
clean = {key: value for key, value in payload.items() if value is not None}
|
|
104
124
|
return self._client.mail_request("POST", "/mail/send-template", json=clean)
|
|
@@ -201,8 +221,9 @@ class MailTemplatesClient:
|
|
|
201
221
|
data: dict[str, Any],
|
|
202
222
|
from_: str | dict[str, str] | None = None,
|
|
203
223
|
version_id: str | None = None,
|
|
224
|
+
simulation: dict[str, str] | None = None,
|
|
204
225
|
) -> dict[str, Any]:
|
|
205
|
-
payload = {"to": to, "from": from_, "data": data, "versionId": version_id}
|
|
226
|
+
payload = {"simulation": simulation, "to": to, "from": from_, "data": data, "versionId": version_id}
|
|
206
227
|
clean = {key: value for key, value in payload.items() if value is not None}
|
|
207
228
|
return self._client.mail_request("POST", f"/mail/templates/{quote(template_id)}/test-send", json=clean)
|
|
208
229
|
|
|
@@ -348,6 +369,7 @@ class MailOTPClient:
|
|
|
348
369
|
idempotency_key: str | None = None,
|
|
349
370
|
subject: str | None = None,
|
|
350
371
|
brand_name: str | None = None,
|
|
372
|
+
simulation: dict[str, str] | None = None,
|
|
351
373
|
) -> dict[str, Any]:
|
|
352
374
|
payload = {
|
|
353
375
|
"to": to,
|
|
@@ -359,6 +381,7 @@ class MailOTPClient:
|
|
|
359
381
|
"idempotencyKey": idempotency_key,
|
|
360
382
|
"subject": subject,
|
|
361
383
|
"brandName": brand_name,
|
|
384
|
+
"simulation": simulation,
|
|
362
385
|
}
|
|
363
386
|
clean = {key: value for key, value in payload.items() if value is not None}
|
|
364
387
|
return self._client.mail_request("POST", "/mail/otp/send", json=clean)
|
stackshift/mail_advanced.py
CHANGED
|
@@ -39,6 +39,8 @@ class MailBatchClient:
|
|
|
39
39
|
headers: dict[str, str] | None = None,
|
|
40
40
|
tags: dict[str, str] | None = None,
|
|
41
41
|
stream: str | None = None,
|
|
42
|
+
tracking: dict[str, bool] | None = None,
|
|
43
|
+
simulation: dict[str, str] | None = None,
|
|
42
44
|
) -> dict[str, Any]:
|
|
43
45
|
payload = {
|
|
44
46
|
"template": template,
|
|
@@ -48,6 +50,8 @@ class MailBatchClient:
|
|
|
48
50
|
"headers": headers,
|
|
49
51
|
"tags": tags,
|
|
50
52
|
"stream": stream,
|
|
53
|
+
"tracking": tracking,
|
|
54
|
+
"simulation": simulation,
|
|
51
55
|
}
|
|
52
56
|
clean = {key: value for key, value in payload.items() if value is not None}
|
|
53
57
|
return self._client.mail_request("POST", "/mail/batch-template", json=clean)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from urllib.parse import quote
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MailBrandsClient:
|
|
8
|
+
def __init__(self, client: Any) -> None:
|
|
9
|
+
self._client = client
|
|
10
|
+
|
|
11
|
+
def create(self, *, name: str, **fields: Any) -> dict[str, Any]:
|
|
12
|
+
return self._client.mail_request("POST", "/mail/brands", json={"name": name, **_brand_fields(fields)})
|
|
13
|
+
|
|
14
|
+
def list(self) -> dict[str, Any]:
|
|
15
|
+
return self._client.mail_request("GET", "/mail/brands")
|
|
16
|
+
|
|
17
|
+
def get(self, brand_id: str) -> dict[str, Any]:
|
|
18
|
+
return self._client.mail_request("GET", f"/mail/brands/{quote(brand_id, safe='')}")
|
|
19
|
+
|
|
20
|
+
def update(self, brand_id: str, *, revision: int, name: str, **fields: Any) -> dict[str, Any]:
|
|
21
|
+
return self._client.mail_request(
|
|
22
|
+
"PUT", f"/mail/brands/{quote(brand_id, safe='')}",
|
|
23
|
+
json={"revision": revision, "name": name, **_brand_fields(fields)},
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def archive(self, brand_id: str) -> dict[str, Any]:
|
|
27
|
+
return self._client.mail_request("POST", f"/mail/brands/{quote(brand_id, safe='')}/archive")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _brand_fields(fields: dict[str, Any]) -> dict[str, Any]:
|
|
31
|
+
names = {"logo_url": "logoUrl", "sending_domain_id": "sendingDomainId", "default_from": "defaultFrom", "reply_to": "replyTo", "postal_address": "postalAddress", "website": "website", "support_email": "supportEmail", "editable_fields": "editableFields"}
|
|
32
|
+
unknown = fields.keys() - names.keys() - set(names.values())
|
|
33
|
+
if unknown:
|
|
34
|
+
raise ValueError("Unknown brand fields: " + ", ".join(sorted(unknown)))
|
|
35
|
+
return {names.get(key, key): value for key, value in fields.items()}
|
stackshift/mail_campaigns.py
CHANGED
|
@@ -8,26 +8,48 @@ class MailAudiencesClient:
|
|
|
8
8
|
def __init__(self, client: Any) -> None:
|
|
9
9
|
self._client = client
|
|
10
10
|
|
|
11
|
-
def create(self, *, name: str, description: str | None = None) -> dict[str, Any]:
|
|
12
|
-
payload = {"name": name, "description": description}
|
|
11
|
+
def create(self, *, brand_id: str, name: str, description: str | None = None) -> dict[str, Any]:
|
|
12
|
+
payload = {"brandId": brand_id, "name": name, "description": description}
|
|
13
13
|
return self._client.mail_request("POST", "/mail/audiences", json={key: value for key, value in payload.items() if value is not None})
|
|
14
14
|
|
|
15
15
|
def list(self) -> dict[str, Any]:
|
|
16
16
|
return self._client.mail_request("GET", "/mail/audiences")
|
|
17
17
|
|
|
18
18
|
def get(self, audience_id: str) -> dict[str, Any]:
|
|
19
|
-
return self._client.mail_request("GET", f"/mail/audiences/{quote(audience_id)}")
|
|
19
|
+
return self._client.mail_request("GET", f"/mail/audiences/{quote(audience_id, safe='')}")
|
|
20
20
|
|
|
21
21
|
def members(self, audience_id: str) -> dict[str, Any]:
|
|
22
|
-
return self._client.mail_request("GET", f"/mail/audiences/{quote(audience_id)}/members")
|
|
22
|
+
return self._client.mail_request("GET", f"/mail/audiences/{quote(audience_id, safe='')}/members")
|
|
23
23
|
|
|
24
24
|
def import_members(self, audience_id: str, members: list[dict[str, Any]]) -> dict[str, Any]:
|
|
25
25
|
return self._client.mail_request(
|
|
26
26
|
"POST",
|
|
27
|
-
f"/mail/audiences/{quote(audience_id)}/imports",
|
|
27
|
+
f"/mail/audiences/{quote(audience_id, safe='')}/imports",
|
|
28
28
|
json={"members": members},
|
|
29
29
|
)
|
|
30
30
|
|
|
31
|
+
def preview_import(
|
|
32
|
+
self, audience_id: str, members: list[dict[str, Any]], *, mode: str = "recorded_consent"
|
|
33
|
+
) -> dict[str, Any]:
|
|
34
|
+
return self._client.mail_request(
|
|
35
|
+
"POST", f"/mail/audiences/{quote(audience_id, safe='')}/imports/preview",
|
|
36
|
+
json={"members": members, "mode": mode},
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def import_detailed(
|
|
40
|
+
self, audience_id: str, members: list[dict[str, Any]], *, mode: str = "recorded_consent"
|
|
41
|
+
) -> dict[str, Any]:
|
|
42
|
+
return self._client.mail_request(
|
|
43
|
+
"POST", f"/mail/audiences/{quote(audience_id, safe='')}/imports",
|
|
44
|
+
json={"members": members, "mode": mode},
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def set_editable_fields(self, audience_id: str, fields: list[str]) -> dict[str, Any]:
|
|
48
|
+
return self._client.mail_request(
|
|
49
|
+
"PUT", f"/mail/audiences/{quote(audience_id, safe='')}/preferences",
|
|
50
|
+
json={"editableFields": fields},
|
|
51
|
+
)
|
|
52
|
+
|
|
31
53
|
|
|
32
54
|
class MailCampaignsClient:
|
|
33
55
|
def __init__(self, client: Any) -> None:
|
|
@@ -38,9 +60,16 @@ class MailCampaignsClient:
|
|
|
38
60
|
*,
|
|
39
61
|
name: str,
|
|
40
62
|
audience_id: str,
|
|
41
|
-
template: str,
|
|
42
|
-
from_: str,
|
|
63
|
+
template: str = "",
|
|
64
|
+
from_: str = "",
|
|
43
65
|
version_id: str | None = None,
|
|
66
|
+
content: dict[str, Any] | None = None,
|
|
67
|
+
variant_b_content: dict[str, Any] | None = None,
|
|
68
|
+
variant_b_percent: int = 0,
|
|
69
|
+
segment_id: str | None = None,
|
|
70
|
+
reply_to: str | None = None,
|
|
71
|
+
tracking: dict[str, bool] | None = None,
|
|
72
|
+
simulation: dict[str, str] | None = None,
|
|
44
73
|
) -> dict[str, Any]:
|
|
45
74
|
payload = {
|
|
46
75
|
"name": name,
|
|
@@ -48,6 +77,9 @@ class MailCampaignsClient:
|
|
|
48
77
|
"template": template,
|
|
49
78
|
"from": from_,
|
|
50
79
|
"versionId": version_id,
|
|
80
|
+
"content": content, "variantBContent": variant_b_content,
|
|
81
|
+
"variantBPercent": variant_b_percent, "segmentId": segment_id,
|
|
82
|
+
"replyTo": reply_to, "tracking": tracking, "simulation": simulation,
|
|
51
83
|
}
|
|
52
84
|
return self._client.mail_request("POST", "/mail/campaigns", json={key: value for key, value in payload.items() if value is not None})
|
|
53
85
|
|
|
@@ -57,5 +89,20 @@ class MailCampaignsClient:
|
|
|
57
89
|
def get(self, campaign_id: str) -> dict[str, Any]:
|
|
58
90
|
return self._client.mail_request("GET", f"/mail/campaigns/{quote(campaign_id)}")
|
|
59
91
|
|
|
60
|
-
def send(self, campaign_id: str) -> dict[str, Any]:
|
|
61
|
-
return self._client.mail_request("POST", f"/mail/campaigns/{quote(campaign_id)}/send")
|
|
92
|
+
def send(self, campaign_id: str, *, revision: int | None = None, approved_max_kobo: int | None = None) -> dict[str, Any]:
|
|
93
|
+
return self._client.mail_request("POST", f"/mail/campaigns/{quote(campaign_id, safe='')}/send", json={key: value for key, value in {"revision": revision, "approvedMaxKobo": approved_max_kobo}.items() if value is not None})
|
|
94
|
+
|
|
95
|
+
def update(self, campaign_id: str, *, revision: int, **draft: Any) -> dict[str, Any]:
|
|
96
|
+
return self._client.mail_request("PUT", f"/mail/campaigns/{quote(campaign_id, safe='')}", json={**draft, "revision": revision})
|
|
97
|
+
|
|
98
|
+
def preview(self, campaign_id: str, data: dict[str, Any] | None = None, variant: str = "a") -> dict[str, Any]:
|
|
99
|
+
return self._client.mail_request("POST", f"/mail/campaigns/{quote(campaign_id, safe='')}/preview", json={"data": data or {}, "variant": variant})
|
|
100
|
+
|
|
101
|
+
def test(self, campaign_id: str, to: str, data: dict[str, Any] | None = None, variant: str = "a", simulation: dict[str, str] | None = None) -> dict[str, Any]:
|
|
102
|
+
return self._client.mail_request("POST", f"/mail/campaigns/{quote(campaign_id, safe='')}/test", json={"to": to, "data": data or {}, "variant": variant, **({"simulation": simulation} if simulation else {})})
|
|
103
|
+
|
|
104
|
+
def schedule(self, campaign_id: str, send_at: str, *, revision: int | None = None, approved_max_kobo: int | None = None) -> dict[str, Any]:
|
|
105
|
+
return self._client.mail_request("POST", f"/mail/campaigns/{quote(campaign_id, safe='')}/schedule", json={"sendAt": send_at, **({"revision": revision} if revision is not None else {}), **({"approvedMaxKobo": approved_max_kobo} if approved_max_kobo is not None else {})})
|
|
106
|
+
|
|
107
|
+
def estimate(self, campaign_id: str) -> dict[str, Any]:
|
|
108
|
+
return self._client.mail_request("GET", f"/mail/campaigns/{quote(campaign_id, safe='')}/estimate")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
class MailAutomationsClient:
|
|
4
|
+
def __init__(self, client: Any) -> None:
|
|
5
|
+
self._client = client
|
|
6
|
+
|
|
7
|
+
def create(self, input: dict[str, Any]) -> dict[str, Any]:
|
|
8
|
+
return self._client.mail_request("POST", "/mail/automations", json=input)
|
|
9
|
+
|
|
10
|
+
def list(self) -> dict[str, Any]:
|
|
11
|
+
return self._client.mail_request("GET", "/mail/automations")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MailFormsClient:
|
|
15
|
+
def __init__(self, client: Any) -> None:
|
|
16
|
+
self._client = client
|
|
17
|
+
|
|
18
|
+
def create(self, input: dict[str, Any]) -> dict[str, Any]:
|
|
19
|
+
return self._client.mail_request("POST", "/mail/forms", json=input)
|
|
20
|
+
|
|
21
|
+
def list(self) -> dict[str, Any]:
|
|
22
|
+
return self._client.mail_request("GET", "/mail/forms")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MailSegmentsClient:
|
|
26
|
+
def __init__(self, client: Any) -> None:
|
|
27
|
+
self._client = client
|
|
28
|
+
|
|
29
|
+
def create(self, input: dict[str, Any]) -> dict[str, Any]:
|
|
30
|
+
return self._client.mail_request("POST", "/mail/segments", json=input)
|
|
31
|
+
|
|
32
|
+
def list(self) -> dict[str, Any]:
|
|
33
|
+
return self._client.mail_request("GET", "/mail/segments")
|
|
34
|
+
|
|
35
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MailMigrationClient:
|
|
7
|
+
def __init__(self, client: Any) -> None:
|
|
8
|
+
self._client = client
|
|
9
|
+
|
|
10
|
+
def audit(self, domain: str, selectors: list[str] | None = None) -> dict[str, Any]:
|
|
11
|
+
return self._client.request("GET", "/mail/audit", params={"domain": domain, "selectors": ",".join(selectors or [])})
|
|
12
|
+
|
|
13
|
+
def plan(self, domain: str, provider: str, selectors: list[str] | None = None) -> dict[str, Any]:
|
|
14
|
+
return self._client.request("GET", "/mail/migration-plan", params={"domain": domain, "provider": provider, "selectors": ",".join(selectors or [])})
|
|
15
|
+
|
|
16
|
+
def checklist(self, domain: str, provider: str) -> dict[str, Any]:
|
|
17
|
+
return self._client.mail_request("GET", "/mail/migration/checklist", params={"domain": domain, "provider": provider})
|
|
18
|
+
|
|
19
|
+
def save_checklist(self, domain: str, provider: str, completed: list[str], *, revision: int) -> dict[str, Any]:
|
|
20
|
+
return self._client.mail_request("PUT", "/mail/migration/checklist", json={"domain": domain, "provider": provider, "completed": completed, "revision": revision})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from urllib.parse import quote
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MailTrackingClient:
|
|
8
|
+
def __init__(self, client: Any) -> None:
|
|
9
|
+
self._client = client
|
|
10
|
+
|
|
11
|
+
def settings(self) -> dict[str, Any]:
|
|
12
|
+
return self._client.mail_request("GET", "/mail/tracking/settings")
|
|
13
|
+
|
|
14
|
+
def update_settings(self, *, opens: bool, clicks: bool) -> dict[str, Any]:
|
|
15
|
+
return self._client.mail_request("PUT", "/mail/tracking/settings", json={"opens": opens, "clicks": clicks})
|
|
16
|
+
|
|
17
|
+
def message(self, message_id: str) -> dict[str, Any]:
|
|
18
|
+
return self._client.mail_request("GET", f"/mail/messages/{quote(message_id, safe='')}/engagement")
|
|
19
|
+
|
|
20
|
+
def campaign(self, campaign_id: str) -> dict[str, Any]:
|
|
21
|
+
return self._client.mail_request("GET", f"/mail/campaigns/{quote(campaign_id, safe='')}/engagement")
|
|
22
|
+
|
|
23
|
+
def domains(self) -> dict[str, Any]:
|
|
24
|
+
return self._client.mail_request("GET", "/mail/tracking/domains")
|
|
25
|
+
|
|
26
|
+
def create_domain(self, domain_id: str, hostname: str) -> dict[str, Any]:
|
|
27
|
+
return self._client.mail_request("POST", "/mail/tracking/domains", json={"domainId": domain_id, "hostname": hostname})
|
|
28
|
+
|
|
29
|
+
def verify_domain(self, domain_id: str) -> dict[str, Any]:
|
|
30
|
+
return self._client.mail_request("POST", f"/mail/tracking/domains/{quote(domain_id, safe='')}/verify")
|
|
31
|
+
|
|
32
|
+
def update_domain(self, domain_id: str, *, enabled: bool, revoke: bool = False) -> None:
|
|
33
|
+
self._client.mail_request("PUT", f"/mail/tracking/domains/{quote(domain_id, safe='')}", json={"enabled": enabled, "revoke": revoke})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from urllib.parse import quote
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class WorkloadSecurityClient:
|
|
8
|
+
def __init__(self, client: Any) -> None:
|
|
9
|
+
self._client = client
|
|
10
|
+
|
|
11
|
+
@staticmethod
|
|
12
|
+
def _root(project_id: str) -> str:
|
|
13
|
+
return f"/projects/{quote(project_id, safe='')}/security"
|
|
14
|
+
|
|
15
|
+
def admissions(self, project_id: str, *, limit: int | None = None) -> dict[str, Any]:
|
|
16
|
+
return self._client.request("GET", f"{self._root(project_id)}/admissions", params=_params(limit=limit))
|
|
17
|
+
|
|
18
|
+
def policy(self, project_id: str, *, environment: str | None = None) -> dict[str, Any]:
|
|
19
|
+
return self._client.request("GET", f"{self._root(project_id)}/policy", params=_params(environment=environment))
|
|
20
|
+
|
|
21
|
+
def dependencies(self, project_id: str, *, environment: str | None = None) -> dict[str, Any]:
|
|
22
|
+
return self._client.request("GET", f"{self._root(project_id)}/dependencies", params=_params(environment=environment))
|
|
23
|
+
|
|
24
|
+
def declare_dependency(self, project_id: str, dependency: dict[str, Any]) -> dict[str, Any]:
|
|
25
|
+
return self._client.request("POST", f"{self._root(project_id)}/dependencies", json=dependency)
|
|
26
|
+
|
|
27
|
+
def refresh_dependencies(self, project_id: str, environment: str) -> dict[str, Any]:
|
|
28
|
+
return self._client.request(
|
|
29
|
+
"POST",
|
|
30
|
+
f"{self._root(project_id)}/dependencies/refresh",
|
|
31
|
+
json={"environment": environment},
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def grants(self, project_id: str, *, environment: str | None = None) -> dict[str, Any]:
|
|
35
|
+
return self._client.request("GET", f"{self._root(project_id)}/grants", params=_params(environment=environment))
|
|
36
|
+
|
|
37
|
+
def request_grant(self, project_id: str, grant: dict[str, Any]) -> dict[str, Any]:
|
|
38
|
+
return self._client.request("POST", f"{self._root(project_id)}/grants", json=grant)
|
|
39
|
+
|
|
40
|
+
def private_routes(self, project_id: str, *, environment: str | None = None) -> dict[str, Any]:
|
|
41
|
+
return self._client.request("GET", f"{self._root(project_id)}/private-routes", params=_params(environment=environment))
|
|
42
|
+
|
|
43
|
+
def request_private_route(self, project_id: str, route: dict[str, Any]) -> dict[str, Any]:
|
|
44
|
+
return self._client.request("POST", f"{self._root(project_id)}/private-routes", json=route)
|
|
45
|
+
|
|
46
|
+
def incidents(
|
|
47
|
+
self,
|
|
48
|
+
project_id: str,
|
|
49
|
+
*,
|
|
50
|
+
status: str | None = None,
|
|
51
|
+
limit: int | None = None,
|
|
52
|
+
) -> dict[str, Any]:
|
|
53
|
+
return self._client.request(
|
|
54
|
+
"GET",
|
|
55
|
+
f"{self._root(project_id)}/incidents",
|
|
56
|
+
params=_params(status=status, limit=limit),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def incident(self, project_id: str, incident_id: str) -> dict[str, Any]:
|
|
60
|
+
return self._client.request(
|
|
61
|
+
"GET",
|
|
62
|
+
f"{self._root(project_id)}/incidents/{quote(incident_id, safe='')}",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _params(**values: Any) -> dict[str, Any]:
|
|
67
|
+
return {key: value for key, value in values.items() if value not in (None, "")}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stackshift
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Official Python SDK for StackShift.
|
|
5
5
|
Requires-Python: >=3.9
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -16,6 +16,27 @@ Official Python SDK for StackShift.
|
|
|
16
16
|
pip install stackshift
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
## AI credits
|
|
20
|
+
|
|
21
|
+
AI credits are account-scoped. Use the shared client to inspect usage, start a top-up, or select managed billing versus your own provider key:
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from stackshift import StackShift
|
|
25
|
+
|
|
26
|
+
stackshift = StackShift()
|
|
27
|
+
balance = stackshift.ai_credits.balance()
|
|
28
|
+
usage = stackshift.ai_credits.usage(product="assets", limit=25)
|
|
29
|
+
quote = stackshift.ai_credits.quote(1_000, "USD")
|
|
30
|
+
checkout = stackshift.ai_credits.checkout(
|
|
31
|
+
quote["id"],
|
|
32
|
+
"unique-checkout-id",
|
|
33
|
+
"paystack",
|
|
34
|
+
)
|
|
35
|
+
stackshift.ai_credits.set_funding("assets", "managed")
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Keep account API keys and checkout creation on your server.
|
|
39
|
+
|
|
19
40
|
## Send email
|
|
20
41
|
|
|
21
42
|
```python
|
|
@@ -213,3 +234,51 @@ upload = stackshift.assets.signed_upload_url(
|
|
|
213
234
|
Then upload the browser `File` to `upload["url"]` from your frontend. Do not expose your StackShift API key to browser code.
|
|
214
235
|
|
|
215
236
|
The SDK only talks to the StackShift REST API. Storage placement, replication, disks, and repair are StackShift internals.
|
|
237
|
+
|
|
238
|
+
### Campaign drafts and engagement
|
|
239
|
+
|
|
240
|
+
`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.
|
|
241
|
+
|
|
242
|
+
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.
|
|
243
|
+
|
|
244
|
+
### Isolated testing, brands, migration, and spending
|
|
245
|
+
|
|
246
|
+
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.
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
brand = client.mail.brands.create(name="Product", editable_fields=["company"])
|
|
250
|
+
audience = client.mail.audiences.create(brand_id=brand["id"], name="Product news")
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
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.
|
|
254
|
+
|
|
255
|
+
`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.
|
|
256
|
+
|
|
257
|
+
## Native video, governed DAM and product rendering
|
|
258
|
+
|
|
259
|
+
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.
|
|
260
|
+
|
|
261
|
+
```python
|
|
262
|
+
dam = sdk.assets.dam.for_space(space_id)
|
|
263
|
+
video = sdk.assets.video.for_space(space_id)
|
|
264
|
+
schema = dam.create_metadata_schema({"name": "Products", "fields": [{
|
|
265
|
+
"key": "product_name", "label": "Product name", "type": "text", "required": True,
|
|
266
|
+
}]})
|
|
267
|
+
published = dam.publish_metadata_schema(schema["id"], schema["revision"])
|
|
268
|
+
workspace = video.workspace(asset_id)
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
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`.
|
|
272
|
+
|
|
273
|
+
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.
|
|
274
|
+
|
|
275
|
+
## Assets storage and Cloudinary migration
|
|
276
|
+
|
|
277
|
+
`sdk.assets.platform.for_space(space_id)` exposes `connections`, `storage`, and `migrations`. Use a verified Cloudinary connection, run a dry migration first, inspect its report, then start the transfer with the same folder prefix and conflict policy.
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
platform = sdk.assets.platform.for_space(space_id)
|
|
281
|
+
preview = platform.migrations.start({"source_connection_id": connection_id,
|
|
282
|
+
"destination_bucket_id": bucket_id, "source_prefix": "campaigns/2026", "dry_run": True})
|
|
283
|
+
report = platform.migrations.report(preview["id"])
|
|
284
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
stackshift/__init__.py,sha256=6w_s5ELWH4aYV4NXysotLKIX1XdHif0ny_3WGaiGZSY,1425
|
|
2
|
+
stackshift/ai_credits.py,sha256=2u5zGSBo_spHqgYJhtmChaEiBq9jUVaTjR1uprxMpNQ,1142
|
|
3
|
+
stackshift/assets.py,sha256=qjV59NFxQEF6_56GdMZVrN_QadExFbDWbcbiBFYXCt0,23292
|
|
4
|
+
stackshift/assets_dam.py,sha256=dz0QgSZuQcwSSYoWFqy1pCx3xgMGYZLc9tCcQE0_kYo,6659
|
|
5
|
+
stackshift/assets_media.py,sha256=S_7TWu6hm5I8WUPMfQsGE9U15mDiYRG4ygXJzWqwbDE,1523
|
|
6
|
+
stackshift/assets_platform.py,sha256=w2Eke0YFXinmtHt1oaZlaiP_J32VymfhVWWLOYrX93M,4125
|
|
7
|
+
stackshift/assets_video.py,sha256=KBIOTsVspHcMltkELsbTmfu-VB6_36dpYQ7HKTqAa2E,2374
|
|
8
|
+
stackshift/assets_workflows.py,sha256=6L8_1d9huhfSrKovwP26tfGKc38o0sbSM-75dt__HRE,6950
|
|
9
|
+
stackshift/client.py,sha256=4osuRDTVvBoZGKKhYBmZkx-oCAezg9zWxB2olXlnqsM,3220
|
|
10
|
+
stackshift/mail.py,sha256=Ny8aEE846vzanT0B8uqyBB0BGm1p4OmERC0zX_6mGPg,15989
|
|
11
|
+
stackshift/mail_advanced.py,sha256=sLte3we0Oy3pSuwjl5c4lfELH6gzaSUKug_WZDUB2DE,4363
|
|
12
|
+
stackshift/mail_billing.py,sha256=n1SMNTHW5lVs7phJhZBiKw6ok-T3kbW0A3-4uF1w6Yg,232
|
|
13
|
+
stackshift/mail_brands.py,sha256=aaOzETtfix0pFpaWBOHYPVrfUZezWqt53ZXJQk1F7mY,1627
|
|
14
|
+
stackshift/mail_campaigns.py,sha256=WAMMQxKt7BvsPzJuJE-uCiLFoyJ1RKh_pD3azWINGXA,5424
|
|
15
|
+
stackshift/mail_events_webhooks.py,sha256=iVYz_5DvGaE-Acw5uh_JN7P5LWvgfN-2bLnb_z1CMRU,5712
|
|
16
|
+
stackshift/mail_exports.py,sha256=sbwBbypS1APv2DnunYmyViEjUHP1PMSrM0tVcbxcits,1054
|
|
17
|
+
stackshift/mail_marketing.py,sha256=7uMZQYjU0siMyqFas_i5YWJTrwT9eYMrWElRThxZMr4,1077
|
|
18
|
+
stackshift/mail_migration.py,sha256=j-VvEbJP92ndQ5HMhdu7obJ4S_4gka7oqNRirIvYkyE,1121
|
|
19
|
+
stackshift/mail_reputation.py,sha256=mPvPkZeqcQc5808PhBK7gYsgf4Fk_U19nRxSsJ60MGU,1112
|
|
20
|
+
stackshift/mail_streams.py,sha256=F7iBzHtWhCFw1cTjwPixNJfCvxdjvsDprZQG2A9-NIs,504
|
|
21
|
+
stackshift/mail_tracking.py,sha256=SRcWTI9Otm5fBIY7nJJ-5qZA13aybUuMdh_PqakBpUg,1575
|
|
22
|
+
stackshift/projects.py,sha256=oYWaE7xNRR6OXuPEwJH-oFFeOmDA9ozq6jJY09ANIHA,498
|
|
23
|
+
stackshift/workload_security.py,sha256=5JtJH9fX1kqSY3ipXUa79JslUULWsN5_gyn9tTGfzlA,2941
|
|
24
|
+
stackshift-1.2.0.dist-info/METADATA,sha256=DZCSjepFeKmKNajeXmUZbNsMaxoG8en0kcxOit3JCPk,10250
|
|
25
|
+
stackshift-1.2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
26
|
+
stackshift-1.2.0.dist-info/top_level.txt,sha256=qe3uWsu_O9mNtq7bIck9Y8rjTIMrH0kzgAlGFDoRGqs,11
|
|
27
|
+
stackshift-1.2.0.dist-info/RECORD,,
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
stackshift/__init__.py,sha256=QJxD-b_E7P0_6s04v1GhbyyaXLweNohIZPnwmS-jUAc,1278
|
|
2
|
-
stackshift/assets.py,sha256=h7PjWQzBVtbpBn0_treRVfPXQw0rJCXBCXVBkC8jIhs,23021
|
|
3
|
-
stackshift/assets_workflows.py,sha256=6L8_1d9huhfSrKovwP26tfGKc38o0sbSM-75dt__HRE,6950
|
|
4
|
-
stackshift/client.py,sha256=myLN4B_SsolilJWpdlTZv8xwYnj1q-xOmjcSaF_P0mM,3000
|
|
5
|
-
stackshift/mail.py,sha256=akjk8ATJLl7Wv8b2re9LPL5n2Vjvjnz8HK6yNhP6Vj0,14863
|
|
6
|
-
stackshift/mail_advanced.py,sha256=e7Z0SCvonNaS36BnEfhioUh9fnZDU2vcIKmqc1Mqmvo,4192
|
|
7
|
-
stackshift/mail_campaigns.py,sha256=-ZQV88gY1PsuKNA4KYcKVqS1To39EOukKu1Is9qRCPU,2198
|
|
8
|
-
stackshift/mail_events_webhooks.py,sha256=iVYz_5DvGaE-Acw5uh_JN7P5LWvgfN-2bLnb_z1CMRU,5712
|
|
9
|
-
stackshift/mail_exports.py,sha256=sbwBbypS1APv2DnunYmyViEjUHP1PMSrM0tVcbxcits,1054
|
|
10
|
-
stackshift/mail_reputation.py,sha256=mPvPkZeqcQc5808PhBK7gYsgf4Fk_U19nRxSsJ60MGU,1112
|
|
11
|
-
stackshift/mail_streams.py,sha256=F7iBzHtWhCFw1cTjwPixNJfCvxdjvsDprZQG2A9-NIs,504
|
|
12
|
-
stackshift/projects.py,sha256=oYWaE7xNRR6OXuPEwJH-oFFeOmDA9ozq6jJY09ANIHA,498
|
|
13
|
-
stackshift-1.0.1.dist-info/METADATA,sha256=B2WaO3NpgVHvZcIcT05RA_MRd5rouC9wlB93AwrQXME,5893
|
|
14
|
-
stackshift-1.0.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
15
|
-
stackshift-1.0.1.dist-info/top_level.txt,sha256=qe3uWsu_O9mNtq7bIck9Y8rjTIMrH0kzgAlGFDoRGqs,11
|
|
16
|
-
stackshift-1.0.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|