stackshift 1.1.0__tar.gz → 1.2.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.
Files changed (45) hide show
  1. {stackshift-1.1.0 → stackshift-1.2.0}/PKG-INFO +33 -1
  2. {stackshift-1.1.0 → stackshift-1.2.0}/README.md +32 -0
  3. {stackshift-1.1.0 → stackshift-1.2.0}/pyproject.toml +1 -1
  4. {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/__init__.py +2 -0
  5. stackshift-1.2.0/stackshift/ai_credits.py +24 -0
  6. {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/assets.py +2 -0
  7. stackshift-1.2.0/stackshift/assets_platform.py +99 -0
  8. {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/client.py +2 -0
  9. {stackshift-1.1.0 → stackshift-1.2.0}/stackshift.egg-info/PKG-INFO +33 -1
  10. {stackshift-1.1.0 → stackshift-1.2.0}/stackshift.egg-info/SOURCES.txt +4 -0
  11. stackshift-1.2.0/tests/test_ai_credits.py +34 -0
  12. stackshift-1.2.0/tests/test_assets_platform.py +41 -0
  13. {stackshift-1.1.0 → stackshift-1.2.0}/setup.cfg +0 -0
  14. {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/assets_dam.py +0 -0
  15. {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/assets_media.py +0 -0
  16. {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/assets_video.py +0 -0
  17. {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/assets_workflows.py +0 -0
  18. {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/mail.py +0 -0
  19. {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/mail_advanced.py +0 -0
  20. {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/mail_billing.py +0 -0
  21. {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/mail_brands.py +0 -0
  22. {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/mail_campaigns.py +0 -0
  23. {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/mail_events_webhooks.py +0 -0
  24. {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/mail_exports.py +0 -0
  25. {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/mail_marketing.py +0 -0
  26. {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/mail_migration.py +0 -0
  27. {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/mail_reputation.py +0 -0
  28. {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/mail_streams.py +0 -0
  29. {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/mail_tracking.py +0 -0
  30. {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/projects.py +0 -0
  31. {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/workload_security.py +0 -0
  32. {stackshift-1.1.0 → stackshift-1.2.0}/stackshift.egg-info/dependency_links.txt +0 -0
  33. {stackshift-1.1.0 → stackshift-1.2.0}/stackshift.egg-info/requires.txt +0 -0
  34. {stackshift-1.1.0 → stackshift-1.2.0}/stackshift.egg-info/top_level.txt +0 -0
  35. {stackshift-1.1.0 → stackshift-1.2.0}/tests/test_assets_media.py +0 -0
  36. {stackshift-1.1.0 → stackshift-1.2.0}/tests/test_assets_workflows.py +0 -0
  37. {stackshift-1.1.0 → stackshift-1.2.0}/tests/test_mail.py +0 -0
  38. {stackshift-1.1.0 → stackshift-1.2.0}/tests/test_mail_advanced.py +0 -0
  39. {stackshift-1.1.0 → stackshift-1.2.0}/tests/test_mail_diagnostics.py +0 -0
  40. {stackshift-1.1.0 → stackshift-1.2.0}/tests/test_mail_events_webhooks.py +0 -0
  41. {stackshift-1.1.0 → stackshift-1.2.0}/tests/test_mail_exports.py +0 -0
  42. {stackshift-1.1.0 → stackshift-1.2.0}/tests/test_mail_platform.py +0 -0
  43. {stackshift-1.1.0 → stackshift-1.2.0}/tests/test_mail_simulation.py +0 -0
  44. {stackshift-1.1.0 → stackshift-1.2.0}/tests/test_mail_subscribers.py +0 -0
  45. {stackshift-1.1.0 → stackshift-1.2.0}/tests/test_mail_tracking.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackshift
3
- Version: 1.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
@@ -250,3 +271,14 @@ workspace = video.workspace(asset_id)
250
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`.
251
272
 
252
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
+ ```
@@ -8,6 +8,27 @@ Official Python SDK for StackShift.
8
8
  pip install stackshift
9
9
  ```
10
10
 
11
+ ## AI credits
12
+
13
+ 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:
14
+
15
+ ```python
16
+ from stackshift import StackShift
17
+
18
+ stackshift = StackShift()
19
+ balance = stackshift.ai_credits.balance()
20
+ usage = stackshift.ai_credits.usage(product="assets", limit=25)
21
+ quote = stackshift.ai_credits.quote(1_000, "USD")
22
+ checkout = stackshift.ai_credits.checkout(
23
+ quote["id"],
24
+ "unique-checkout-id",
25
+ "paystack",
26
+ )
27
+ stackshift.ai_credits.set_funding("assets", "managed")
28
+ ```
29
+
30
+ Keep account API keys and checkout creation on your server.
31
+
11
32
  ## Send email
12
33
 
13
34
  ```python
@@ -242,3 +263,14 @@ workspace = video.workspace(asset_id)
242
263
  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
264
 
244
265
  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.
266
+
267
+ ## Assets storage and Cloudinary migration
268
+
269
+ `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.
270
+
271
+ ```python
272
+ platform = sdk.assets.platform.for_space(space_id)
273
+ preview = platform.migrations.start({"source_connection_id": connection_id,
274
+ "destination_bucket_id": bucket_id, "source_prefix": "campaigns/2026", "dry_run": True})
275
+ report = platform.migrations.report(preview["id"])
276
+ ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "stackshift"
7
- version = "1.1.0"
7
+ version = "1.2.0"
8
8
  description = "Official Python SDK for StackShift."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -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,
@@ -17,6 +18,7 @@ from .workload_security import WorkloadSecurityClient
17
18
 
18
19
  __all__ = [
19
20
  "StackShift",
21
+ "AICreditsClient",
20
22
  "ASSET_TRANSFORM_PRESETS",
21
23
  "AssetsClient",
22
24
  "MailClient",
@@ -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})
@@ -9,6 +9,7 @@ from urllib.parse import quote
9
9
  from .assets_workflows import AssetWorkflowsMixin
10
10
  from .assets_dam import AssetDAMClient
11
11
  from .assets_video import AssetVideoClient
12
+ from .assets_platform import AssetPlatformClient
12
13
 
13
14
 
14
15
  ASSET_TRANSFORM_PRESETS: list[dict[str, Any]] = [
@@ -56,6 +57,7 @@ class AssetsClient(AssetWorkflowsMixin):
56
57
  self._client = client
57
58
  self.dam = AssetDAMClient(client)
58
59
  self.video = AssetVideoClient(client)
60
+ self.platform = AssetPlatformClient(client)
59
61
 
60
62
  def upload(
61
63
  self,
@@ -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)
@@ -26,11 +26,13 @@ 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
32
33
  from .workload_security import WorkloadSecurityClient
33
34
 
35
+ self.ai_credits = AICreditsClient(self)
34
36
  self.projects = ProjectsClient(self)
35
37
  self.assets = AssetsClient(self)
36
38
  self.mail = MailClient(self)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackshift
3
- Version: 1.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
@@ -250,3 +271,14 @@ workspace = video.workspace(asset_id)
250
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`.
251
272
 
252
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
+ ```
@@ -1,9 +1,11 @@
1
1
  README.md
2
2
  pyproject.toml
3
3
  stackshift/__init__.py
4
+ stackshift/ai_credits.py
4
5
  stackshift/assets.py
5
6
  stackshift/assets_dam.py
6
7
  stackshift/assets_media.py
8
+ stackshift/assets_platform.py
7
9
  stackshift/assets_video.py
8
10
  stackshift/assets_workflows.py
9
11
  stackshift/client.py
@@ -26,7 +28,9 @@ stackshift.egg-info/SOURCES.txt
26
28
  stackshift.egg-info/dependency_links.txt
27
29
  stackshift.egg-info/requires.txt
28
30
  stackshift.egg-info/top_level.txt
31
+ tests/test_ai_credits.py
29
32
  tests/test_assets_media.py
33
+ tests/test_assets_platform.py
30
34
  tests/test_assets_workflows.py
31
35
  tests/test_mail.py
32
36
  tests/test_mail_advanced.py
@@ -0,0 +1,34 @@
1
+ import unittest
2
+
3
+ from stackshift.ai_credits import AICreditsClient
4
+
5
+
6
+ class AICreditsClientTests(unittest.TestCase):
7
+ def test_account_credit_requests_preserve_checkout_and_funding_intent(self):
8
+ class Client:
9
+ def __init__(self):
10
+ self.calls = []
11
+
12
+ def request(self, *args, **kwargs):
13
+ self.calls.append((args, kwargs))
14
+ return {"id": "result"}
15
+
16
+ client = Client()
17
+ credits = AICreditsClient(client)
18
+ self.assertEqual(credits.balance(), {"id": "result"})
19
+ credits.usage(product="agents", user="user", limit=10)
20
+ credits.quote(1000, "USD")
21
+ credits.checkout("quote", "retry-key", "paystack")
22
+ credits.funding()
23
+ credits.set_funding("assets", "byok")
24
+
25
+ self.assertEqual(client.calls[0][0], ("GET", "/billing/ai-credits"))
26
+ self.assertEqual(client.calls[1][1]["params"]["product"], "agents")
27
+ self.assertEqual(client.calls[2][1]["json"], {"credits": 1000, "currency": "USD"})
28
+ self.assertEqual(client.calls[3][1]["json"]["idempotency_key"], "retry-key")
29
+ self.assertEqual(client.calls[4][0], ("GET", "/billing/ai-credits/funding"))
30
+ self.assertEqual(client.calls[5][1]["json"], {"product": "assets", "mode": "byok"})
31
+
32
+
33
+ if __name__ == "__main__":
34
+ unittest.main()
@@ -0,0 +1,41 @@
1
+ import json
2
+
3
+ import requests
4
+
5
+ from stackshift import StackShift
6
+
7
+
8
+ class Session:
9
+ def __init__(self):
10
+ self.calls = []
11
+
12
+ def request(self, method, url, **kwargs):
13
+ self.calls.append((method, url, kwargs))
14
+ response = requests.Response()
15
+ response.status_code = 200
16
+ response._content = json.dumps({"success": True, "data": {"id": "result"}}).encode()
17
+ return response
18
+
19
+
20
+ def test_connection_storage_and_migration_requests():
21
+ session = Session()
22
+ platform = StackShift(api_key="account", session=session).assets.platform
23
+ platform.connections.create({"provider": "cloudinary", "configuration": {"cloud_name": "demo"}})
24
+ method, url, options = session.calls[-1]
25
+ assert method == "POST" and url.endswith("/assets/connections")
26
+ assert options["json"]["provider"] == "cloudinary"
27
+ assert options["headers"]["Authorization"] == "Bearer account"
28
+
29
+ platform = platform.for_space("space-1")
30
+ platform.storage.configure("bucket/one", 7, {"storage_mode": "stackshift_s2"})
31
+ method, url, options = session.calls[-1]
32
+ assert method == "PUT" and url.endswith("/assets/buckets/bucket%2Fone/storage")
33
+ assert options["headers"]["If-Match"] == '"7"'
34
+ assert options["headers"]["X-Asset-Space-ID"] == "space-1"
35
+
36
+ platform.migrations.start({"destination_bucket_id": "bucket", "source_prefix": "campaign/"})
37
+ assert session.calls[-1][2]["json"]["source_prefix"] == "campaign/"
38
+ platform.migrations.report("migration/one")
39
+ method, url, options = session.calls[-1]
40
+ assert method == "GET" and url.endswith("/assets/migrations/migration%2Fone/report")
41
+ assert options["params"] == {"format": "json"}
File without changes