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.
- superartifacts/__init__.py +39 -0
- superartifacts/_client.py +280 -0
- superartifacts/_errors.py +125 -0
- superartifacts/_generated/__init__.py +0 -0
- superartifacts/_generated/models.py +1353 -0
- superartifacts/_generated/resources.py +3286 -0
- superartifacts/_helpers.py +195 -0
- superartifacts/py.typed +0 -0
- superartifacts-0.1.0.dist-info/METADATA +241 -0
- superartifacts-0.1.0.dist-info/RECORD +12 -0
- superartifacts-0.1.0.dist-info/WHEEL +4 -0
- superartifacts-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -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]
|
superartifacts/py.typed
ADDED
|
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,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.
|