stackshift 1.2.1__tar.gz → 1.3.1__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.
Files changed (55) hide show
  1. {stackshift-1.2.1 → stackshift-1.3.1}/PKG-INFO +66 -1
  2. {stackshift-1.2.1 → stackshift-1.3.1}/README.md +65 -0
  3. {stackshift-1.2.1 → stackshift-1.3.1}/pyproject.toml +1 -1
  4. {stackshift-1.2.1 → stackshift-1.3.1}/stackshift/__init__.py +26 -0
  5. {stackshift-1.2.1 → stackshift-1.3.1}/stackshift/assets.py +17 -0
  6. {stackshift-1.2.1 → stackshift-1.3.1}/stackshift/assets_workflows.py +8 -0
  7. {stackshift-1.2.1 → stackshift-1.3.1}/stackshift/client.py +3 -1
  8. {stackshift-1.2.1 → stackshift-1.3.1}/stackshift/mail.py +16 -0
  9. {stackshift-1.2.1 → stackshift-1.3.1}/stackshift/mail_billing.py +3 -0
  10. {stackshift-1.2.1 → stackshift-1.3.1}/stackshift/mail_brands.py +1 -1
  11. {stackshift-1.2.1 → stackshift-1.3.1}/stackshift/mail_campaigns.py +7 -5
  12. {stackshift-1.2.1 → stackshift-1.3.1}/stackshift/mail_events_webhooks.py +9 -0
  13. stackshift-1.3.1/stackshift/mail_mailboxes.py +276 -0
  14. stackshift-1.3.1/stackshift/partner.py +93 -0
  15. stackshift-1.3.1/stackshift/support_action_manifest.py +40 -0
  16. stackshift-1.3.1/stackshift/support_action_receiver.py +58 -0
  17. stackshift-1.3.1/stackshift/support_action_sqlite.py +46 -0
  18. stackshift-1.3.1/stackshift/support_action_types.py +83 -0
  19. stackshift-1.3.1/stackshift/support_actions.py +141 -0
  20. {stackshift-1.2.1 → stackshift-1.3.1}/stackshift.egg-info/PKG-INFO +66 -1
  21. {stackshift-1.2.1 → stackshift-1.3.1}/stackshift.egg-info/SOURCES.txt +11 -1
  22. {stackshift-1.2.1 → stackshift-1.3.1}/tests/test_assets_workflows.py +17 -0
  23. {stackshift-1.2.1 → stackshift-1.3.1}/tests/test_mail.py +3 -3
  24. {stackshift-1.2.1 → stackshift-1.3.1}/tests/test_mail_events_webhooks.py +19 -0
  25. stackshift-1.3.1/tests/test_mail_mailboxes.py +91 -0
  26. {stackshift-1.2.1 → stackshift-1.3.1}/tests/test_mail_platform.py +24 -0
  27. stackshift-1.3.1/tests/test_partner.py +82 -0
  28. stackshift-1.3.1/tests/test_support_actions.py +122 -0
  29. {stackshift-1.2.1 → stackshift-1.3.1}/setup.cfg +0 -0
  30. {stackshift-1.2.1 → stackshift-1.3.1}/stackshift/ai_credits.py +0 -0
  31. {stackshift-1.2.1 → stackshift-1.3.1}/stackshift/assets_dam.py +0 -0
  32. {stackshift-1.2.1 → stackshift-1.3.1}/stackshift/assets_media.py +0 -0
  33. {stackshift-1.2.1 → stackshift-1.3.1}/stackshift/assets_platform.py +0 -0
  34. {stackshift-1.2.1 → stackshift-1.3.1}/stackshift/assets_video.py +0 -0
  35. {stackshift-1.2.1 → stackshift-1.3.1}/stackshift/mail_advanced.py +0 -0
  36. {stackshift-1.2.1 → stackshift-1.3.1}/stackshift/mail_exports.py +0 -0
  37. {stackshift-1.2.1 → stackshift-1.3.1}/stackshift/mail_marketing.py +0 -0
  38. {stackshift-1.2.1 → stackshift-1.3.1}/stackshift/mail_migration.py +0 -0
  39. {stackshift-1.2.1 → stackshift-1.3.1}/stackshift/mail_reputation.py +0 -0
  40. {stackshift-1.2.1 → stackshift-1.3.1}/stackshift/mail_streams.py +0 -0
  41. {stackshift-1.2.1 → stackshift-1.3.1}/stackshift/mail_tracking.py +0 -0
  42. {stackshift-1.2.1 → stackshift-1.3.1}/stackshift/projects.py +0 -0
  43. {stackshift-1.2.1 → stackshift-1.3.1}/stackshift/workload_security.py +0 -0
  44. {stackshift-1.2.1 → stackshift-1.3.1}/stackshift.egg-info/dependency_links.txt +0 -0
  45. {stackshift-1.2.1 → stackshift-1.3.1}/stackshift.egg-info/requires.txt +0 -0
  46. {stackshift-1.2.1 → stackshift-1.3.1}/stackshift.egg-info/top_level.txt +0 -0
  47. {stackshift-1.2.1 → stackshift-1.3.1}/tests/test_ai_credits.py +0 -0
  48. {stackshift-1.2.1 → stackshift-1.3.1}/tests/test_assets_media.py +0 -0
  49. {stackshift-1.2.1 → stackshift-1.3.1}/tests/test_assets_platform.py +0 -0
  50. {stackshift-1.2.1 → stackshift-1.3.1}/tests/test_mail_advanced.py +0 -0
  51. {stackshift-1.2.1 → stackshift-1.3.1}/tests/test_mail_diagnostics.py +0 -0
  52. {stackshift-1.2.1 → stackshift-1.3.1}/tests/test_mail_exports.py +0 -0
  53. {stackshift-1.2.1 → stackshift-1.3.1}/tests/test_mail_simulation.py +0 -0
  54. {stackshift-1.2.1 → stackshift-1.3.1}/tests/test_mail_subscribers.py +0 -0
  55. {stackshift-1.2.1 → stackshift-1.3.1}/tests/test_mail_tracking.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackshift
3
- Version: 1.2.1
3
+ Version: 1.3.1
4
4
  Summary: Official Python SDK for StackShift.
5
5
  Requires-Python: >=3.9
6
6
  Description-Content-Type: text/markdown
@@ -16,6 +16,25 @@ Official Python SDK for StackShift.
16
16
  pip install stackshift
17
17
  ```
18
18
 
19
+ ## Partner platforms
20
+
21
+ Staff-enabled partner accounts buy domains for merchants from their StackShift wallet. Use an API token with the `partner:manage` scope (and `mail:manage` to run campaigns) on your server:
22
+
23
+ ```python
24
+ domain = stackshift.partner.purchase_domain(
25
+ domain="merchantshop.com",
26
+ years=1,
27
+ merchant_ref="m_123",
28
+ registrant={"first_name": "Ada", "last_name": "Obi", "email": "ada@merchantshop.com", "phone": "+2348012345678", "address": "1 Marina", "city": "Lagos", "state": "Lagos", "postal_code": "101001", "country": "NG"},
29
+ brand={"name": "Merchant Shop", "postalAddress": "1 Marina, Lagos, Nigeria"},
30
+ replies={"mode": "forward", "forwardTo": "ada@gmail.com"},
31
+ idempotency_key="order-m_123-1",
32
+ )
33
+ status = stackshift.partner.get_domain(domain["id"])
34
+ ```
35
+
36
+ `partner` also exposes `overview`, `update_settings`, `list_domains` (with `q` and `attention` to find the merchants that need you), `retry_domain`, `get_diagnosis`, `restore_dns`, `transfer_out`, `resume_brand`, `get_complaints`, `list_complaint_sources`, `suppress_never_opened`, `undo_suppression`, `usage`, `list_requests` and `create_request`. `transfer_out` needs the `partner:transfer` scope (or `*`); `partner:manage` alone is refused. Purchases are capped at 30 per minute and, per partner, at 25 domains in a rolling 24 hours (429 `daily_purchase_limit`; staff can raise it). Validation errors are 400, state conflicts are 409. The `brand` and `replies` dictionaries use API camelCase keys.
37
+
19
38
  ## AI credits
20
39
 
21
40
  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:
@@ -101,6 +120,35 @@ valid = stackshift.mail.webhooks.verify_signature(
101
120
 
102
121
  Webhook handlers should be idempotent. StackShift retries non-2xx responses. Delivery events do not include full email bodies or OTP codes by default.
103
122
 
123
+ ## Mailboxes
124
+
125
+ Host real mailboxes on your own domain. The mailbox owner gets an invitation by email and sets their own password.
126
+
127
+ ```python
128
+ domain = stackshift.mail.mailbox_domains.create("acme.com")
129
+ print(domain["records"])
130
+ stackshift.mail.mailbox_domains.verify(domain["id"])
131
+
132
+ mailbox = stackshift.mail.mailboxes.create(
133
+ domain_id=domain["id"],
134
+ local_part="ada",
135
+ owner_email="ada@personal.example",
136
+ plan="basic",
137
+ )
138
+
139
+ stackshift.mail.aliases.create(mailbox_id=mailbox["id"], local_part="sales", send_as=True)
140
+ stackshift.mail.routes.create(
141
+ domain_id=domain["id"],
142
+ pattern="orders-*",
143
+ targets=[{"kind": "app", "webhookUrl": "https://app.example.com/inbound"}],
144
+ )
145
+
146
+ devices = stackshift.mail.mailboxes.list_devices(mailbox["id"])
147
+ stackshift.mail.mailboxes.suspend(mailbox["id"])
148
+ ```
149
+
150
+ `stackshift.mail.groups` manages team addresses, and `stackshift.mail.mailbox_imports` copies mail from Google, Microsoft or any IMAP server.
151
+
104
152
  ## Send a template
105
153
 
106
154
  Templates are rendered by StackShift servers. The Python SDK only calls the REST API.
@@ -282,3 +330,20 @@ preview = platform.migrations.start({"source_connection_id": connection_id,
282
330
  "destination_bucket_id": bucket_id, "source_prefix": "campaigns/2026", "dry_run": True})
283
331
  report = platform.migrations.report(preview["id"])
284
332
  ```
333
+
334
+ ## Support action receivers (unreleased)
335
+
336
+ `ActionManifest`, `ActionDescriptor`, installation consent types and `validate_action_catalog` describe `stackshift.support.app/v2`. V1 remains actionless. Registered writes require idempotency, a reconciliation endpoint and a registered read verifier; installations explicitly select `action_operations`. These SDK helpers perform no registration or network calls.
337
+
338
+ Use `receive_action(raw_bytes, headers, store=..., execute=..., reconcile=..., verification=...)`. Verification requires `key_id`, `secret` (the installation action secret, not webhook secret), `installation_id`, `actions`, and a trusted `expected_customer_external_id`. Do not derive that expectation from unverified request JSON. Each synchronous handler returns `(state, result_dict, optional_safe_error_code)`. Success output is checked against the registered flat schema. Receipt helpers `encode_receipt`, `sign_action`, and `verify_receipt` are also exported.
339
+
340
+ `SQLiteActionStore('/absolute/path/claims.sqlite')` uses standard-library sqlite3, WAL and FULL synchronization. Never use an in-memory database for restart safety. The runnable stdin receiver in `examples/support_action_receiver.py` intentionally returns `demo_not_implemented`; replace its handlers with authorized customer-bound business transactions. Set `ACTION_KEY_ID`, `ACTION_SECRET`, `ACTION_INSTALLATION_ID`, and trusted `EXPECTED_CUSTOMER_EXTERNAL_ID`:
341
+
342
+ ```sh
343
+ PYTHONPATH=. python examples/support_action_receiver.py /absolute/path/claims.sqlite headers.json actions.json < body.json
344
+ .venv/bin/python -m unittest discover -s tests -p test_support_actions.py -v
345
+ ```
346
+
347
+ Send the returned `body` unchanged with `headers`. Execution retries retain exact original bytes and request ID with fresh delivery IDs. A handler error is stored as unknown; running/unknown work is never dispatched again. Reconciliation is a separate handler with empty arguments and the original target request ID, and must query actual downstream state. Accepted results remain replayable; there is no asynchronous callback endpoint yet. Disk claims do not make remote side effects atomic: use downstream idempotency or one transactional business/receipt store, and preserve uncertain claims for reconciliation/human review. This is a single-host SQLite example; distributed receivers need a shared durable store.
348
+
349
+ See `../support-actions-conformance/README.md` for exact signatures and adapter requirements. Seven action tests pass, including shared vectors, two-connection concurrency, restart and fenced completion. The existing virtualenv is required here because system Python lacks the SDK's declared `requests` dependency. No live installation/provider qualification was performed; unreleased.
@@ -8,6 +8,25 @@ Official Python SDK for StackShift.
8
8
  pip install stackshift
9
9
  ```
10
10
 
11
+ ## Partner platforms
12
+
13
+ Staff-enabled partner accounts buy domains for merchants from their StackShift wallet. Use an API token with the `partner:manage` scope (and `mail:manage` to run campaigns) on your server:
14
+
15
+ ```python
16
+ domain = stackshift.partner.purchase_domain(
17
+ domain="merchantshop.com",
18
+ years=1,
19
+ merchant_ref="m_123",
20
+ registrant={"first_name": "Ada", "last_name": "Obi", "email": "ada@merchantshop.com", "phone": "+2348012345678", "address": "1 Marina", "city": "Lagos", "state": "Lagos", "postal_code": "101001", "country": "NG"},
21
+ brand={"name": "Merchant Shop", "postalAddress": "1 Marina, Lagos, Nigeria"},
22
+ replies={"mode": "forward", "forwardTo": "ada@gmail.com"},
23
+ idempotency_key="order-m_123-1",
24
+ )
25
+ status = stackshift.partner.get_domain(domain["id"])
26
+ ```
27
+
28
+ `partner` also exposes `overview`, `update_settings`, `list_domains` (with `q` and `attention` to find the merchants that need you), `retry_domain`, `get_diagnosis`, `restore_dns`, `transfer_out`, `resume_brand`, `get_complaints`, `list_complaint_sources`, `suppress_never_opened`, `undo_suppression`, `usage`, `list_requests` and `create_request`. `transfer_out` needs the `partner:transfer` scope (or `*`); `partner:manage` alone is refused. Purchases are capped at 30 per minute and, per partner, at 25 domains in a rolling 24 hours (429 `daily_purchase_limit`; staff can raise it). Validation errors are 400, state conflicts are 409. The `brand` and `replies` dictionaries use API camelCase keys.
29
+
11
30
  ## AI credits
12
31
 
13
32
  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:
@@ -93,6 +112,35 @@ valid = stackshift.mail.webhooks.verify_signature(
93
112
 
94
113
  Webhook handlers should be idempotent. StackShift retries non-2xx responses. Delivery events do not include full email bodies or OTP codes by default.
95
114
 
115
+ ## Mailboxes
116
+
117
+ Host real mailboxes on your own domain. The mailbox owner gets an invitation by email and sets their own password.
118
+
119
+ ```python
120
+ domain = stackshift.mail.mailbox_domains.create("acme.com")
121
+ print(domain["records"])
122
+ stackshift.mail.mailbox_domains.verify(domain["id"])
123
+
124
+ mailbox = stackshift.mail.mailboxes.create(
125
+ domain_id=domain["id"],
126
+ local_part="ada",
127
+ owner_email="ada@personal.example",
128
+ plan="basic",
129
+ )
130
+
131
+ stackshift.mail.aliases.create(mailbox_id=mailbox["id"], local_part="sales", send_as=True)
132
+ stackshift.mail.routes.create(
133
+ domain_id=domain["id"],
134
+ pattern="orders-*",
135
+ targets=[{"kind": "app", "webhookUrl": "https://app.example.com/inbound"}],
136
+ )
137
+
138
+ devices = stackshift.mail.mailboxes.list_devices(mailbox["id"])
139
+ stackshift.mail.mailboxes.suspend(mailbox["id"])
140
+ ```
141
+
142
+ `stackshift.mail.groups` manages team addresses, and `stackshift.mail.mailbox_imports` copies mail from Google, Microsoft or any IMAP server.
143
+
96
144
  ## Send a template
97
145
 
98
146
  Templates are rendered by StackShift servers. The Python SDK only calls the REST API.
@@ -274,3 +322,20 @@ preview = platform.migrations.start({"source_connection_id": connection_id,
274
322
  "destination_bucket_id": bucket_id, "source_prefix": "campaigns/2026", "dry_run": True})
275
323
  report = platform.migrations.report(preview["id"])
276
324
  ```
325
+
326
+ ## Support action receivers (unreleased)
327
+
328
+ `ActionManifest`, `ActionDescriptor`, installation consent types and `validate_action_catalog` describe `stackshift.support.app/v2`. V1 remains actionless. Registered writes require idempotency, a reconciliation endpoint and a registered read verifier; installations explicitly select `action_operations`. These SDK helpers perform no registration or network calls.
329
+
330
+ Use `receive_action(raw_bytes, headers, store=..., execute=..., reconcile=..., verification=...)`. Verification requires `key_id`, `secret` (the installation action secret, not webhook secret), `installation_id`, `actions`, and a trusted `expected_customer_external_id`. Do not derive that expectation from unverified request JSON. Each synchronous handler returns `(state, result_dict, optional_safe_error_code)`. Success output is checked against the registered flat schema. Receipt helpers `encode_receipt`, `sign_action`, and `verify_receipt` are also exported.
331
+
332
+ `SQLiteActionStore('/absolute/path/claims.sqlite')` uses standard-library sqlite3, WAL and FULL synchronization. Never use an in-memory database for restart safety. The runnable stdin receiver in `examples/support_action_receiver.py` intentionally returns `demo_not_implemented`; replace its handlers with authorized customer-bound business transactions. Set `ACTION_KEY_ID`, `ACTION_SECRET`, `ACTION_INSTALLATION_ID`, and trusted `EXPECTED_CUSTOMER_EXTERNAL_ID`:
333
+
334
+ ```sh
335
+ PYTHONPATH=. python examples/support_action_receiver.py /absolute/path/claims.sqlite headers.json actions.json < body.json
336
+ .venv/bin/python -m unittest discover -s tests -p test_support_actions.py -v
337
+ ```
338
+
339
+ Send the returned `body` unchanged with `headers`. Execution retries retain exact original bytes and request ID with fresh delivery IDs. A handler error is stored as unknown; running/unknown work is never dispatched again. Reconciliation is a separate handler with empty arguments and the original target request ID, and must query actual downstream state. Accepted results remain replayable; there is no asynchronous callback endpoint yet. Disk claims do not make remote side effects atomic: use downstream idempotency or one transactional business/receipt store, and preserve uncertain claims for reconciliation/human review. This is a single-host SQLite example; distributed receivers need a shared durable store.
340
+
341
+ See `../support-actions-conformance/README.md` for exact signatures and adapter requirements. Seven action tests pass, including shared vectors, two-connection concurrency, restart and fenced completion. The existing virtualenv is required here because system Python lacks the SDK's declared `requests` dependency. No live installation/provider qualification was performed; unreleased.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "stackshift"
7
- version = "1.2.1"
7
+ version = "1.3.1"
8
8
  description = "Official Python SDK for StackShift."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -13,6 +13,15 @@ from .mail_events_webhooks import MailEventsClient, MailWebhooksClient, verify_w
13
13
  from .mail_streams import MailStreamsClient
14
14
  from .mail_campaigns import MailAudiencesClient, MailCampaignsClient
15
15
  from .mail_exports import MailExportsClient
16
+ from .mail_mailboxes import (
17
+ MailboxAliasesClient,
18
+ MailboxDomainsClient,
19
+ MailboxGroupsClient,
20
+ MailboxImportsClient,
21
+ MailboxesClient,
22
+ MailboxRoutesClient,
23
+ )
24
+ from .partner import PartnerClient
16
25
  from .projects import ProjectsClient
17
26
  from .workload_security import WorkloadSecurityClient
18
27
 
@@ -35,6 +44,13 @@ __all__ = [
35
44
  "MailAudiencesClient",
36
45
  "MailCampaignsClient",
37
46
  "MailExportsClient",
47
+ "MailboxAliasesClient",
48
+ "MailboxDomainsClient",
49
+ "MailboxGroupsClient",
50
+ "MailboxImportsClient",
51
+ "MailboxesClient",
52
+ "MailboxRoutesClient",
53
+ "PartnerClient",
38
54
  "ProjectsClient",
39
55
  "WorkloadSecurityClient",
40
56
  "asset_transform_options",
@@ -42,3 +58,13 @@ __all__ = [
42
58
  "get_asset_transform_preset",
43
59
  "verify_webhook_signature",
44
60
  ]
61
+
62
+ from .support_actions import (ActionProtocolError, sign_action, verify_action, verify_receipt, encode_receipt)
63
+ from .support_action_receiver import ActionClaim, ActionDurableStore, SignedActionResponse, receive_action
64
+ from .support_action_sqlite import SQLiteActionStore
65
+ from .support_action_types import ActionDescriptor, ActionField, ActionManifest, ActionRequest, ActionReceipt
66
+ __all__ += ["ActionProtocolError", "sign_action", "verify_action", "verify_receipt", "encode_receipt", "ActionClaim", "ActionDurableStore", "SignedActionResponse", "receive_action", "SQLiteActionStore", "ActionDescriptor", "ActionField", "ActionManifest", "ActionRequest", "ActionReceipt"]
67
+ from .support_action_manifest import validate_action_catalog
68
+ __all__ += ['validate_action_catalog']
69
+ from .support_action_types import ActionInstallationInput, ActionInstallationCredentials
70
+ __all__ += ['ActionInstallationInput', 'ActionInstallationCredentials']
@@ -230,6 +230,23 @@ class AssetsClient(AssetWorkflowsMixin):
230
230
  headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
231
231
  return self._client.request("POST", path, headers=headers)
232
232
 
233
+ def usage(self, asset_id: str) -> dict[str, Any]:
234
+ return self._client.request("GET", f"/assets/{quote(asset_id, safe='')}/usage")
235
+
236
+ def hold(self, asset_id: str) -> dict[str, Any]:
237
+ return self._client.request("GET", f"/assets/{quote(asset_id, safe='')}/hold")
238
+
239
+ def create_reupload_request(
240
+ self, asset_id: str, *, email: str | None = None, message: str | None = None
241
+ ) -> dict[str, Any]:
242
+ payload = {key: value for key, value in {"email": email, "message": message}.items() if value}
243
+ return self._client.request(
244
+ "POST", f"/assets/{quote(asset_id, safe='')}/reupload-requests", json=payload
245
+ )
246
+
247
+ def reupload_requests(self, asset_id: str) -> dict[str, Any]:
248
+ return self._client.request("GET", f"/assets/{quote(asset_id, safe='')}/reupload-requests")
249
+
233
250
  def versions(self, asset_id: str) -> dict[str, Any]:
234
251
  return self._client.request("GET", f"/assets/{asset_id}/versions")
235
252
 
@@ -17,6 +17,14 @@ class AssetWorkflowsMixin:
17
17
  json={"name": name, "definition": definition, "eager": eager},
18
18
  )
19
19
 
20
+ def estimate_transform(
21
+ self, asset_id: str, version_id: str, definition: dict[str, Any]
22
+ ) -> dict[str, Any]:
23
+ return self._client.request(
24
+ "POST", f"/assets/{quote(asset_id, safe='')}/transforms/estimate",
25
+ json={"version_id": version_id, "definition": definition},
26
+ )
27
+
20
28
  def materialize_transform(
21
29
  self,
22
30
  asset_id: str,
@@ -12,7 +12,7 @@ class StackShift:
12
12
  *,
13
13
  api_key: str | None = None,
14
14
  base_url: str = "https://api.stackshift.cloud/api/v1",
15
- mail_base_url: str = "https://api.stackshift.cloud/v1",
15
+ mail_base_url: str = "https://api.stackshift.cloud/api/v1",
16
16
  cdn_base_url: str = "https://cdn.stackshift.cloud",
17
17
  session: requests.Session | None = None,
18
18
  ) -> None:
@@ -29,6 +29,7 @@ class StackShift:
29
29
  from .ai_credits import AICreditsClient
30
30
  from .assets import AssetsClient
31
31
  from .mail import MailClient
32
+ from .partner import PartnerClient
32
33
  from .projects import ProjectsClient
33
34
  from .workload_security import WorkloadSecurityClient
34
35
 
@@ -36,6 +37,7 @@ class StackShift:
36
37
  self.projects = ProjectsClient(self)
37
38
  self.assets = AssetsClient(self)
38
39
  self.mail = MailClient(self)
40
+ self.partner = PartnerClient(self)
39
41
  self.workload_security = WorkloadSecurityClient(self)
40
42
 
41
43
  def request(self, method: str, path: str, **kwargs: Any) -> Any:
@@ -13,6 +13,14 @@ from .mail_marketing import MailAutomationsClient, MailFormsClient, MailSegments
13
13
  from .mail_migration import MailMigrationClient
14
14
  from .mail_campaigns import MailAudiencesClient, MailCampaignsClient
15
15
  from .mail_exports import MailExportsClient
16
+ from .mail_mailboxes import (
17
+ MailboxAliasesClient,
18
+ MailboxDomainsClient,
19
+ MailboxGroupsClient,
20
+ MailboxImportsClient,
21
+ MailboxesClient,
22
+ MailboxRoutesClient,
23
+ )
16
24
  from .mail_tracking import MailTrackingClient
17
25
 
18
26
 
@@ -38,6 +46,12 @@ class MailClient:
38
46
  self.campaigns = MailCampaignsClient(client)
39
47
  self.tracking = MailTrackingClient(client)
40
48
  self.exports = MailExportsClient(client)
49
+ self.mailbox_domains = MailboxDomainsClient(client)
50
+ self.mailboxes = MailboxesClient(client)
51
+ self.aliases = MailboxAliasesClient(client)
52
+ self.groups = MailboxGroupsClient(client)
53
+ self.routes = MailboxRoutesClient(client)
54
+ self.mailbox_imports = MailboxImportsClient(client)
41
55
  self.limits = MailLimitsClient(client)
42
56
  self.reputation = MailReputationClient(client)
43
57
  self.scheduled = MailScheduledClient(client)
@@ -297,12 +311,14 @@ class MailSuppressionsClient:
297
311
  *,
298
312
  email: str | None = None,
299
313
  reason: str | None = None,
314
+ brand_id: str | None = None,
300
315
  limit: int | None = None,
301
316
  cursor: str | None = None,
302
317
  ) -> dict[str, Any]:
303
318
  params = {
304
319
  "email": email,
305
320
  "reason": reason,
321
+ "brandId": brand_id,
306
322
  "limit": limit,
307
323
  "cursor": cursor,
308
324
  }
@@ -7,3 +7,6 @@ class MailBillingClient:
7
7
 
8
8
  def summary(self) -> dict[str, Any]:
9
9
  return self._client.mail_request("GET", "/mail/billing")
10
+
11
+ def period_usage(self, period: str) -> dict[str, Any]:
12
+ return self._client.mail_request("GET", "/mail/billing", params={"period": period})
@@ -28,7 +28,7 @@ class MailBrandsClient:
28
28
 
29
29
 
30
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"}
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", "merchant_ref": "merchantRef"}
32
32
  unknown = fields.keys() - names.keys() - set(names.values())
33
33
  if unknown:
34
34
  raise ValueError("Unknown brand fields: " + ", ".join(sorted(unknown)))
@@ -12,8 +12,9 @@ class MailAudiencesClient:
12
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
- def list(self) -> dict[str, Any]:
16
- return self._client.mail_request("GET", "/mail/audiences")
15
+ def list(self, *, brand_id: str | None = None) -> dict[str, Any]:
16
+ params = {"brandId": brand_id} if brand_id else None
17
+ return self._client.mail_request("GET", "/mail/audiences", params=params)
17
18
 
18
19
  def get(self, audience_id: str) -> dict[str, Any]:
19
20
  return self._client.mail_request("GET", f"/mail/audiences/{quote(audience_id, safe='')}")
@@ -58,8 +59,8 @@ class MailCampaignsClient:
58
59
  def create(
59
60
  self,
60
61
  *,
61
- name: str,
62
62
  audience_id: str,
63
+ name: str | None = None,
63
64
  template: str = "",
64
65
  from_: str = "",
65
66
  version_id: str | None = None,
@@ -83,8 +84,9 @@ class MailCampaignsClient:
83
84
  }
84
85
  return self._client.mail_request("POST", "/mail/campaigns", json={key: value for key, value in payload.items() if value is not None})
85
86
 
86
- def list(self) -> dict[str, Any]:
87
- return self._client.mail_request("GET", "/mail/campaigns")
87
+ def list(self, *, brand_id: str | None = None) -> dict[str, Any]:
88
+ params = {"brandId": brand_id} if brand_id else None
89
+ return self._client.mail_request("GET", "/mail/campaigns", params=params)
88
90
 
89
91
  def get(self, campaign_id: str) -> dict[str, Any]:
90
92
  return self._client.mail_request("GET", f"/mail/campaigns/{quote(campaign_id)}")
@@ -79,6 +79,15 @@ class MailWebhooksClient:
79
79
  def rotate_secret(self, webhook_id: str) -> dict[str, Any]:
80
80
  return self._client.mail_request("POST", f"/mail/webhooks/{quote(webhook_id)}/rotate-secret")
81
81
 
82
+ def reveal_secret(self, webhook_id: str) -> dict[str, Any]:
83
+ return self._client.mail_request("POST", f"/mail/webhooks/{quote(webhook_id)}/reveal-secret")
84
+
85
+ def diagnosis(self, webhook_id: str) -> dict[str, Any]:
86
+ return self._client.mail_request("GET", f"/mail/webhooks/{quote(webhook_id)}/diagnosis")
87
+
88
+ def diagnose(self, webhook_id: str) -> dict[str, Any]:
89
+ return self._client.mail_request("POST", f"/mail/webhooks/{quote(webhook_id)}/diagnosis")
90
+
82
91
  def delete(self, webhook_id: str) -> None:
83
92
  self._client.mail_request("DELETE", f"/mail/webhooks/{quote(webhook_id)}")
84
93