Skip to content

Commit 68622b8

Browse files
author
Delega Bot
committed
Initial Python SDK: sync + async client, typed models, 42 tests
- Zero required deps (stdlib urllib for sync client) - Optional httpx for async: pip install delega[async] - Namespace pattern: client.tasks.list(), client.agents.create() - Typed dataclasses with from_dict() for all models - Exception hierarchy: DelegaError → DelegaAPIError → Auth/NotFound/RateLimit - Full API coverage: tasks, comments, agents, projects, webhooks, delegation - Python 3.9+ compatible, hatchling build backend - 42 unit tests with mocked HTTP
0 parents  commit 68622b8

12 files changed

Lines changed: 1705 additions & 0 deletions

File tree

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.venv/
2+
__pycache__/
3+
*.pyc
4+
.pytest_cache/
5+
dist/
6+
*.egg-info/

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Delega
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.

README.md

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# Delega Python SDK
2+
3+
Official Python SDK for the [Delega](https://delega.dev) API.
4+
5+
## Installation
6+
7+
```bash
8+
pip install delega
9+
```
10+
11+
For async support:
12+
13+
```bash
14+
pip install 'delega[async]'
15+
```
16+
17+
## Quick Start
18+
19+
```python
20+
from delega import Delega
21+
22+
client = Delega(api_key="dlg_...")
23+
24+
# List tasks
25+
tasks = client.tasks.list()
26+
27+
# Create a task
28+
task = client.tasks.create("Deploy to production", priority=1, labels=["ops"])
29+
30+
# Complete a task
31+
client.tasks.complete(task.id)
32+
```
33+
34+
## Authentication
35+
36+
Pass your API key directly or set the `DELEGA_API_KEY` environment variable:
37+
38+
```python
39+
# Explicit
40+
client = Delega(api_key="dlg_...")
41+
42+
# From environment
43+
# export DELEGA_API_KEY=dlg_...
44+
client = Delega()
45+
```
46+
47+
For self-hosted instances:
48+
49+
```python
50+
client = Delega(api_key="dlg_...", base_url="https://delega.yourcompany.com")
51+
```
52+
53+
## Tasks
54+
55+
```python
56+
# List with filters
57+
tasks = client.tasks.list(priority=1, completed=False)
58+
tasks = client.tasks.list(labels=["urgent"], due_before="2026-12-31")
59+
60+
# Search
61+
tasks = client.tasks.search("deploy")
62+
63+
# CRUD
64+
task = client.tasks.create("Fix bug", description="Crash on login", priority=1)
65+
task = client.tasks.get("task_id")
66+
task = client.tasks.update("task_id", content="Updated title", priority=3)
67+
client.tasks.delete("task_id")
68+
69+
# Completion
70+
client.tasks.complete("task_id")
71+
client.tasks.uncomplete("task_id")
72+
73+
# Delegation
74+
subtask = client.tasks.delegate("parent_task_id", "Research options", priority=2)
75+
76+
# Comments
77+
client.tasks.add_comment("task_id", "Looks good, shipping it")
78+
comments = client.tasks.list_comments("task_id")
79+
```
80+
81+
## Agents
82+
83+
```python
84+
agents = client.agents.list()
85+
agent = client.agents.create("deploy-bot", display_name="Deploy Bot")
86+
print(agent.api_key) # Only available at creation time
87+
88+
client.agents.update(agent.id, description="Handles deployments")
89+
result = client.agents.rotate_key(agent.id)
90+
print(result["api_key"])
91+
92+
client.agents.delete(agent.id)
93+
```
94+
95+
## Projects
96+
97+
```python
98+
projects = client.projects.list()
99+
project = client.projects.create("Backend", emoji="⚙️", color="#3498db")
100+
```
101+
102+
## Webhooks
103+
104+
```python
105+
webhooks = client.webhooks.list()
106+
webhook = client.webhooks.create(
107+
"https://example.com/webhook",
108+
events=["task.created", "task.completed"],
109+
secret="whsec_...",
110+
)
111+
```
112+
113+
## Account
114+
115+
```python
116+
me = client.me() # Get authenticated agent info
117+
usage = client.usage() # Get API usage stats
118+
```
119+
120+
## Async Client
121+
122+
```python
123+
from delega import AsyncDelega
124+
125+
async with AsyncDelega(api_key="dlg_...") as client:
126+
tasks = await client.tasks.list()
127+
task = await client.tasks.create("Async task")
128+
await client.tasks.complete(task.id)
129+
```
130+
131+
The async client has the same interface as the sync client, but all methods are coroutines. Requires `httpx` (`pip install 'delega[async]'`).
132+
133+
## Error Handling
134+
135+
```python
136+
from delega import DelegaError, DelegaAPIError, DelegaAuthError, DelegaNotFoundError, DelegaRateLimitError
137+
138+
try:
139+
task = client.tasks.get("nonexistent")
140+
except DelegaNotFoundError:
141+
print("Task not found")
142+
except DelegaAuthError:
143+
print("Invalid API key")
144+
except DelegaRateLimitError:
145+
print("Too many requests")
146+
except DelegaAPIError as e:
147+
print(f"API error {e.status_code}: {e.error_message}")
148+
except DelegaError as e:
149+
print(f"SDK error: {e}")
150+
```
151+
152+
## Models
153+
154+
All resource methods return typed dataclasses:
155+
156+
- `Task` - id, content, description, priority, labels, due_date, completed, project_id, parent_id, created_at, updated_at
157+
- `Comment` - id, task_id, content, created_at
158+
- `Agent` - id, name, display_name, description, api_key, created_at, updated_at
159+
- `Project` - id, name, emoji, color, created_at, updated_at
160+
161+
## License
162+
163+
MIT

pyproject.toml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
[build-system]
2+
requires = ["hatchling"]
3+
build-backend = "hatchling.build"
4+
5+
[project]
6+
name = "delega"
7+
version = "0.1.0"
8+
description = "Official Python SDK for the Delega API"
9+
readme = "README.md"
10+
license = "MIT"
11+
requires-python = ">=3.9"
12+
authors = [
13+
{ name = "Delega", email = "support@delega.dev" },
14+
]
15+
classifiers = [
16+
"Development Status :: 4 - Beta",
17+
"Intended Audience :: Developers",
18+
"License :: OSI Approved :: MIT License",
19+
"Programming Language :: Python :: 3",
20+
"Programming Language :: Python :: 3.9",
21+
"Programming Language :: Python :: 3.10",
22+
"Programming Language :: Python :: 3.11",
23+
"Programming Language :: Python :: 3.12",
24+
"Programming Language :: Python :: 3.13",
25+
"Typing :: Typed",
26+
]
27+
keywords = ["delega", "api", "sdk", "task-management", "delegation"]
28+
29+
[project.optional-dependencies]
30+
async = ["httpx>=0.27"]
31+
32+
[project.urls]
33+
Homepage = "https://delega.dev"
34+
Documentation = "https://docs.delega.dev"
35+
Repository = "https://github.com/delega-dev/delega-python"
36+
Issues = "https://github.com/delega-dev/delega-python/issues"
37+
38+
[tool.hatch.build.targets.wheel]
39+
packages = ["src/delega"]

src/delega/__init__.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""Delega Python SDK - Official client for the Delega API."""
2+
3+
from .client import Delega
4+
from .exceptions import (
5+
DelegaAPIError,
6+
DelegaAuthError,
7+
DelegaError,
8+
DelegaNotFoundError,
9+
DelegaRateLimitError,
10+
)
11+
from .models import Agent, Comment, Project, Task
12+
13+
__version__ = "0.1.0"
14+
15+
__all__ = [
16+
"Agent",
17+
"AsyncDelega",
18+
"Comment",
19+
"Delega",
20+
"DelegaAPIError",
21+
"DelegaAuthError",
22+
"DelegaError",
23+
"DelegaNotFoundError",
24+
"DelegaRateLimitError",
25+
"Project",
26+
"Task",
27+
]
28+
29+
30+
def __getattr__(name: str) -> object:
31+
if name == "AsyncDelega":
32+
from .async_client import AsyncDelega
33+
34+
return AsyncDelega
35+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

src/delega/_http.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"""Low-level HTTP transport using urllib (stdlib only)."""
2+
3+
from __future__ import annotations
4+
5+
import json
6+
import urllib.error
7+
import urllib.parse
8+
import urllib.request
9+
from typing import Any, Optional
10+
11+
from .exceptions import (
12+
DelegaAPIError,
13+
DelegaAuthError,
14+
DelegaNotFoundError,
15+
DelegaRateLimitError,
16+
)
17+
18+
_DEFAULT_TIMEOUT = 30
19+
20+
21+
class HTTPClient:
22+
"""Synchronous HTTP client using urllib."""
23+
24+
def __init__(self, base_url: str, api_key: str, timeout: int = _DEFAULT_TIMEOUT) -> None:
25+
self._base_url = base_url.rstrip("/")
26+
self._api_key = api_key
27+
self._timeout = timeout
28+
29+
def _headers(self) -> dict[str, str]:
30+
return {
31+
"X-Agent-Key": self._api_key,
32+
"Content-Type": "application/json",
33+
"Accept": "application/json",
34+
}
35+
36+
def request(
37+
self,
38+
method: str,
39+
path: str,
40+
*,
41+
params: Optional[dict[str, Any]] = None,
42+
body: Optional[dict[str, Any]] = None,
43+
) -> Any:
44+
"""Send an HTTP request and return the parsed JSON response.
45+
46+
Args:
47+
method: HTTP method (GET, POST, PUT, PATCH, DELETE).
48+
path: API path (e.g. ``/v1/tasks``).
49+
params: Optional query parameters.
50+
body: Optional JSON request body.
51+
52+
Returns:
53+
Parsed JSON response, or ``True`` for successful ``DELETE``
54+
requests with no body.
55+
56+
Raises:
57+
DelegaAuthError: On 401/403 responses.
58+
DelegaNotFoundError: On 404 responses.
59+
DelegaRateLimitError: On 429 responses.
60+
DelegaAPIError: On other non-2xx responses.
61+
"""
62+
url = self._base_url + path
63+
if params:
64+
filtered = {k: v for k, v in params.items() if v is not None}
65+
if filtered:
66+
query = urllib.parse.urlencode(filtered, doseq=True)
67+
url = f"{url}?{query}"
68+
69+
data = json.dumps(body).encode("utf-8") if body is not None else None
70+
req = urllib.request.Request(url, data=data, headers=self._headers(), method=method)
71+
72+
try:
73+
with urllib.request.urlopen(req, timeout=self._timeout) as resp:
74+
resp_body = resp.read().decode("utf-8")
75+
if not resp_body:
76+
return True
77+
return json.loads(resp_body)
78+
except urllib.error.HTTPError as exc:
79+
error_body = exc.read().decode("utf-8", errors="replace")
80+
try:
81+
error_data = json.loads(error_body)
82+
message = error_data.get("error", error_data.get("message", error_body))
83+
except (json.JSONDecodeError, ValueError):
84+
message = error_body or exc.reason
85+
86+
status = exc.code
87+
if status in (401, 403):
88+
raise DelegaAuthError(error_message=message, status_code=status) from exc
89+
if status == 404:
90+
raise DelegaNotFoundError(error_message=message) from exc
91+
if status == 429:
92+
raise DelegaRateLimitError(error_message=message) from exc
93+
raise DelegaAPIError(status_code=status, error_message=message) from exc
94+
95+
def get(self, path: str, *, params: Optional[dict[str, Any]] = None) -> Any:
96+
"""Send a GET request."""
97+
return self.request("GET", path, params=params)
98+
99+
def post(self, path: str, *, body: Optional[dict[str, Any]] = None) -> Any:
100+
"""Send a POST request."""
101+
return self.request("POST", path, body=body)
102+
103+
def patch(self, path: str, *, body: Optional[dict[str, Any]] = None) -> Any:
104+
"""Send a PATCH request."""
105+
return self.request("PATCH", path, body=body)
106+
107+
def put(self, path: str, *, body: Optional[dict[str, Any]] = None) -> Any:
108+
"""Send a PUT request."""
109+
return self.request("PUT", path, body=body)
110+
111+
def delete(self, path: str) -> Any:
112+
"""Send a DELETE request."""
113+
return self.request("DELETE", path)

0 commit comments

Comments
 (0)