snapstate-sdk 1.0.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.
- snapstate_sdk-1.0.0/PKG-INFO +290 -0
- snapstate_sdk-1.0.0/README.md +275 -0
- snapstate_sdk-1.0.0/pyproject.toml +26 -0
- snapstate_sdk-1.0.0/setup.cfg +4 -0
- snapstate_sdk-1.0.0/src/checkpoint_sdk/__init__.py +45 -0
- snapstate_sdk-1.0.0/src/checkpoint_sdk/client.py +652 -0
- snapstate_sdk-1.0.0/src/checkpoint_sdk/errors.py +51 -0
- snapstate_sdk-1.0.0/src/checkpoint_sdk/types.py +74 -0
- snapstate_sdk-1.0.0/src/snapstate_sdk/__init__.py +52 -0
- snapstate_sdk-1.0.0/src/snapstate_sdk/client.py +656 -0
- snapstate_sdk-1.0.0/src/snapstate_sdk/errors.py +54 -0
- snapstate_sdk-1.0.0/src/snapstate_sdk/types.py +74 -0
- snapstate_sdk-1.0.0/src/snapstate_sdk.egg-info/PKG-INFO +290 -0
- snapstate_sdk-1.0.0/src/snapstate_sdk.egg-info/SOURCES.txt +16 -0
- snapstate_sdk-1.0.0/src/snapstate_sdk.egg-info/dependency_links.txt +1 -0
- snapstate_sdk-1.0.0/src/snapstate_sdk.egg-info/requires.txt +5 -0
- snapstate_sdk-1.0.0/src/snapstate_sdk.egg-info/top_level.txt +2 -0
- snapstate_sdk-1.0.0/tests/test_client.py +463 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: snapstate-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python SDK for the SnapState workflow state persistence service
|
|
5
|
+
License: MIT
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: httpx>=0.25.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
14
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
15
|
+
|
|
16
|
+
# snapstate-sdk (Python)
|
|
17
|
+
|
|
18
|
+
Python SDK for the [Checkpoint Service](https://github.com/your-org/snapstate) — a workflow state persistence API that lets AI agents save and resume multi-step work across interruptions, crashes, and handoffs between agents.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install snapstate-sdk
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Requires Python 3.9+ and `httpx>=0.25.0` (installed automatically).
|
|
27
|
+
|
|
28
|
+
## Quick start (sync)
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from snapstate_sdk import SnapStateClient
|
|
32
|
+
|
|
33
|
+
client = SnapStateClient(
|
|
34
|
+
api_key="snp_your_key_here",
|
|
35
|
+
base_url="http://localhost:3000",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Save state after each step
|
|
39
|
+
result = client.save(
|
|
40
|
+
workflow_id="wf_research_001",
|
|
41
|
+
step=1,
|
|
42
|
+
label="sources_gathered",
|
|
43
|
+
state={"sources": ["arxiv.org/123"], "progress": 0.25},
|
|
44
|
+
)
|
|
45
|
+
print(f"Saved: {result.checkpoint_id} (etag: {result.etag})")
|
|
46
|
+
|
|
47
|
+
# Resume a workflow — get the latest state
|
|
48
|
+
resumed = client.resume("wf_research_001")
|
|
49
|
+
print(f"Resuming from step {resumed.latest_checkpoint.step}")
|
|
50
|
+
state = resumed.latest_checkpoint.state
|
|
51
|
+
|
|
52
|
+
# Get full checkpoint history
|
|
53
|
+
history = client.replay("wf_research_001")
|
|
54
|
+
for cp in history.checkpoints:
|
|
55
|
+
print(f" Step {cp.step}: {cp.label}")
|
|
56
|
+
|
|
57
|
+
client.close()
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Async usage
|
|
61
|
+
|
|
62
|
+
Every method has an `async_` prefixed equivalent:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import asyncio
|
|
66
|
+
from snapstate_sdk import SnapStateClient
|
|
67
|
+
|
|
68
|
+
async def main():
|
|
69
|
+
client = SnapStateClient(api_key="snp_...", base_url="http://localhost:3000")
|
|
70
|
+
|
|
71
|
+
result = await client.async_save(
|
|
72
|
+
workflow_id="wf_001",
|
|
73
|
+
step=1,
|
|
74
|
+
state={"status": "running"},
|
|
75
|
+
)
|
|
76
|
+
print(f"Saved: {result.checkpoint_id}")
|
|
77
|
+
|
|
78
|
+
resumed = await client.async_resume("wf_001")
|
|
79
|
+
print(f"Latest step: {resumed.latest_checkpoint.step}")
|
|
80
|
+
|
|
81
|
+
await client.async_close()
|
|
82
|
+
|
|
83
|
+
asyncio.run(main())
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Context managers are supported for both sync and async usage:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
# Sync
|
|
90
|
+
with SnapStateClient(api_key="snp_...") as client:
|
|
91
|
+
client.save(workflow_id="wf_001", step=1, state={})
|
|
92
|
+
|
|
93
|
+
# Async
|
|
94
|
+
async with SnapStateClient(api_key="snp_...") as client:
|
|
95
|
+
await client.async_save(workflow_id="wf_001", step=1, state={})
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Agent identity
|
|
99
|
+
|
|
100
|
+
Register your agent once at startup, then tag checkpoints with `agent_id`:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
client = SnapStateClient(api_key="snp_...", base_url="http://localhost:3000")
|
|
104
|
+
|
|
105
|
+
# Register agent identity
|
|
106
|
+
client.register_agent(
|
|
107
|
+
agent_id="research-bot",
|
|
108
|
+
name="Research Bot",
|
|
109
|
+
description="Searches and summarizes sources",
|
|
110
|
+
capabilities=["web_search", "summarization"],
|
|
111
|
+
metadata={"model": "claude-sonnet-4-6", "version": "2.0.0"},
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Tag checkpoints with agent identity
|
|
115
|
+
client.save(
|
|
116
|
+
workflow_id="wf_collab_001",
|
|
117
|
+
step=1,
|
|
118
|
+
state={"findings": [...]},
|
|
119
|
+
agent_id="research-bot", # identity tag
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Another agent picks up the workflow
|
|
123
|
+
resumed = client.resume("wf_collab_001")
|
|
124
|
+
prior_agent = resumed.latest_checkpoint.metadata.get("agent_id")
|
|
125
|
+
print(f"Picking up from: {prior_agent}")
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Error handling
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from snapstate_sdk import SnapStateClient
|
|
132
|
+
from snapstate_sdk.errors import (
|
|
133
|
+
AuthError,
|
|
134
|
+
NotFoundError,
|
|
135
|
+
ConflictError,
|
|
136
|
+
RateLimitError,
|
|
137
|
+
PayloadTooLargeError,
|
|
138
|
+
ValidationError,
|
|
139
|
+
SnapStateError, # base class
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
client = SnapStateClient(api_key="snp_...", base_url="http://localhost:3000")
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
resumed = client.resume("wf_missing")
|
|
146
|
+
except NotFoundError:
|
|
147
|
+
print("No prior state — starting fresh")
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
client.save(
|
|
151
|
+
workflow_id="wf_001",
|
|
152
|
+
step=2,
|
|
153
|
+
state={"data": "..."},
|
|
154
|
+
if_match="old-etag", # optimistic concurrency
|
|
155
|
+
)
|
|
156
|
+
except ConflictError:
|
|
157
|
+
print("State was modified by another agent — re-read and retry")
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
client.save(workflow_id="wf_001", step=1, state={})
|
|
161
|
+
except AuthError:
|
|
162
|
+
print("Check your API key")
|
|
163
|
+
except RateLimitError as e:
|
|
164
|
+
print(f"Rate limited — retry after {e.retry_after}s")
|
|
165
|
+
except PayloadTooLargeError:
|
|
166
|
+
print("State exceeds 1 MB limit — consider compressing")
|
|
167
|
+
except SnapStateError as e:
|
|
168
|
+
print(f"Unexpected error: {e} (HTTP {e.status_code}, code={e.code})")
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## API reference
|
|
172
|
+
|
|
173
|
+
### `SnapStateClient(api_key, base_url, timeout, max_retries)`
|
|
174
|
+
|
|
175
|
+
| Parameter | Type | Default | Description |
|
|
176
|
+
|-----------|------|---------|-------------|
|
|
177
|
+
| `api_key` | `str` | required | API key starting with `snp_` |
|
|
178
|
+
| `base_url` | `str` | `http://localhost:3000` | Checkpoint Service base URL |
|
|
179
|
+
| `timeout` | `float` | `30.0` | Request timeout in seconds |
|
|
180
|
+
| `max_retries` | `int` | `3` | Retry attempts on 429 before raising `RateLimitError` |
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
### Checkpoint methods
|
|
185
|
+
|
|
186
|
+
#### `save(workflow_id, step, state, label, metadata, agent_id, ttl_seconds, if_match) → SaveResult`
|
|
187
|
+
|
|
188
|
+
Save state for a workflow step.
|
|
189
|
+
|
|
190
|
+
| Parameter | Type | Default | Description |
|
|
191
|
+
|-----------|------|---------|-------------|
|
|
192
|
+
| `workflow_id` | `str` | required | Unique workflow identifier |
|
|
193
|
+
| `step` | `int` | required | Sequential step number |
|
|
194
|
+
| `state` | `dict` | required | Full state to persist (max 1 MB) |
|
|
195
|
+
| `label` | `str` | `None` | Human-readable step label |
|
|
196
|
+
| `metadata` | `dict` | `None` | Arbitrary metadata |
|
|
197
|
+
| `agent_id` | `str` | `None` | Agent identity tag |
|
|
198
|
+
| `ttl_seconds` | `int` | `None` | Override default TTL |
|
|
199
|
+
| `if_match` | `str` | `None` | ETag for optimistic concurrency |
|
|
200
|
+
|
|
201
|
+
Returns `SaveResult(checkpoint_id, workflow_id, step, etag, created_at, diff_from_previous, size_bytes)`.
|
|
202
|
+
|
|
203
|
+
#### `get(checkpoint_id) → Checkpoint`
|
|
204
|
+
|
|
205
|
+
Fetch a specific checkpoint by ID.
|
|
206
|
+
|
|
207
|
+
#### `resume(workflow_id) → WorkflowResume`
|
|
208
|
+
|
|
209
|
+
Get the latest checkpoint for a workflow. Raises `NotFoundError` if no checkpoints exist.
|
|
210
|
+
|
|
211
|
+
Returns `WorkflowResume(workflow_id, latest_checkpoint, total_checkpoints, workflow_started_at, last_activity_at)`.
|
|
212
|
+
|
|
213
|
+
#### `replay(workflow_id, from_step, to_step, limit) → WorkflowReplay`
|
|
214
|
+
|
|
215
|
+
Get the full ordered checkpoint history.
|
|
216
|
+
|
|
217
|
+
Returns `WorkflowReplay(workflow_id, checkpoints, total, has_more)`.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
### Agent methods
|
|
222
|
+
|
|
223
|
+
#### `register_agent(agent_id, name, description, capabilities, metadata) → Agent`
|
|
224
|
+
|
|
225
|
+
Register or update an agent (upsert). Safe to call on every startup.
|
|
226
|
+
|
|
227
|
+
#### `list_agents() → list[Agent]`
|
|
228
|
+
|
|
229
|
+
List all agents for this account.
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
### Webhook methods
|
|
234
|
+
|
|
235
|
+
#### `register_webhook(url, events, secret) → dict`
|
|
236
|
+
|
|
237
|
+
Register a webhook URL. `events` is a list of `checkpoint.saved`, `workflow.resumed`, `workflow.expired`.
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
### Lifecycle
|
|
242
|
+
|
|
243
|
+
#### `close()`
|
|
244
|
+
|
|
245
|
+
Close the sync HTTP client.
|
|
246
|
+
|
|
247
|
+
#### `async_close()`
|
|
248
|
+
|
|
249
|
+
Close the async HTTP client.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
### Async equivalents
|
|
254
|
+
|
|
255
|
+
Every method above has an `async_` prefixed version:
|
|
256
|
+
`async_save`, `async_get`, `async_resume`, `async_replay`, `async_register_agent`, `async_list_agents`.
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
### Return types
|
|
261
|
+
|
|
262
|
+
| Type | Fields |
|
|
263
|
+
|------|--------|
|
|
264
|
+
| `Checkpoint` | `checkpoint_id, workflow_id, step, label, state, metadata, etag, created_at, diff_from_previous, size_bytes, agent_id` |
|
|
265
|
+
| `WorkflowResume` | `workflow_id, latest_checkpoint, total_checkpoints, workflow_started_at, last_activity_at` |
|
|
266
|
+
| `WorkflowReplay` | `workflow_id, checkpoints, total, has_more` |
|
|
267
|
+
| `Agent` | `agent_id, name, description, capabilities, metadata, last_seen_at, created_at` |
|
|
268
|
+
| `SaveResult` | `checkpoint_id, workflow_id, step, etag, created_at, diff_from_previous, size_bytes` |
|
|
269
|
+
|
|
270
|
+
### Error types
|
|
271
|
+
|
|
272
|
+
| Exception | HTTP status | When raised |
|
|
273
|
+
|-----------|-------------|-------------|
|
|
274
|
+
| `AuthError` | 401 | Invalid or missing API key |
|
|
275
|
+
| `NotFoundError` | 404 | Resource not found |
|
|
276
|
+
| `ConflictError` | 409 | ETag mismatch (optimistic concurrency) |
|
|
277
|
+
| `PayloadTooLargeError` | 413 | State exceeds 1 MB |
|
|
278
|
+
| `RateLimitError` | 429 | Rate limit exceeded after all retries |
|
|
279
|
+
| `ValidationError` | 400 | Invalid input |
|
|
280
|
+
| `SnapStateError` | any | Base class for all SDK errors |
|
|
281
|
+
|
|
282
|
+
All exceptions expose `.status_code` (int) and `.code` (str, machine-readable error code).
|
|
283
|
+
`RateLimitError` additionally exposes `.retry_after` (int, seconds).
|
|
284
|
+
|
|
285
|
+
## Running tests
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
pip install "snapstate-sdk[dev]"
|
|
289
|
+
pytest tests/
|
|
290
|
+
```
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# snapstate-sdk (Python)
|
|
2
|
+
|
|
3
|
+
Python SDK for the [Checkpoint Service](https://github.com/your-org/snapstate) — a workflow state persistence API that lets AI agents save and resume multi-step work across interruptions, crashes, and handoffs between agents.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install snapstate-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Python 3.9+ and `httpx>=0.25.0` (installed automatically).
|
|
12
|
+
|
|
13
|
+
## Quick start (sync)
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from snapstate_sdk import SnapStateClient
|
|
17
|
+
|
|
18
|
+
client = SnapStateClient(
|
|
19
|
+
api_key="snp_your_key_here",
|
|
20
|
+
base_url="http://localhost:3000",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Save state after each step
|
|
24
|
+
result = client.save(
|
|
25
|
+
workflow_id="wf_research_001",
|
|
26
|
+
step=1,
|
|
27
|
+
label="sources_gathered",
|
|
28
|
+
state={"sources": ["arxiv.org/123"], "progress": 0.25},
|
|
29
|
+
)
|
|
30
|
+
print(f"Saved: {result.checkpoint_id} (etag: {result.etag})")
|
|
31
|
+
|
|
32
|
+
# Resume a workflow — get the latest state
|
|
33
|
+
resumed = client.resume("wf_research_001")
|
|
34
|
+
print(f"Resuming from step {resumed.latest_checkpoint.step}")
|
|
35
|
+
state = resumed.latest_checkpoint.state
|
|
36
|
+
|
|
37
|
+
# Get full checkpoint history
|
|
38
|
+
history = client.replay("wf_research_001")
|
|
39
|
+
for cp in history.checkpoints:
|
|
40
|
+
print(f" Step {cp.step}: {cp.label}")
|
|
41
|
+
|
|
42
|
+
client.close()
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Async usage
|
|
46
|
+
|
|
47
|
+
Every method has an `async_` prefixed equivalent:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import asyncio
|
|
51
|
+
from snapstate_sdk import SnapStateClient
|
|
52
|
+
|
|
53
|
+
async def main():
|
|
54
|
+
client = SnapStateClient(api_key="snp_...", base_url="http://localhost:3000")
|
|
55
|
+
|
|
56
|
+
result = await client.async_save(
|
|
57
|
+
workflow_id="wf_001",
|
|
58
|
+
step=1,
|
|
59
|
+
state={"status": "running"},
|
|
60
|
+
)
|
|
61
|
+
print(f"Saved: {result.checkpoint_id}")
|
|
62
|
+
|
|
63
|
+
resumed = await client.async_resume("wf_001")
|
|
64
|
+
print(f"Latest step: {resumed.latest_checkpoint.step}")
|
|
65
|
+
|
|
66
|
+
await client.async_close()
|
|
67
|
+
|
|
68
|
+
asyncio.run(main())
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Context managers are supported for both sync and async usage:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
# Sync
|
|
75
|
+
with SnapStateClient(api_key="snp_...") as client:
|
|
76
|
+
client.save(workflow_id="wf_001", step=1, state={})
|
|
77
|
+
|
|
78
|
+
# Async
|
|
79
|
+
async with SnapStateClient(api_key="snp_...") as client:
|
|
80
|
+
await client.async_save(workflow_id="wf_001", step=1, state={})
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Agent identity
|
|
84
|
+
|
|
85
|
+
Register your agent once at startup, then tag checkpoints with `agent_id`:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
client = SnapStateClient(api_key="snp_...", base_url="http://localhost:3000")
|
|
89
|
+
|
|
90
|
+
# Register agent identity
|
|
91
|
+
client.register_agent(
|
|
92
|
+
agent_id="research-bot",
|
|
93
|
+
name="Research Bot",
|
|
94
|
+
description="Searches and summarizes sources",
|
|
95
|
+
capabilities=["web_search", "summarization"],
|
|
96
|
+
metadata={"model": "claude-sonnet-4-6", "version": "2.0.0"},
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Tag checkpoints with agent identity
|
|
100
|
+
client.save(
|
|
101
|
+
workflow_id="wf_collab_001",
|
|
102
|
+
step=1,
|
|
103
|
+
state={"findings": [...]},
|
|
104
|
+
agent_id="research-bot", # identity tag
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Another agent picks up the workflow
|
|
108
|
+
resumed = client.resume("wf_collab_001")
|
|
109
|
+
prior_agent = resumed.latest_checkpoint.metadata.get("agent_id")
|
|
110
|
+
print(f"Picking up from: {prior_agent}")
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Error handling
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from snapstate_sdk import SnapStateClient
|
|
117
|
+
from snapstate_sdk.errors import (
|
|
118
|
+
AuthError,
|
|
119
|
+
NotFoundError,
|
|
120
|
+
ConflictError,
|
|
121
|
+
RateLimitError,
|
|
122
|
+
PayloadTooLargeError,
|
|
123
|
+
ValidationError,
|
|
124
|
+
SnapStateError, # base class
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
client = SnapStateClient(api_key="snp_...", base_url="http://localhost:3000")
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
resumed = client.resume("wf_missing")
|
|
131
|
+
except NotFoundError:
|
|
132
|
+
print("No prior state — starting fresh")
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
client.save(
|
|
136
|
+
workflow_id="wf_001",
|
|
137
|
+
step=2,
|
|
138
|
+
state={"data": "..."},
|
|
139
|
+
if_match="old-etag", # optimistic concurrency
|
|
140
|
+
)
|
|
141
|
+
except ConflictError:
|
|
142
|
+
print("State was modified by another agent — re-read and retry")
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
client.save(workflow_id="wf_001", step=1, state={})
|
|
146
|
+
except AuthError:
|
|
147
|
+
print("Check your API key")
|
|
148
|
+
except RateLimitError as e:
|
|
149
|
+
print(f"Rate limited — retry after {e.retry_after}s")
|
|
150
|
+
except PayloadTooLargeError:
|
|
151
|
+
print("State exceeds 1 MB limit — consider compressing")
|
|
152
|
+
except SnapStateError as e:
|
|
153
|
+
print(f"Unexpected error: {e} (HTTP {e.status_code}, code={e.code})")
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## API reference
|
|
157
|
+
|
|
158
|
+
### `SnapStateClient(api_key, base_url, timeout, max_retries)`
|
|
159
|
+
|
|
160
|
+
| Parameter | Type | Default | Description |
|
|
161
|
+
|-----------|------|---------|-------------|
|
|
162
|
+
| `api_key` | `str` | required | API key starting with `snp_` |
|
|
163
|
+
| `base_url` | `str` | `http://localhost:3000` | Checkpoint Service base URL |
|
|
164
|
+
| `timeout` | `float` | `30.0` | Request timeout in seconds |
|
|
165
|
+
| `max_retries` | `int` | `3` | Retry attempts on 429 before raising `RateLimitError` |
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
### Checkpoint methods
|
|
170
|
+
|
|
171
|
+
#### `save(workflow_id, step, state, label, metadata, agent_id, ttl_seconds, if_match) → SaveResult`
|
|
172
|
+
|
|
173
|
+
Save state for a workflow step.
|
|
174
|
+
|
|
175
|
+
| Parameter | Type | Default | Description |
|
|
176
|
+
|-----------|------|---------|-------------|
|
|
177
|
+
| `workflow_id` | `str` | required | Unique workflow identifier |
|
|
178
|
+
| `step` | `int` | required | Sequential step number |
|
|
179
|
+
| `state` | `dict` | required | Full state to persist (max 1 MB) |
|
|
180
|
+
| `label` | `str` | `None` | Human-readable step label |
|
|
181
|
+
| `metadata` | `dict` | `None` | Arbitrary metadata |
|
|
182
|
+
| `agent_id` | `str` | `None` | Agent identity tag |
|
|
183
|
+
| `ttl_seconds` | `int` | `None` | Override default TTL |
|
|
184
|
+
| `if_match` | `str` | `None` | ETag for optimistic concurrency |
|
|
185
|
+
|
|
186
|
+
Returns `SaveResult(checkpoint_id, workflow_id, step, etag, created_at, diff_from_previous, size_bytes)`.
|
|
187
|
+
|
|
188
|
+
#### `get(checkpoint_id) → Checkpoint`
|
|
189
|
+
|
|
190
|
+
Fetch a specific checkpoint by ID.
|
|
191
|
+
|
|
192
|
+
#### `resume(workflow_id) → WorkflowResume`
|
|
193
|
+
|
|
194
|
+
Get the latest checkpoint for a workflow. Raises `NotFoundError` if no checkpoints exist.
|
|
195
|
+
|
|
196
|
+
Returns `WorkflowResume(workflow_id, latest_checkpoint, total_checkpoints, workflow_started_at, last_activity_at)`.
|
|
197
|
+
|
|
198
|
+
#### `replay(workflow_id, from_step, to_step, limit) → WorkflowReplay`
|
|
199
|
+
|
|
200
|
+
Get the full ordered checkpoint history.
|
|
201
|
+
|
|
202
|
+
Returns `WorkflowReplay(workflow_id, checkpoints, total, has_more)`.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
### Agent methods
|
|
207
|
+
|
|
208
|
+
#### `register_agent(agent_id, name, description, capabilities, metadata) → Agent`
|
|
209
|
+
|
|
210
|
+
Register or update an agent (upsert). Safe to call on every startup.
|
|
211
|
+
|
|
212
|
+
#### `list_agents() → list[Agent]`
|
|
213
|
+
|
|
214
|
+
List all agents for this account.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
### Webhook methods
|
|
219
|
+
|
|
220
|
+
#### `register_webhook(url, events, secret) → dict`
|
|
221
|
+
|
|
222
|
+
Register a webhook URL. `events` is a list of `checkpoint.saved`, `workflow.resumed`, `workflow.expired`.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
### Lifecycle
|
|
227
|
+
|
|
228
|
+
#### `close()`
|
|
229
|
+
|
|
230
|
+
Close the sync HTTP client.
|
|
231
|
+
|
|
232
|
+
#### `async_close()`
|
|
233
|
+
|
|
234
|
+
Close the async HTTP client.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
### Async equivalents
|
|
239
|
+
|
|
240
|
+
Every method above has an `async_` prefixed version:
|
|
241
|
+
`async_save`, `async_get`, `async_resume`, `async_replay`, `async_register_agent`, `async_list_agents`.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
### Return types
|
|
246
|
+
|
|
247
|
+
| Type | Fields |
|
|
248
|
+
|------|--------|
|
|
249
|
+
| `Checkpoint` | `checkpoint_id, workflow_id, step, label, state, metadata, etag, created_at, diff_from_previous, size_bytes, agent_id` |
|
|
250
|
+
| `WorkflowResume` | `workflow_id, latest_checkpoint, total_checkpoints, workflow_started_at, last_activity_at` |
|
|
251
|
+
| `WorkflowReplay` | `workflow_id, checkpoints, total, has_more` |
|
|
252
|
+
| `Agent` | `agent_id, name, description, capabilities, metadata, last_seen_at, created_at` |
|
|
253
|
+
| `SaveResult` | `checkpoint_id, workflow_id, step, etag, created_at, diff_from_previous, size_bytes` |
|
|
254
|
+
|
|
255
|
+
### Error types
|
|
256
|
+
|
|
257
|
+
| Exception | HTTP status | When raised |
|
|
258
|
+
|-----------|-------------|-------------|
|
|
259
|
+
| `AuthError` | 401 | Invalid or missing API key |
|
|
260
|
+
| `NotFoundError` | 404 | Resource not found |
|
|
261
|
+
| `ConflictError` | 409 | ETag mismatch (optimistic concurrency) |
|
|
262
|
+
| `PayloadTooLargeError` | 413 | State exceeds 1 MB |
|
|
263
|
+
| `RateLimitError` | 429 | Rate limit exceeded after all retries |
|
|
264
|
+
| `ValidationError` | 400 | Invalid input |
|
|
265
|
+
| `SnapStateError` | any | Base class for all SDK errors |
|
|
266
|
+
|
|
267
|
+
All exceptions expose `.status_code` (int) and `.code` (str, machine-readable error code).
|
|
268
|
+
`RateLimitError` additionally exposes `.retry_after` (int, seconds).
|
|
269
|
+
|
|
270
|
+
## Running tests
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
pip install "snapstate-sdk[dev]"
|
|
274
|
+
pytest tests/
|
|
275
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "snapstate-sdk"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Python SDK for the SnapState workflow state persistence service"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = {text = "MIT"}
|
|
7
|
+
requires-python = ">=3.9"
|
|
8
|
+
dependencies = ["httpx>=0.25.0"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Programming Language :: Python :: 3",
|
|
11
|
+
"License :: OSI Approved :: MIT License",
|
|
12
|
+
"Operating System :: OS Independent",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
dev = ["pytest>=7.0", "pytest-asyncio>=0.21.0"]
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["setuptools>=68.0"]
|
|
20
|
+
build-backend = "setuptools.build_meta"
|
|
21
|
+
|
|
22
|
+
[tool.setuptools.packages.find]
|
|
23
|
+
where = ["src"]
|
|
24
|
+
|
|
25
|
+
[tool.pytest.ini_options]
|
|
26
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
checkpoint-sdk — Python SDK for the Checkpoint workflow state persistence service.
|
|
3
|
+
|
|
4
|
+
Quick start::
|
|
5
|
+
|
|
6
|
+
from checkpoint_sdk import CheckpointClient
|
|
7
|
+
|
|
8
|
+
client = CheckpointClient(api_key="cpk_...", base_url="http://localhost:3000")
|
|
9
|
+
|
|
10
|
+
result = client.save(workflow_id="wf_001", step=1, state={"progress": 0})
|
|
11
|
+
resumed = client.resume("wf_001")
|
|
12
|
+
client.close()
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .client import CheckpointClient
|
|
16
|
+
from .errors import (
|
|
17
|
+
AuthError,
|
|
18
|
+
CheckpointError,
|
|
19
|
+
ConflictError,
|
|
20
|
+
NotFoundError,
|
|
21
|
+
PayloadTooLargeError,
|
|
22
|
+
RateLimitError,
|
|
23
|
+
ValidationError,
|
|
24
|
+
)
|
|
25
|
+
from .types import Agent, Checkpoint, SaveResult, WorkflowResume, WorkflowReplay
|
|
26
|
+
|
|
27
|
+
__version__ = "1.0.0"
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"CheckpointClient",
|
|
31
|
+
# Types
|
|
32
|
+
"Checkpoint",
|
|
33
|
+
"WorkflowResume",
|
|
34
|
+
"WorkflowReplay",
|
|
35
|
+
"Agent",
|
|
36
|
+
"SaveResult",
|
|
37
|
+
# Errors
|
|
38
|
+
"CheckpointError",
|
|
39
|
+
"AuthError",
|
|
40
|
+
"NotFoundError",
|
|
41
|
+
"ConflictError",
|
|
42
|
+
"RateLimitError",
|
|
43
|
+
"ValidationError",
|
|
44
|
+
"PayloadTooLargeError",
|
|
45
|
+
]
|