superartifacts 0.1.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.
@@ -0,0 +1,195 @@
1
+ """The hand-written parts of the resources: paging, the upload round trip and publishing a file."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+ from collections.abc import AsyncIterator, Callable, Iterator, Mapping
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from ._errors import SuperArtifactsError
12
+
13
+ OMIT: Any = object()
14
+ """Leave a field out, where None would send a null that means something."""
15
+
16
+ Files = Mapping[str, "bytes | str | os.PathLike[str]"]
17
+
18
+
19
+ def _items(page: dict[str, Any], key: str) -> list[Any]:
20
+ items = page.get(key)
21
+ if items is None:
22
+ raise SuperArtifactsError(f"This read answered no {key!r} to page through.")
23
+ return items
24
+
25
+
26
+ def _next_cursor(page: dict[str, Any]) -> str | None:
27
+ # Data reads answer `cursor.next`, mentions answer `cursorNext`.
28
+ cursor = page.get("cursor")
29
+ return cursor.get("next") if isinstance(cursor, dict) else page.get("cursorNext")
30
+
31
+
32
+ def _last_page(page: dict[str, Any], number: int, items: list[Any]) -> bool:
33
+ return not items or (number + 1) * page.get("pageSize", len(items)) >= page.get("total", 0)
34
+
35
+
36
+ def walk_cursor(fetch: Callable[..., Any], key: str, args: dict[str, Any]) -> Iterator[Any]:
37
+ args = dict(args)
38
+ while True:
39
+ page = fetch(**args)
40
+ yield from _items(page, key)
41
+ args["cursor"] = _next_cursor(page)
42
+ if not args["cursor"]:
43
+ return
44
+
45
+
46
+ async def awalk_cursor(
47
+ fetch: Callable[..., Any], key: str, args: dict[str, Any]
48
+ ) -> AsyncIterator[Any]:
49
+ args = dict(args)
50
+ while True:
51
+ page = await fetch(**args)
52
+ for item in _items(page, key):
53
+ yield item
54
+ args["cursor"] = _next_cursor(page)
55
+ if not args["cursor"]:
56
+ return
57
+
58
+
59
+ def walk_pages(fetch: Callable[..., Any], key: str, args: dict[str, Any]) -> Iterator[Any]:
60
+ args = dict(args)
61
+ args.setdefault("page", 0)
62
+ while True:
63
+ page = fetch(**args)
64
+ items = _items(page, key)
65
+ yield from items
66
+ if _last_page(page, args["page"], items):
67
+ return
68
+ args["page"] += 1
69
+
70
+
71
+ async def awalk_pages(
72
+ fetch: Callable[..., Any], key: str, args: dict[str, Any]
73
+ ) -> AsyncIterator[Any]:
74
+ args = dict(args)
75
+ args.setdefault("page", 0)
76
+ while True:
77
+ page = await fetch(**args)
78
+ items = _items(page, key)
79
+ for item in items:
80
+ yield item
81
+ if _last_page(page, args["page"], items):
82
+ return
83
+ args["page"] += 1
84
+
85
+
86
+ def _read(files: Files) -> dict[str, bytes]:
87
+ return {
88
+ path: bytes(source) if isinstance(source, (bytes, bytearray)) else Path(source).read_bytes()
89
+ for path, source in files.items()
90
+ }
91
+
92
+
93
+ def _declare(blobs: dict[str, bytes]) -> list[dict[str, Any]]:
94
+ return [{"path": path, "bytes": len(blob)} for path, blob in blobs.items()]
95
+
96
+
97
+ class UploadHelper:
98
+ def upload(self, files: Files, *, timeout: float | None = None) -> dict[str, Any]:
99
+ """Stage `{path in the artifact: bytes or local file}` and PUT each file.
100
+
101
+ Returns what `stage` answered; pass its `uploadId` to
102
+ `artifacts.publish(uploads=...)`. The URLs are the credential, so the
103
+ API key is not sent with the bytes.
104
+ """
105
+ blobs = _read(files)
106
+ staged = self.stage(files=_declare(blobs), timeout=timeout) # type: ignore[attr-defined]
107
+ # The answer lists the files in the order they were declared.
108
+ for upload, blob in zip(staged["uploads"], blobs.values(), strict=True):
109
+ self._client._request("PUT", upload["url"], timeout, {"content": blob}) # type: ignore[attr-defined]
110
+ return staged
111
+
112
+
113
+ class AsyncUploadHelper:
114
+ async def upload(self, files: Files, *, timeout: float | None = None) -> dict[str, Any]:
115
+ """Stage `{path in the artifact: bytes or local file}` and PUT each file.
116
+
117
+ Returns what `stage` answered; pass its `uploadId` to
118
+ `artifacts.publish(uploads=...)`. The URLs are the credential, so the
119
+ API key is not sent with the bytes.
120
+ """
121
+ blobs = _read(files)
122
+ staged = await self.stage(files=_declare(blobs), timeout=timeout) # type: ignore[attr-defined]
123
+ for upload, blob in zip(staged["uploads"], blobs.values(), strict=True):
124
+ await self._client._request("PUT", upload["url"], timeout, {"content": blob}) # type: ignore[attr-defined]
125
+ return staged
126
+
127
+
128
+ def _slug_of(path: Path) -> str:
129
+ name = path.parent.name if path.name == "index.html" else path.stem if path.is_file() else path.name
130
+ return re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
131
+
132
+
133
+ def _gather(path: str | os.PathLike[str], slug: str | None) -> tuple[str, str, dict[str, Path]]:
134
+ """Resolve `path` to (slug, index.html text, every other file by its path in the artifact)."""
135
+ root = Path(path).resolve()
136
+ slug = slug or _slug_of(root)
137
+ if not slug:
138
+ raise SuperArtifactsError(f"Cannot name an artifact after {str(root)!r}: pass slug=.")
139
+ if root.is_file():
140
+ return slug, root.read_text(encoding="utf-8"), {}
141
+ if not (root / "index.html").is_file():
142
+ raise SuperArtifactsError(f"{str(root)!r} is not a file, or a folder with an index.html.")
143
+ beside = {
144
+ file.relative_to(root).as_posix(): file
145
+ for file in sorted(root.rglob("*"))
146
+ if file.is_file() and not any(part.startswith(".") for part in file.relative_to(root).parts)
147
+ }
148
+ html = beside.pop("index.html").read_text(encoding="utf-8")
149
+ return slug, html, beside
150
+
151
+
152
+ class ArtifactHelper:
153
+ def publish_file(
154
+ self,
155
+ path: str | os.PathLike[str],
156
+ *,
157
+ slug: str | None = None,
158
+ timeout: float | None = None,
159
+ **fields: Any,
160
+ ) -> dict[str, Any]:
161
+ """Publish a page you already made: an HTML file, or a folder with an `index.html`.
162
+
163
+ The page comes from your own model, your own tools, your own build; this
164
+ hosts it. A folder's other files go up beside it, uploaded for you. `slug`
165
+ defaults to the file's name (the folder's name for a folder or an
166
+ `index.html`); every other field is what `publish` takes.
167
+ """
168
+ slug, html, beside = _gather(path, slug)
169
+ if beside:
170
+ staged = self._client.uploads.upload(beside, timeout=timeout) # type: ignore[attr-defined]
171
+ fields["uploads"] = staged["uploadId"]
172
+ return self.publish(slug=slug, html=html, timeout=timeout, **fields) # type: ignore[attr-defined]
173
+
174
+
175
+ class AsyncArtifactHelper:
176
+ async def publish_file(
177
+ self,
178
+ path: str | os.PathLike[str],
179
+ *,
180
+ slug: str | None = None,
181
+ timeout: float | None = None,
182
+ **fields: Any,
183
+ ) -> dict[str, Any]:
184
+ """Publish a page you already made: an HTML file, or a folder with an `index.html`.
185
+
186
+ The page comes from your own model, your own tools, your own build; this
187
+ hosts it. A folder's other files go up beside it, uploaded for you. `slug`
188
+ defaults to the file's name (the folder's name for a folder or an
189
+ `index.html`); every other field is what `publish` takes.
190
+ """
191
+ slug, html, beside = _gather(path, slug)
192
+ if beside:
193
+ staged = await self._client.uploads.upload(beside, timeout=timeout) # type: ignore[attr-defined]
194
+ fields["uploads"] = staged["uploadId"]
195
+ return await self.publish(slug=slug, html=html, timeout=timeout, **fields) # type: ignore[attr-defined]
File without changes
@@ -0,0 +1,241 @@
1
+ Metadata-Version: 2.5
2
+ Name: superartifacts
3
+ Version: 0.1.0
4
+ Summary: Python SDK for the Super Artifacts REST API: publish artifacts and read their data from your own code.
5
+ Project-URL: Homepage, https://superartifacts.app/docs/sdks/python
6
+ Project-URL: Repository, https://github.com/Superbelt/sdk
7
+ Project-URL: Issues, https://github.com/Superbelt/sdk/issues
8
+ Author: Super Artifacts
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: api,artifacts,publishing,sdk,super-artifacts
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: httpx<1,>=0.27
23
+ Requires-Dist: pydantic<3,>=2
24
+ Description-Content-Type: text/markdown
25
+
26
+ # Super Artifacts for Python
27
+
28
+ Needs a Solo plan or higher. MCP and skills are unaffected.
29
+
30
+ The official Python SDK for the [Super Artifacts](https://superartifacts.app) REST API: publish
31
+ artifacts, read what visitors submitted, manage projects and templates, from your own code.
32
+
33
+ Full docs: <https://superartifacts.app/docs/sdks/python>
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ pip install superartifacts
39
+ ```
40
+
41
+ Python 3.10 or newer. Create a key in
42
+ [Settings → API keys](https://superartifacts.app/settings/api-keys).
43
+
44
+ ## Quickstart
45
+
46
+ ```python
47
+ import os
48
+
49
+ from superartifacts import SuperArtifacts
50
+
51
+ sa = SuperArtifacts(api_key=os.environ["SUPERARTIFACTS_API_KEY"])
52
+
53
+ print(sa.account.whoami()["handle"])
54
+
55
+ sa.artifacts.publish(
56
+ slug="hello",
57
+ html="<!doctype html><h1>Hello</h1>",
58
+ )
59
+
60
+ for artifact in sa.artifacts.list()["artifacts"]:
61
+ print(artifact["slug"])
62
+
63
+ for row in sa.data.iter_artifact_rows(artifact="hello"):
64
+ print(row)
65
+ ```
66
+
67
+ `api_key` falls back to `SUPERARTIFACTS_API_KEY`, so `SuperArtifacts()` is enough.
68
+
69
+ Keyword arguments hold path, query and body fields together, in snake_case: the SDK splits them
70
+ as the API expects, so `values_file=` goes out as `valuesFile`. Values nested inside a dict keep
71
+ the API's spelling. Answers are plain dicts, as the
72
+ [API reference](https://superartifacts.app/docs/reference/api-endpoints) describes them.
73
+
74
+ **Server-side only.** The API refuses any request carrying an `Origin` header, which every browser
75
+ adds. Keep the key on a server.
76
+
77
+ ## Async
78
+
79
+ `AsyncSuperArtifacts` is the same surface, awaited:
80
+
81
+ ```python
82
+ import asyncio
83
+
84
+ from superartifacts import AsyncSuperArtifacts
85
+
86
+
87
+ async def main():
88
+ async with AsyncSuperArtifacts() as sa:
89
+ print(await sa.account.whoami())
90
+ async for row in sa.data.iter_artifact_rows(artifact="hello"):
91
+ print(row)
92
+
93
+
94
+ asyncio.run(main())
95
+ ```
96
+
97
+ ## Configuration
98
+
99
+ ```python
100
+ SuperArtifacts(
101
+ api_key=None, # or SUPERARTIFACTS_API_KEY
102
+ base_url="https://superartifacts.app",
103
+ timeout=60.0, # seconds
104
+ max_retries=2,
105
+ http_client=None, # your own httpx.Client; yours to close
106
+ )
107
+ ```
108
+
109
+ Every method also takes `timeout=` for that call, and every `POST` takes `idempotency_key=`.
110
+ Use the client as a context manager, or call `close()`.
111
+
112
+ ## Errors
113
+
114
+ Everything raised is a `SuperArtifactsError`, with `.status`, `.code`, `.message`, `.retry_after`
115
+ and the full error object in `.details`.
116
+ A client-side failure has `.status` 0 and its own `.code`: a missing key is an `AuthenticationError`
117
+ with `missing_api_key`.
118
+
119
+ | Status | Exception |
120
+ | --- | --- |
121
+ | 400 | `InvalidArgumentsError` |
122
+ | 401 | `AuthenticationError` (also raised when there is no key) |
123
+ | 402 | `PlanLimitError` |
124
+ | 403 | `PermissionError`, or `ScopeRequiredError` when a read key tried a write |
125
+ | 404 | `NotFoundError` |
126
+ | 409 | `ConflictError` |
127
+ | 413 | `PayloadTooLargeError` |
128
+ | 429 | `RateLimitError` |
129
+ | 5xx | `InternalError` |
130
+ | none | `APIConnectionError`, `APITimeoutError` |
131
+
132
+ ```python
133
+ from superartifacts import NotFoundError
134
+
135
+ try:
136
+ sa.artifacts.get(artifact="missing")
137
+ except NotFoundError as error:
138
+ print(error.code, error.message)
139
+ ```
140
+
141
+ `superartifacts.PermissionError` is not the builtin of that name: import it by name when you
142
+ catch it.
143
+
144
+ ## Retries
145
+
146
+ Up to `max_retries` more tries, two by default:
147
+
148
+ - A `429` is retried after its `Retry-After`. Past 60 seconds it is not waited out: the
149
+ `RateLimitError` comes straight back with `retry_after` set.
150
+ - `502`, `503`, `504` and network errors are retried with exponential backoff and jitter, for
151
+ `GET`, `DELETE` and `POST`.
152
+ - `PATCH` and `PUT` are retried on `429` only. `500` and every other `4xx` are never retried.
153
+
154
+ Every `POST` sends an `Idempotency-Key`, one per call and the same across its retries, so a retried
155
+ publish happens once. Pass `idempotency_key=` to choose your own, which also makes a retry of your
156
+ own code safe.
157
+
158
+ ## Pagination
159
+
160
+ `iter_*` methods walk every page and yield one item at a time, taking the same arguments as the
161
+ list they wrap:
162
+
163
+ | Method | Wraps |
164
+ | --- | --- |
165
+ | `sa.data.iter_rows(...)` | `sa.data.list_rows(...)`, with `dataset=` or `collection=` |
166
+ | `sa.data.iter_artifact_rows(...)` | `sa.data.list_artifact_rows(...)` |
167
+ | `sa.entries.iter(...)` | `sa.entries.list(...)` |
168
+ | `sa.mentions.iter(...)` | `sa.mentions.list(...)` |
169
+
170
+ ## Publish a page you made
171
+
172
+ Super Artifacts runs no model: you bring the inference. Make the page with your own model, write it
173
+ to a file, publish the file.
174
+
175
+ ```python
176
+ from pathlib import Path
177
+
178
+ from anthropic import Anthropic
179
+ from superartifacts import SuperArtifacts
180
+
181
+ # Your model, your key. We run no inference.
182
+ msg = Anthropic().messages.create(
183
+ model="claude-haiku-5-5",
184
+ max_tokens=4000,
185
+ messages=[{
186
+ "role": "user",
187
+ "content": "A launch checklist. HTML only.",
188
+ }],
189
+ )
190
+ Path("checklist.html").write_text(
191
+ msg.content[0].text
192
+ )
193
+
194
+ # We host the file. Reads SUPERARTIFACTS_API_KEY
195
+ sa = SuperArtifacts()
196
+ page = sa.artifacts.publish_file(
197
+ "checklist.html"
198
+ )
199
+ print(page["url"])
200
+ ```
201
+
202
+ `publish_file` takes an HTML file, or a folder with an `index.html` (its other files are uploaded
203
+ beside it). The slug defaults to the file's or folder's name. `prompt` is optional: send it only if
204
+ you want the request shown on the artifact's card; nothing is generated from it.
205
+
206
+ ## Uploads
207
+
208
+ A JSON body stops at 1 MiB. Bigger files go through `uploads.upload`, which stages them and sends
209
+ the bytes straight to their upload URLs. It takes a mapping of path in the artifact to bytes or a
210
+ local file, within the limits of 5 MiB a file and 10 MiB a set:
211
+
212
+ ```python
213
+ staged = sa.uploads.upload({"images/hero.png": "./hero.png", "notes.txt": b"hello"})
214
+
215
+ sa.artifacts.publish(
216
+ slug="hello",
217
+ html='<!doctype html><img src="images/hero.png">',
218
+ uploads=staged["uploadId"],
219
+ )
220
+ ```
221
+
222
+ ## License
223
+
224
+ MIT
225
+
226
+ ## Webhook endpoints for an agent
227
+
228
+ Create an endpoint filtered to the agent that owns the calling key. Store the returned
229
+ signing secret in the receiver’s secret store; it is returned only by create and rotate.
230
+ The listener must be publicly reachable. Mutations require a key with write scope.
231
+
232
+ ```python
233
+ endpoint = sa.webhooks.create(
234
+ url="https://agent.example/webhook",
235
+ agent="me",
236
+ event_types=["trigger.fired"],
237
+ )
238
+ # Save endpoint["secret"] securely; never log it.
239
+ sa.webhooks.test(id=endpoint["id"])
240
+ messages = sa.webhooks.messages(id=endpoint["id"], since_minutes=60)
241
+ ```
@@ -0,0 +1,12 @@
1
+ superartifacts/__init__.py,sha256=WVslkhjAbGv8Bc1oKVO9M9gjavnR6bLLNpRIiizW6H0,925
2
+ superartifacts/_client.py,sha256=nd7TVKKlBfPh4zYMQW-JiyocPz5KCcjLI5nRWneGdZo,10107
3
+ superartifacts/_errors.py,sha256=HYhnW9rutgRQg14z4kSSj0sBQKYVmbDg91kp6Ef2p1Y,3390
4
+ superartifacts/_helpers.py,sha256=rJgf0TIev09r7wlA1B4Gr0PTQc68qoW5njtVHwULbRg,7454
5
+ superartifacts/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ superartifacts/_generated/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ superartifacts/_generated/models.py,sha256=97YStbCFbreDoUmpvvXRHCgnkTS2w3nC4Eql6gTnX0k,36997
8
+ superartifacts/_generated/resources.py,sha256=8fl-tSYWGPimX31iT_1IHSYsTZTVRmh9ZqutHbE6c04,149762
9
+ superartifacts-0.1.0.dist-info/METADATA,sha256=Vts_BkvZwlMyXxq0PCQwxh7pWBYMBcFeoK-iuubkfHU,7439
10
+ superartifacts-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
11
+ superartifacts-0.1.0.dist-info/licenses/LICENSE,sha256=JUpWclesPd7rcrwHwG5xYoXMXPUqDDhh_HXkdHKPnNs,1072
12
+ superartifacts-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Super Artifacts
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.