stackshift 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: stackshift
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for StackShift.
5
+ Requires-Python: >=3.9
6
+ Requires-Dist: requests>=2.31
@@ -0,0 +1,42 @@
1
+ # StackShift Python SDK
2
+
3
+ Official Python SDK for StackShift.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install stackshift
9
+ ```
10
+
11
+ ## Upload an asset
12
+
13
+ ```python
14
+ from stackshift import StackShift
15
+
16
+ stackshift = StackShift()
17
+
18
+ asset = stackshift.assets.upload(
19
+ "avatar.png",
20
+ folder="avatars",
21
+ visibility="public",
22
+ metadata={"user_id": "user_123"},
23
+ )
24
+
25
+ print(asset["url"])
26
+ ```
27
+
28
+ You do not pass a deployed StackShift project ID. The API key identifies the StackShift account, and StackShift resolves the default asset space internally.
29
+
30
+ ## Private asset URL
31
+
32
+ ```python
33
+ signed = stackshift.assets.signed_url(
34
+ asset["id"],
35
+ expires_in="10m",
36
+ max_downloads=1,
37
+ )
38
+
39
+ print(signed["url"])
40
+ ```
41
+
42
+ The SDK only talks to the StackShift REST API. Storage placement, replication, disks, and repair are StackShift internals.
@@ -0,0 +1,13 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "stackshift"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK for StackShift."
9
+ requires-python = ">=3.9"
10
+ dependencies = ["requests>=2.31"]
11
+
12
+ [tool.setuptools]
13
+ packages = ["stackshift"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ from .client import StackShift
2
+ from .assets import AssetsClient
3
+ from .projects import ProjectsClient
4
+
5
+ __all__ = ["StackShift", "AssetsClient", "ProjectsClient"]
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any, BinaryIO
6
+
7
+
8
+ class AssetsClient:
9
+ def __init__(self, client: Any) -> None:
10
+ self._client = client
11
+
12
+ def upload(
13
+ self,
14
+ file: str | Path | BinaryIO,
15
+ *,
16
+ bucket: str | None = None,
17
+ key: str | None = None,
18
+ folder: str | None = None,
19
+ visibility: str | None = None,
20
+ cache_control: str | None = None,
21
+ metadata: dict[str, Any] | None = None,
22
+ ) -> dict[str, Any]:
23
+ return self._multipart("POST", "/assets/upload", file, {
24
+ "bucket": bucket,
25
+ "key": key,
26
+ "folder": folder,
27
+ "visibility": visibility,
28
+ "cacheControl": cache_control,
29
+ "metadata": json.dumps(metadata) if metadata else None,
30
+ })
31
+
32
+ def list(self, **params: Any) -> dict[str, Any]:
33
+ clean = {key: value for key, value in params.items() if value not in (None, "")}
34
+ return self._client.request("GET", "/assets", params=clean)
35
+
36
+ def get(self, asset_id: str) -> dict[str, Any]:
37
+ return self._client.request("GET", f"/assets/{asset_id}")
38
+
39
+ def delete(self, asset_id: str) -> dict[str, Any]:
40
+ return self._client.request("DELETE", f"/assets/{asset_id}")
41
+
42
+ def replace(
43
+ self,
44
+ asset_id: str,
45
+ file: str | Path | BinaryIO,
46
+ *,
47
+ cache_control: str | None = None,
48
+ metadata: dict[str, Any] | None = None,
49
+ ) -> dict[str, Any]:
50
+ return self._multipart("PUT", f"/assets/{asset_id}/replace", file, {
51
+ "cacheControl": cache_control,
52
+ "metadata": json.dumps(metadata) if metadata else None,
53
+ })
54
+
55
+ def signed_url(
56
+ self,
57
+ asset_id: str,
58
+ *,
59
+ expires_in: str = "10m",
60
+ max_downloads: int | None = None,
61
+ ) -> dict[str, Any]:
62
+ return self._client.request("POST", f"/assets/{asset_id}/signed-url", json={
63
+ "expiresIn": expires_in,
64
+ "maxDownloads": max_downloads,
65
+ })
66
+
67
+ def signed_upload_url(self, **payload: Any) -> dict[str, Any]:
68
+ return self.create_upload_session(**payload)
69
+
70
+ def create_upload_session(self, **payload: Any) -> dict[str, Any]:
71
+ clean = {key: value for key, value in payload.items() if value not in (None, "")}
72
+ return self._client.request("POST", "/assets/upload-sessions", json=clean)
73
+
74
+ def _multipart(self, method: str, suffix: str, file: str | Path | BinaryIO, fields: dict[str, Any]) -> dict[str, Any]:
75
+ should_close = False
76
+ if isinstance(file, (str, Path)):
77
+ handle = Path(file).open("rb")
78
+ filename = Path(file).name
79
+ should_close = True
80
+ else:
81
+ handle = file
82
+ filename = getattr(file, "name", "asset")
83
+
84
+ try:
85
+ data = {key: value for key, value in fields.items() if value not in (None, "")}
86
+ return self._client.request(method, suffix, data=data, files={"file": (filename, handle)})
87
+ finally:
88
+ if should_close:
89
+ handle.close()
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import Any
5
+
6
+ import requests
7
+
8
+
9
+ class StackShift:
10
+ def __init__(
11
+ self,
12
+ *,
13
+ api_key: str | None = None,
14
+ base_url: str = "https://api.stackshift.cloud/api/v1",
15
+ session: requests.Session | None = None,
16
+ ) -> None:
17
+ api_key = api_key or os.getenv("STACKSHIFT_API_KEY")
18
+ if not api_key:
19
+ raise ValueError("StackShift SDK requires an api_key")
20
+
21
+ self.api_key = api_key
22
+ self.base_url = base_url.rstrip("/")
23
+ self.session = session or requests.Session()
24
+
25
+ from .assets import AssetsClient
26
+ from .projects import ProjectsClient
27
+
28
+ self.projects = ProjectsClient(self)
29
+ self.assets = AssetsClient(self)
30
+
31
+ def request(self, method: str, path: str, **kwargs: Any) -> Any:
32
+ headers = kwargs.pop("headers", {})
33
+ headers["Authorization"] = f"Bearer {self.api_key}"
34
+ headers["User-Agent"] = "StackShift-Python/0.1"
35
+
36
+ response = self.session.request(method, f"{self.base_url}{path}", headers=headers, **kwargs)
37
+ payload = response.json() if response.content else None
38
+ if response.status_code < 200 or response.status_code >= 300:
39
+ message = payload.get("message") if isinstance(payload, dict) else None
40
+ raise RuntimeError(message or f"StackShift API request failed with {response.status_code}")
41
+
42
+ if isinstance(payload, dict) and "success" in payload and "data" in payload:
43
+ return payload["data"]
44
+ return payload
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ class ProjectsClient:
7
+ def __init__(self, client: Any) -> None:
8
+ self._client = client
9
+
10
+ def list(self, *, page: int | None = None, per_page: int | None = None) -> list[dict[str, Any]]:
11
+ params = {
12
+ key: value
13
+ for key, value in {"page": page, "per_page": per_page}.items()
14
+ if value not in (None, "")
15
+ }
16
+ return self._client.request("GET", "/projects/", params=params)
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: stackshift
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for StackShift.
5
+ Requires-Python: >=3.9
6
+ Requires-Dist: requests>=2.31
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ stackshift/__init__.py
4
+ stackshift/assets.py
5
+ stackshift/client.py
6
+ stackshift/projects.py
7
+ stackshift.egg-info/PKG-INFO
8
+ stackshift.egg-info/SOURCES.txt
9
+ stackshift.egg-info/dependency_links.txt
10
+ stackshift.egg-info/requires.txt
11
+ stackshift.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ requests>=2.31
@@ -0,0 +1 @@
1
+ stackshift