Skip to content

Commit 9feb960

Browse files
Gkclaude
authored andcommitted
Initial release: CueAPI Python SDK v0.1.0
- Full CRUD for cues (create, list, get, update, delete, pause, resume) - Webhook signature verification (v1= HMAC-SHA256) - Pydantic models for all API responses - Typed exception hierarchy (AuthenticationError, RateLimitError, etc.) - 22 tests passing against staging - Examples: basic usage, worker setup, webhook handler - GitHub Actions workflow ready for PyPI publish on tag push Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
0 parents  commit 9feb960

File tree

20 files changed

+1232
-0
lines changed

20 files changed

+1232
-0
lines changed

.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# CueAPI SDK configuration
2+
CUEAPI_API_KEY=cue_sk_your_key
3+
CUEAPI_WEBHOOK_SECRET=whsec_your_secret
4+
5+
# For running tests against staging
6+
CUEAPI_TEST_KEY=cue_sk_your_staging_key

.github/workflows/publish.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Publish to PyPI
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*"
7+
8+
jobs:
9+
publish:
10+
runs-on: ubuntu-latest
11+
permissions:
12+
id-token: write
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- name: Set up Python
17+
uses: actions/setup-python@v5
18+
with:
19+
python-version: "3.12"
20+
21+
- name: Install build tools
22+
run: pip install hatchling build
23+
24+
- name: Build package
25+
run: python -m build
26+
27+
- name: Publish to PyPI
28+
uses: pypa/gh-action-pypi-publish@release/v1

.gitignore

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
__pycache__/
2+
*.py[cod]
3+
*.egg-info/
4+
dist/
5+
build/
6+
.eggs/
7+
*.egg
8+
.venv/
9+
venv/
10+
.env
11+
.pytest_cache/
12+
.mypy_cache/
13+
.ruff_cache/

README.md

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# CueAPI Python SDK
2+
3+
The official Python SDK for [CueAPI](https://cueapi.ai) — scheduling infrastructure for agents.
4+
5+
## Install
6+
7+
```bash
8+
pip install cueapi-sdk
9+
```
10+
11+
## Quickstart
12+
13+
```python
14+
from cueapi import CueAPI
15+
16+
client = CueAPI("cue_sk_your_key")
17+
cue = client.cues.create(
18+
name="daily-report",
19+
cron="0 9 * * *",
20+
callback="https://my-app.com/webhook",
21+
payload={"task": "generate_report"},
22+
)
23+
print(f"Next run: {cue.next_run}")
24+
```
25+
26+
## Why CueAPI?
27+
28+
- **Replace fragile cron jobs** — managed scheduling with automatic retries, execution logs, and failure alerts. No servers to maintain.
29+
- **Built for AI agents** — schedule agent tasks, coordinate multi-agent pipelines, and retry failed workflows with exponential backoff.
30+
- **Two transport modes** — webhook delivery to your public URL, or worker pull for agents behind firewalls.
31+
32+
## Transport Modes
33+
34+
### Webhook
35+
36+
CueAPI POSTs a signed payload to your callback URL when a cue fires:
37+
38+
```python
39+
cue = client.cues.create(
40+
name="webhook-task",
41+
cron="0 9 * * *",
42+
callback="https://my-app.com/webhook",
43+
payload={"action": "sync"},
44+
)
45+
```
46+
47+
### Worker
48+
49+
Your local daemon polls CueAPI for executions. No public URL needed:
50+
51+
```python
52+
cue = client.cues.create(
53+
name="worker-task",
54+
cron="0 */6 * * *",
55+
transport="worker",
56+
payload={"pipeline": "etl"},
57+
)
58+
```
59+
60+
Install the worker: `pip install cueapi-worker`
61+
62+
## Method Reference
63+
64+
### `CueAPI(api_key, *, base_url, timeout)`
65+
66+
Create a client. `api_key` starts with `cue_sk_`.
67+
68+
### `client.cues.create(...)`
69+
70+
| Parameter | Type | Description |
71+
|---|---|---|
72+
| `name` | `str` | **Required.** Unique name for the cue. |
73+
| `cron` | `str` | Cron expression for recurring schedules. |
74+
| `at` | `str \| datetime` | ISO 8601 datetime for one-time schedules. |
75+
| `timezone` | `str` | IANA timezone (default `"UTC"`). |
76+
| `callback` | `str` | Webhook URL for execution delivery. |
77+
| `transport` | `str` | `"webhook"` (default) or `"worker"`. |
78+
| `payload` | `dict` | JSON payload included in each execution. |
79+
| `description` | `str` | Optional description. |
80+
| `retry` | `dict` | `{"max_attempts": 3, "backoff_minutes": [1, 5, 15]}` |
81+
| `on_failure` | `dict` | `{"email": true, "webhook": null, "pause": false}` |
82+
83+
Returns a `Cue` object.
84+
85+
### `client.cues.list(*, limit, offset, status)`
86+
87+
Returns a `CueList` with `.cues`, `.total`, `.limit`, `.offset`.
88+
89+
### `client.cues.get(cue_id)`
90+
91+
Returns a `Cue` object.
92+
93+
### `client.cues.update(cue_id, **fields)`
94+
95+
Update any field. Only provided fields are changed.
96+
97+
### `client.cues.pause(cue_id)`
98+
99+
Pause a cue. Returns the updated `Cue`.
100+
101+
### `client.cues.resume(cue_id)`
102+
103+
Resume a paused cue. Returns the updated `Cue`.
104+
105+
### `client.cues.delete(cue_id)`
106+
107+
Delete a cue. Returns `None`.
108+
109+
## Webhook Verification
110+
111+
Verify incoming webhook signatures in your handler:
112+
113+
```python
114+
from cueapi import verify_webhook
115+
116+
is_valid = verify_webhook(
117+
payload=request.body,
118+
signature=request.headers["X-CueAPI-Signature"],
119+
timestamp=request.headers["X-CueAPI-Timestamp"],
120+
secret="whsec_your_secret",
121+
)
122+
```
123+
124+
## Error Handling
125+
126+
```python
127+
from cueapi import CueAPI, AuthenticationError, RateLimitError, CueNotFoundError
128+
129+
try:
130+
cue = client.cues.get("cue_abc123")
131+
except CueNotFoundError:
132+
print("Cue not found")
133+
except AuthenticationError:
134+
print("Invalid API key")
135+
except RateLimitError as e:
136+
print(f"Rate limited. Retry after {e.retry_after}s")
137+
```
138+
139+
| Exception | HTTP Status | When |
140+
|---|---|---|
141+
| `AuthenticationError` | 401 | Invalid or missing API key |
142+
| `CueLimitExceededError` | 403 | Plan cue limit reached |
143+
| `CueNotFoundError` | 404 | Cue ID doesn't exist |
144+
| `InvalidScheduleError` | 400/422 | Bad cron expression or request body |
145+
| `RateLimitError` | 429 | Too many requests |
146+
| `CueAPIServerError` | 5xx | Server error |
147+
148+
## Links
149+
150+
- [Documentation](https://docs.cueapi.ai)
151+
- [API Reference](https://docs.cueapi.ai/api-reference/overview/)
152+
- [Dashboard](https://dashboard.cueapi.ai)
153+
- [CueAPI](https://cueapi.ai)

cueapi/__init__.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""CueAPI Python SDK — scheduling infrastructure for agents."""
2+
3+
from cueapi.client import CueAPI
4+
from cueapi.exceptions import (
5+
AuthenticationError,
6+
CueAPIError,
7+
CueAPIServerError,
8+
CueLimitExceededError,
9+
CueNotFoundError,
10+
InvalidScheduleError,
11+
RateLimitError,
12+
)
13+
from cueapi.webhook import verify_webhook
14+
15+
__version__ = "0.1.0"
16+
17+
__all__ = [
18+
"CueAPI",
19+
"verify_webhook",
20+
"CueAPIError",
21+
"AuthenticationError",
22+
"RateLimitError",
23+
"CueNotFoundError",
24+
"CueLimitExceededError",
25+
"InvalidScheduleError",
26+
"CueAPIServerError",
27+
]

cueapi/client.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
"""CueAPI client — the main entry point for the SDK."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Any, Dict, Optional
6+
7+
import httpx
8+
9+
from cueapi.exceptions import (
10+
AuthenticationError,
11+
CueAPIError,
12+
CueAPIServerError,
13+
CueLimitExceededError,
14+
CueNotFoundError,
15+
InvalidScheduleError,
16+
RateLimitError,
17+
)
18+
from cueapi.resources.cues import CuesResource
19+
20+
DEFAULT_BASE_URL = "https://api.cueapi.ai"
21+
DEFAULT_TIMEOUT = 30.0
22+
23+
24+
class CueAPI:
25+
"""CueAPI client.
26+
27+
Usage::
28+
29+
from cueapi import CueAPI
30+
31+
client = CueAPI("cue_sk_your_key")
32+
cue = client.cues.create(
33+
name="daily-report",
34+
cron="0 9 * * *",
35+
callback="https://my-app.com/webhook",
36+
)
37+
"""
38+
39+
def __init__(
40+
self,
41+
api_key: str,
42+
*,
43+
base_url: str = DEFAULT_BASE_URL,
44+
timeout: float = DEFAULT_TIMEOUT,
45+
) -> None:
46+
"""Initialize the CueAPI client.
47+
48+
Args:
49+
api_key: Your CueAPI API key (starts with ``cue_sk_``).
50+
base_url: API base URL (default ``https://api.cueapi.ai``).
51+
timeout: Request timeout in seconds (default 30).
52+
"""
53+
if not api_key:
54+
raise ValueError("api_key is required")
55+
56+
self._api_key = api_key
57+
self._base_url = base_url.rstrip("/")
58+
self._http = httpx.Client(
59+
base_url=self._base_url,
60+
headers={
61+
"Authorization": f"Bearer {api_key}",
62+
"Content-Type": "application/json",
63+
"User-Agent": "cueapi-python/0.1.0",
64+
},
65+
timeout=timeout,
66+
)
67+
68+
# Resources
69+
self.cues = CuesResource(self)
70+
71+
def close(self) -> None:
72+
"""Close the underlying HTTP client."""
73+
self._http.close()
74+
75+
def __enter__(self) -> CueAPI:
76+
return self
77+
78+
def __exit__(self, *args: Any) -> None:
79+
self.close()
80+
81+
# --- HTTP helpers ---
82+
83+
def _request(
84+
self,
85+
method: str,
86+
path: str,
87+
*,
88+
json: Optional[Dict[str, Any]] = None,
89+
params: Optional[Dict[str, Any]] = None,
90+
) -> Any:
91+
"""Make an HTTP request and handle errors."""
92+
response = self._http.request(method, path, json=json, params=params)
93+
return self._handle_response(response)
94+
95+
def _handle_response(self, response: httpx.Response) -> Any:
96+
"""Parse response, raise typed exceptions on errors."""
97+
if response.status_code == 204:
98+
return None
99+
100+
try:
101+
data = response.json()
102+
except Exception:
103+
data = {"error": {"message": response.text, "code": "unknown"}}
104+
105+
if response.is_success:
106+
return data
107+
108+
# Extract error info
109+
error_body = data.get("error", data)
110+
message = error_body.get("message", "Unknown error")
111+
code = error_body.get("code", "unknown")
112+
status = response.status_code
113+
114+
kwargs = dict(
115+
message=message,
116+
status_code=status,
117+
code=code,
118+
body=data,
119+
)
120+
121+
if status == 401:
122+
raise AuthenticationError(**kwargs)
123+
elif status == 403:
124+
raise CueLimitExceededError(**kwargs)
125+
elif status == 404:
126+
raise CueNotFoundError(**kwargs)
127+
elif status == 429:
128+
retry_after = response.headers.get("Retry-After")
129+
raise RateLimitError(
130+
retry_after=int(retry_after) if retry_after else None,
131+
**kwargs,
132+
)
133+
elif status == 400 or status == 422:
134+
raise InvalidScheduleError(**kwargs)
135+
elif status >= 500:
136+
raise CueAPIServerError(**kwargs)
137+
else:
138+
raise CueAPIError(**kwargs)
139+
140+
def _get(self, path: str, **kwargs: Any) -> Any:
141+
return self._request("GET", path, **kwargs)
142+
143+
def _post(self, path: str, **kwargs: Any) -> Any:
144+
return self._request("POST", path, **kwargs)
145+
146+
def _patch(self, path: str, **kwargs: Any) -> Any:
147+
return self._request("PATCH", path, **kwargs)
148+
149+
def _delete(self, path: str, **kwargs: Any) -> Any:
150+
return self._request("DELETE", path, **kwargs)

0 commit comments

Comments
 (0)