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.
- {stackshift-1.1.0 → stackshift-1.2.0}/PKG-INFO +33 -1
- {stackshift-1.1.0 → stackshift-1.2.0}/README.md +32 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/pyproject.toml +1 -1
- {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/__init__.py +2 -0
- stackshift-1.2.0/stackshift/ai_credits.py +24 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/assets.py +2 -0
- stackshift-1.2.0/stackshift/assets_platform.py +99 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/client.py +2 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/stackshift.egg-info/PKG-INFO +33 -1
- {stackshift-1.1.0 → stackshift-1.2.0}/stackshift.egg-info/SOURCES.txt +4 -0
- stackshift-1.2.0/tests/test_ai_credits.py +34 -0
- stackshift-1.2.0/tests/test_assets_platform.py +41 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/setup.cfg +0 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/assets_dam.py +0 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/assets_media.py +0 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/assets_video.py +0 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/assets_workflows.py +0 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/mail.py +0 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/mail_advanced.py +0 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/mail_billing.py +0 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/mail_brands.py +0 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/mail_campaigns.py +0 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/mail_events_webhooks.py +0 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/mail_exports.py +0 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/mail_marketing.py +0 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/mail_migration.py +0 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/mail_reputation.py +0 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/mail_streams.py +0 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/mail_tracking.py +0 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/projects.py +0 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/stackshift/workload_security.py +0 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/stackshift.egg-info/dependency_links.txt +0 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/stackshift.egg-info/requires.txt +0 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/stackshift.egg-info/top_level.txt +0 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/tests/test_assets_media.py +0 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/tests/test_assets_workflows.py +0 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/tests/test_mail.py +0 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/tests/test_mail_advanced.py +0 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/tests/test_mail_diagnostics.py +0 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/tests/test_mail_events_webhooks.py +0 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/tests/test_mail_exports.py +0 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/tests/test_mail_platform.py +0 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/tests/test_mail_simulation.py +0 -0
- {stackshift-1.1.0 → stackshift-1.2.0}/tests/test_mail_subscribers.py +0 -0
- {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.
|
|
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
|
+
```
|
|
@@ -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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|