craaft 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.
- craaft-1.0.0/.github/workflows/publish.yml +53 -0
- craaft-1.0.0/.github/workflows/test.yml +36 -0
- craaft-1.0.0/.gitignore +12 -0
- craaft-1.0.0/PKG-INFO +175 -0
- craaft-1.0.0/README.md +146 -0
- craaft-1.0.0/examples/README.md +28 -0
- craaft-1.0.0/examples/advanced_client.py +72 -0
- craaft-1.0.0/examples/card_lifecycle.py +65 -0
- craaft-1.0.0/examples/error_handling.py +67 -0
- craaft-1.0.0/examples/quickstart.py +49 -0
- craaft-1.0.0/examples/retries.py +63 -0
- craaft-1.0.0/examples/searching.py +46 -0
- craaft-1.0.0/pyproject.toml +65 -0
- craaft-1.0.0/src/craaft/__init__.py +44 -0
- craaft-1.0.0/src/craaft/_http.py +244 -0
- craaft-1.0.0/src/craaft/_version.py +1 -0
- craaft-1.0.0/src/craaft/client.py +81 -0
- craaft-1.0.0/src/craaft/exceptions.py +82 -0
- craaft-1.0.0/src/craaft/models.py +212 -0
- craaft-1.0.0/src/craaft/resources/__init__.py +0 -0
- craaft-1.0.0/src/craaft/resources/_base.py +12 -0
- craaft-1.0.0/src/craaft/resources/cards.py +76 -0
- craaft-1.0.0/src/craaft/resources/columns.py +35 -0
- craaft-1.0.0/src/craaft/resources/comments.py +17 -0
- craaft-1.0.0/src/craaft/resources/me.py +29 -0
- craaft-1.0.0/src/craaft/resources/projects.py +117 -0
- craaft-1.0.0/tests/__init__.py +0 -0
- craaft-1.0.0/tests/conftest.py +0 -0
- craaft-1.0.0/tests/test_cards.py +200 -0
- craaft-1.0.0/tests/test_client.py +123 -0
- craaft-1.0.0/tests/test_columns.py +64 -0
- craaft-1.0.0/tests/test_comments.py +35 -0
- craaft-1.0.0/tests/test_exceptions.py +75 -0
- craaft-1.0.0/tests/test_http.py +250 -0
- craaft-1.0.0/tests/test_me.py +55 -0
- craaft-1.0.0/tests/test_models.py +259 -0
- craaft-1.0.0/tests/test_projects.py +227 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
name: Upload Package to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
release-build:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.x"
|
|
20
|
+
|
|
21
|
+
- name: Build release distributions
|
|
22
|
+
run: |
|
|
23
|
+
python -m pip install build
|
|
24
|
+
python -m build
|
|
25
|
+
|
|
26
|
+
- name: Upload distributions
|
|
27
|
+
uses: actions/upload-artifact@v4
|
|
28
|
+
with:
|
|
29
|
+
name: release-dists
|
|
30
|
+
path: dist/
|
|
31
|
+
|
|
32
|
+
pypi-publish:
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
environment:
|
|
35
|
+
name: pypi
|
|
36
|
+
url: https://pypi.org/p/craaft/
|
|
37
|
+
needs:
|
|
38
|
+
- release-build
|
|
39
|
+
|
|
40
|
+
permissions:
|
|
41
|
+
id-token: write
|
|
42
|
+
|
|
43
|
+
steps:
|
|
44
|
+
- name: Retrieve release distributions
|
|
45
|
+
uses: actions/download-artifact@v4
|
|
46
|
+
with:
|
|
47
|
+
name: release-dists
|
|
48
|
+
path: dist/
|
|
49
|
+
|
|
50
|
+
- name: Publish release distributions to PyPI
|
|
51
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
52
|
+
with:
|
|
53
|
+
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: Test Python SDK
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ "main" ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ "main" ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
strategy:
|
|
14
|
+
fail-fast: false
|
|
15
|
+
matrix:
|
|
16
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
21
|
+
uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: ${{ matrix.python-version }}
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: |
|
|
26
|
+
python -m pip install --upgrade pip
|
|
27
|
+
python -m pip install flake8 pytest
|
|
28
|
+
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
|
29
|
+
pip install ".[dev]"
|
|
30
|
+
- name: Lint with flake8
|
|
31
|
+
run: |
|
|
32
|
+
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
|
33
|
+
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
|
34
|
+
- name: Test with pytest
|
|
35
|
+
run: |
|
|
36
|
+
pytest
|
craaft-1.0.0/.gitignore
ADDED
craaft-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: craaft
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python client for the Craaft API.
|
|
5
|
+
Project-URL: Homepage, https://craaft.io
|
|
6
|
+
Project-URL: Source, https://github.com/craaft/python-sdk
|
|
7
|
+
Project-URL: Issues, https://github.com/craaft/python-sdk/issues
|
|
8
|
+
Author-email: Sean Nieuwoudt <sean@underwulf.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: api,client,craaft,sdk
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: requests>=2.32
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
25
|
+
Requires-Dist: responses>=0.25; extra == 'dev'
|
|
26
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
27
|
+
Requires-Dist: types-requests; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# Craaft Python SDK
|
|
31
|
+
|
|
32
|
+
A small, synchronous Python client for the [Craaft API](https://craaft.io). It wraps the REST endpoints with typed dataclasses, a sensible retry policy, and a friendly exception hierarchy.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install craaft
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Python 3.10 or newer.
|
|
41
|
+
|
|
42
|
+
## Quickstart
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from datetime import datetime, timedelta, timezone
|
|
46
|
+
|
|
47
|
+
from craaft import CraaftClient
|
|
48
|
+
|
|
49
|
+
# Reads CRAAFT_API_TOKEN (and optionally CRAAFT_BASE_URL) from the environment.
|
|
50
|
+
with CraaftClient() as client:
|
|
51
|
+
me = client.me.get()
|
|
52
|
+
print(f"Hi {me.name}")
|
|
53
|
+
|
|
54
|
+
project = client.projects.create(name="Demo", description="A new board")
|
|
55
|
+
|
|
56
|
+
card = client.projects.create_card(
|
|
57
|
+
project.id,
|
|
58
|
+
title="Ship the SDK",
|
|
59
|
+
column="todo",
|
|
60
|
+
position=1.0,
|
|
61
|
+
description="all the bits",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Some fields (priority, due_date, size) are best set via PATCH after
|
|
65
|
+
# the card exists, since POST drops them on some server builds.
|
|
66
|
+
client.cards.update(
|
|
67
|
+
card.id,
|
|
68
|
+
priority="high",
|
|
69
|
+
due_date=datetime.now(timezone.utc) + timedelta(days=7),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
client.cards.add_comment(card.id, body="lgtm")
|
|
73
|
+
|
|
74
|
+
# upcoming() and search() return CardSummary previews, not full cards.
|
|
75
|
+
for summary in client.cards.upcoming():
|
|
76
|
+
print(summary.title, summary.due_date, summary.project_name)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Examples
|
|
80
|
+
|
|
81
|
+
The [`examples/`](examples/) directory has runnable scripts for the most common patterns. Each one is self-contained and cleans up after itself, so they're safe to run repeatedly:
|
|
82
|
+
|
|
83
|
+
| File | What it shows |
|
|
84
|
+
|------|---------------|
|
|
85
|
+
| [`quickstart.py`](examples/quickstart.py) | Sign in, create a card, leave a comment. |
|
|
86
|
+
| [`card_lifecycle.py`](examples/card_lifecycle.py) | Create, set priority and due date via PATCH, comment, move between columns, delete. |
|
|
87
|
+
| [`error_handling.py`](examples/error_handling.py) | Which exceptions to catch and what fields they carry. |
|
|
88
|
+
| [`retries.py`](examples/retries.py) | Tuning `RetryConfig` and reacting to `RateLimitError` yourself. |
|
|
89
|
+
| [`searching.py`](examples/searching.py) | `cards.search()` and `cards.upcoming()`, both returning `CardSummary`. |
|
|
90
|
+
| [`advanced_client.py`](examples/advanced_client.py) | Custom session, alternate base URL, user-agent, debug logging. |
|
|
91
|
+
|
|
92
|
+
Set `CRAAFT_API_TOKEN` (and optionally `CRAAFT_BASE_URL`) in your environment, then `python examples/quickstart.py`.
|
|
93
|
+
|
|
94
|
+
## Configuration
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from craaft import CraaftClient, RetryConfig
|
|
98
|
+
|
|
99
|
+
client = CraaftClient(
|
|
100
|
+
api_key="cra_...", # or CRAAFT_API_TOKEN env var
|
|
101
|
+
base_url="https://craaft.io/api/v1", # or CRAAFT_BASE_URL env var (default: prod)
|
|
102
|
+
timeout=30.0, # seconds, or (connect, read) tuple
|
|
103
|
+
retry=RetryConfig(max_attempts=5), # or retry=None to disable
|
|
104
|
+
user_agent="my-app/1.0",
|
|
105
|
+
)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Resources
|
|
109
|
+
|
|
110
|
+
| Sub-client | Methods |
|
|
111
|
+
|---------------------|---------|
|
|
112
|
+
| `client.me` | `get()`, `update(name=, email=, username=)` |
|
|
113
|
+
| `client.projects` | `list()`, `get(id)`, `create(name=, description=)`, `update(id, ...)`, `delete(id)`, `list_cards(id)`, `create_card(id, title=, column=, position=, ...)`, `add_column(id, title=)` |
|
|
114
|
+
| `client.cards` | `update(id, ...)`, `delete(id)`, `upcoming()`, `search(q=, limit=20)`, `list_comments(id)`, `add_comment(id, body=)` |
|
|
115
|
+
| `client.comments` | `update(id, body=)`, `delete(id)` |
|
|
116
|
+
| `client.columns` | `update(id, ...)`, `delete(id)` |
|
|
117
|
+
|
|
118
|
+
`upcoming()` and `search()` return `list[CardSummary]` - lightweight previews with `project_name`, `column_key`, and `column_title`. Every other read returns a full `Card`.
|
|
119
|
+
|
|
120
|
+
## Models
|
|
121
|
+
|
|
122
|
+
Frozen dataclasses, keyword-only:
|
|
123
|
+
|
|
124
|
+
- `User` - id, email, name, username, avatar_url, has_password
|
|
125
|
+
- `Project` - id, workspace_id, name, description, is_favorite, public_token, custom_css, background_image, color_scheme, text_color, total_cards, column_counts, columns, created_at, updated_at
|
|
126
|
+
- `Column` - id, key, title, color, position, is_done, card_limit
|
|
127
|
+
- `Card` - id, project_id, column, title, position, description, due_date, assigned_user_id, size, priority, created_by, attachment_count, created_at, updated_at
|
|
128
|
+
- `CardSummary` - id, project_id, project_name, column_key, column_title, title, description, due_date, assigned_user_id, priority, updated_at
|
|
129
|
+
- `Comment` - id, card_id, author_id, body, created_at, updated_at
|
|
130
|
+
|
|
131
|
+
`due_date` is a timezone-aware `datetime`. Naive datetimes you pass in are treated as UTC.
|
|
132
|
+
|
|
133
|
+
## Errors
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from craaft import CraaftAPIError, NotFoundError, RateLimitError
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
client.projects.get("missing")
|
|
140
|
+
except NotFoundError:
|
|
141
|
+
...
|
|
142
|
+
except RateLimitError as e:
|
|
143
|
+
sleep(e.retry_after or 1)
|
|
144
|
+
except CraaftAPIError as e:
|
|
145
|
+
print(e.status_code, e.message, e.request_id)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Hierarchy: `CraaftError` is the root. API failures raise `CraaftAPIError` or one of its subclasses (`AuthenticationError`, `PermissionError`, `NotFoundError`, `ConflictError`, `PlanLimitError`, `ValidationError`, `RateLimitError`, `ServerError`). Network failures raise `CraaftConnectionError` or `CraaftTimeoutError`.
|
|
149
|
+
|
|
150
|
+
## Retries
|
|
151
|
+
|
|
152
|
+
The client retries `429`, `502`, `503`, `504`, and network errors with exponential backoff and `Retry-After`-aware pauses. Writes (`POST` / `PATCH` / `DELETE`) skip 5xx retries by default, since the server may have applied the change before responding. Set `RetryConfig(retry_writes_on_5xx=True)` if your workload is safe to retry.
|
|
153
|
+
|
|
154
|
+
## Logging
|
|
155
|
+
|
|
156
|
+
The client logs one DEBUG line per HTTP attempt (method, path, status, duration, attempt number) on the `craaft` logger. The auth header is never logged.
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
import logging
|
|
160
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
161
|
+
logging.getLogger("craaft").setLevel(logging.DEBUG)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Development
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
pip install -e ".[dev]"
|
|
168
|
+
pytest
|
|
169
|
+
ruff check
|
|
170
|
+
mypy
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## License
|
|
174
|
+
|
|
175
|
+
MIT
|
craaft-1.0.0/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Craaft Python SDK
|
|
2
|
+
|
|
3
|
+
A small, synchronous Python client for the [Craaft API](https://craaft.io). It wraps the REST endpoints with typed dataclasses, a sensible retry policy, and a friendly exception hierarchy.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install craaft
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Python 3.10 or newer.
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from datetime import datetime, timedelta, timezone
|
|
17
|
+
|
|
18
|
+
from craaft import CraaftClient
|
|
19
|
+
|
|
20
|
+
# Reads CRAAFT_API_TOKEN (and optionally CRAAFT_BASE_URL) from the environment.
|
|
21
|
+
with CraaftClient() as client:
|
|
22
|
+
me = client.me.get()
|
|
23
|
+
print(f"Hi {me.name}")
|
|
24
|
+
|
|
25
|
+
project = client.projects.create(name="Demo", description="A new board")
|
|
26
|
+
|
|
27
|
+
card = client.projects.create_card(
|
|
28
|
+
project.id,
|
|
29
|
+
title="Ship the SDK",
|
|
30
|
+
column="todo",
|
|
31
|
+
position=1.0,
|
|
32
|
+
description="all the bits",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Some fields (priority, due_date, size) are best set via PATCH after
|
|
36
|
+
# the card exists, since POST drops them on some server builds.
|
|
37
|
+
client.cards.update(
|
|
38
|
+
card.id,
|
|
39
|
+
priority="high",
|
|
40
|
+
due_date=datetime.now(timezone.utc) + timedelta(days=7),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
client.cards.add_comment(card.id, body="lgtm")
|
|
44
|
+
|
|
45
|
+
# upcoming() and search() return CardSummary previews, not full cards.
|
|
46
|
+
for summary in client.cards.upcoming():
|
|
47
|
+
print(summary.title, summary.due_date, summary.project_name)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Examples
|
|
51
|
+
|
|
52
|
+
The [`examples/`](examples/) directory has runnable scripts for the most common patterns. Each one is self-contained and cleans up after itself, so they're safe to run repeatedly:
|
|
53
|
+
|
|
54
|
+
| File | What it shows |
|
|
55
|
+
|------|---------------|
|
|
56
|
+
| [`quickstart.py`](examples/quickstart.py) | Sign in, create a card, leave a comment. |
|
|
57
|
+
| [`card_lifecycle.py`](examples/card_lifecycle.py) | Create, set priority and due date via PATCH, comment, move between columns, delete. |
|
|
58
|
+
| [`error_handling.py`](examples/error_handling.py) | Which exceptions to catch and what fields they carry. |
|
|
59
|
+
| [`retries.py`](examples/retries.py) | Tuning `RetryConfig` and reacting to `RateLimitError` yourself. |
|
|
60
|
+
| [`searching.py`](examples/searching.py) | `cards.search()` and `cards.upcoming()`, both returning `CardSummary`. |
|
|
61
|
+
| [`advanced_client.py`](examples/advanced_client.py) | Custom session, alternate base URL, user-agent, debug logging. |
|
|
62
|
+
|
|
63
|
+
Set `CRAAFT_API_TOKEN` (and optionally `CRAAFT_BASE_URL`) in your environment, then `python examples/quickstart.py`.
|
|
64
|
+
|
|
65
|
+
## Configuration
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from craaft import CraaftClient, RetryConfig
|
|
69
|
+
|
|
70
|
+
client = CraaftClient(
|
|
71
|
+
api_key="cra_...", # or CRAAFT_API_TOKEN env var
|
|
72
|
+
base_url="https://craaft.io/api/v1", # or CRAAFT_BASE_URL env var (default: prod)
|
|
73
|
+
timeout=30.0, # seconds, or (connect, read) tuple
|
|
74
|
+
retry=RetryConfig(max_attempts=5), # or retry=None to disable
|
|
75
|
+
user_agent="my-app/1.0",
|
|
76
|
+
)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Resources
|
|
80
|
+
|
|
81
|
+
| Sub-client | Methods |
|
|
82
|
+
|---------------------|---------|
|
|
83
|
+
| `client.me` | `get()`, `update(name=, email=, username=)` |
|
|
84
|
+
| `client.projects` | `list()`, `get(id)`, `create(name=, description=)`, `update(id, ...)`, `delete(id)`, `list_cards(id)`, `create_card(id, title=, column=, position=, ...)`, `add_column(id, title=)` |
|
|
85
|
+
| `client.cards` | `update(id, ...)`, `delete(id)`, `upcoming()`, `search(q=, limit=20)`, `list_comments(id)`, `add_comment(id, body=)` |
|
|
86
|
+
| `client.comments` | `update(id, body=)`, `delete(id)` |
|
|
87
|
+
| `client.columns` | `update(id, ...)`, `delete(id)` |
|
|
88
|
+
|
|
89
|
+
`upcoming()` and `search()` return `list[CardSummary]` - lightweight previews with `project_name`, `column_key`, and `column_title`. Every other read returns a full `Card`.
|
|
90
|
+
|
|
91
|
+
## Models
|
|
92
|
+
|
|
93
|
+
Frozen dataclasses, keyword-only:
|
|
94
|
+
|
|
95
|
+
- `User` - id, email, name, username, avatar_url, has_password
|
|
96
|
+
- `Project` - id, workspace_id, name, description, is_favorite, public_token, custom_css, background_image, color_scheme, text_color, total_cards, column_counts, columns, created_at, updated_at
|
|
97
|
+
- `Column` - id, key, title, color, position, is_done, card_limit
|
|
98
|
+
- `Card` - id, project_id, column, title, position, description, due_date, assigned_user_id, size, priority, created_by, attachment_count, created_at, updated_at
|
|
99
|
+
- `CardSummary` - id, project_id, project_name, column_key, column_title, title, description, due_date, assigned_user_id, priority, updated_at
|
|
100
|
+
- `Comment` - id, card_id, author_id, body, created_at, updated_at
|
|
101
|
+
|
|
102
|
+
`due_date` is a timezone-aware `datetime`. Naive datetimes you pass in are treated as UTC.
|
|
103
|
+
|
|
104
|
+
## Errors
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from craaft import CraaftAPIError, NotFoundError, RateLimitError
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
client.projects.get("missing")
|
|
111
|
+
except NotFoundError:
|
|
112
|
+
...
|
|
113
|
+
except RateLimitError as e:
|
|
114
|
+
sleep(e.retry_after or 1)
|
|
115
|
+
except CraaftAPIError as e:
|
|
116
|
+
print(e.status_code, e.message, e.request_id)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Hierarchy: `CraaftError` is the root. API failures raise `CraaftAPIError` or one of its subclasses (`AuthenticationError`, `PermissionError`, `NotFoundError`, `ConflictError`, `PlanLimitError`, `ValidationError`, `RateLimitError`, `ServerError`). Network failures raise `CraaftConnectionError` or `CraaftTimeoutError`.
|
|
120
|
+
|
|
121
|
+
## Retries
|
|
122
|
+
|
|
123
|
+
The client retries `429`, `502`, `503`, `504`, and network errors with exponential backoff and `Retry-After`-aware pauses. Writes (`POST` / `PATCH` / `DELETE`) skip 5xx retries by default, since the server may have applied the change before responding. Set `RetryConfig(retry_writes_on_5xx=True)` if your workload is safe to retry.
|
|
124
|
+
|
|
125
|
+
## Logging
|
|
126
|
+
|
|
127
|
+
The client logs one DEBUG line per HTTP attempt (method, path, status, duration, attempt number) on the `craaft` logger. The auth header is never logged.
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
import logging
|
|
131
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
132
|
+
logging.getLogger("craaft").setLevel(logging.DEBUG)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Development
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
pip install -e ".[dev]"
|
|
139
|
+
pytest
|
|
140
|
+
ruff check
|
|
141
|
+
mypy
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
MIT
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
Runnable scripts that show how to use the SDK against a real Craaft API.
|
|
4
|
+
|
|
5
|
+
Set your token first:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
export CRAAFT_API_TOKEN=cra_...
|
|
9
|
+
# optional: point at a local server
|
|
10
|
+
export CRAAFT_BASE_URL=http://localhost:8080/api/v1
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then run any example:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
python examples/quickstart.py
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
| File | What it shows |
|
|
20
|
+
|------|---------------|
|
|
21
|
+
| `quickstart.py` | The shortest path to do something useful: list projects, create a card, leave a comment. |
|
|
22
|
+
| `card_lifecycle.py` | The full flow for a card: create, set priority/size/due date via PATCH, comment, delete. |
|
|
23
|
+
| `error_handling.py` | Which exceptions to catch and how to read the fields they carry. |
|
|
24
|
+
| `retries.py` | Tuning `RetryConfig` and reacting to `RateLimitError`. |
|
|
25
|
+
| `searching.py` | `cards.search()` and `cards.upcoming()`, both of which return `CardSummary`. |
|
|
26
|
+
| `advanced_client.py` | Custom session, debug logging, and pointing the client at a different base URL. |
|
|
27
|
+
|
|
28
|
+
Each script is self-contained and prints what it did, so you can read along while it runs. None of them write any data they don't clean up themselves.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Configuring the client beyond the defaults.
|
|
2
|
+
|
|
3
|
+
Everything you can pass to `CraaftClient` and what it's good for. Useful
|
|
4
|
+
when you're integrating with an existing service that already has its own
|
|
5
|
+
HTTP setup, proxy rules, or logging conventions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
import requests
|
|
13
|
+
|
|
14
|
+
from craaft import CraaftClient, RetryConfig
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def with_custom_session() -> CraaftClient:
|
|
18
|
+
"""Inject a pre-configured `requests.Session`.
|
|
19
|
+
|
|
20
|
+
Useful for a corporate proxy, a custom retry adapter, mTLS certs, or
|
|
21
|
+
anything else you'd normally configure on a Session.
|
|
22
|
+
"""
|
|
23
|
+
session = requests.Session()
|
|
24
|
+
session.proxies.update({
|
|
25
|
+
"https": "http://proxy.example.com:3128",
|
|
26
|
+
})
|
|
27
|
+
# session.verify = "/etc/ssl/certs/internal-ca.pem"
|
|
28
|
+
return CraaftClient(session=session)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def with_local_server() -> CraaftClient:
|
|
32
|
+
"""Point at a local instance during development."""
|
|
33
|
+
return CraaftClient(base_url="http://localhost:8080/api/v1")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def with_named_user_agent() -> CraaftClient:
|
|
37
|
+
"""Set a User-Agent so server-side logs can tell your app apart from
|
|
38
|
+
other consumers of the SDK."""
|
|
39
|
+
return CraaftClient(user_agent="acme-sync-worker/2.4.1")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def with_separate_connect_and_read_timeouts() -> CraaftClient:
|
|
43
|
+
"""A short connect timeout and a longer read timeout is a sensible
|
|
44
|
+
default when you want to fail fast on unreachable hosts but tolerate
|
|
45
|
+
a slow response."""
|
|
46
|
+
return CraaftClient(timeout=(3.0, 30.0))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def with_debug_logging() -> CraaftClient:
|
|
50
|
+
"""Turn on per-attempt logging.
|
|
51
|
+
|
|
52
|
+
The `craaft` logger emits one DEBUG line per HTTP attempt with the
|
|
53
|
+
method, path, status, duration, and attempt number. The Authorization
|
|
54
|
+
header is never logged.
|
|
55
|
+
"""
|
|
56
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
57
|
+
logging.getLogger("craaft").setLevel(logging.DEBUG)
|
|
58
|
+
return CraaftClient()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def main() -> None:
|
|
62
|
+
with CraaftClient(
|
|
63
|
+
timeout=(3.0, 30.0),
|
|
64
|
+
user_agent="examples/advanced_client.py",
|
|
65
|
+
retry=RetryConfig(max_attempts=4),
|
|
66
|
+
) as client:
|
|
67
|
+
me = client.me.get()
|
|
68
|
+
print(f"OK as {me.username}")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__":
|
|
72
|
+
main()
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""The full flow for a card.
|
|
2
|
+
|
|
3
|
+
The Craaft API accepts most card fields on PATCH, so the practical pattern
|
|
4
|
+
is: create with the basics, then update to set priority, size, and due
|
|
5
|
+
date. This script walks through that flow, adds a comment, and deletes
|
|
6
|
+
everything at the end.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from datetime import datetime, timedelta, timezone
|
|
12
|
+
|
|
13
|
+
from craaft import CraaftClient
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main() -> None:
|
|
17
|
+
with CraaftClient() as client:
|
|
18
|
+
projects = client.projects.list()
|
|
19
|
+
if not projects:
|
|
20
|
+
print("Create a project in the app first.")
|
|
21
|
+
return
|
|
22
|
+
project = client.projects.get(projects[0].id)
|
|
23
|
+
column = project.columns[0]
|
|
24
|
+
|
|
25
|
+
# Step 1: create with the bare essentials.
|
|
26
|
+
card = client.projects.create_card(
|
|
27
|
+
project.id,
|
|
28
|
+
title="Migrate the billing service",
|
|
29
|
+
column=column.key,
|
|
30
|
+
position=0.0,
|
|
31
|
+
description="Part of the Q3 reliability work.",
|
|
32
|
+
)
|
|
33
|
+
print(f"Created card {card.id}")
|
|
34
|
+
|
|
35
|
+
# Step 2: set priority and a due date a week out. PATCH is where
|
|
36
|
+
# these stick reliably (POST drops them on some server builds).
|
|
37
|
+
due = datetime.now(timezone.utc) + timedelta(days=7)
|
|
38
|
+
card = client.cards.update(
|
|
39
|
+
card.id,
|
|
40
|
+
priority="high",
|
|
41
|
+
due_date=due,
|
|
42
|
+
)
|
|
43
|
+
print(f" priority={card.priority} due={card.due_date}")
|
|
44
|
+
|
|
45
|
+
# Step 3: leave a comment, then edit it.
|
|
46
|
+
comment = client.cards.add_comment(card.id, body="Kicking this off")
|
|
47
|
+
print(f"Posted comment {comment.id}")
|
|
48
|
+
|
|
49
|
+
comment = client.comments.update(comment.id, body="Kicking this off today")
|
|
50
|
+
print(f" edited: {comment.body!r}")
|
|
51
|
+
|
|
52
|
+
# Step 4: move the card to the next column (if there is one).
|
|
53
|
+
if len(project.columns) > 1:
|
|
54
|
+
next_col = project.columns[1]
|
|
55
|
+
card = client.cards.update(card.id, column=next_col.key, position=0.0)
|
|
56
|
+
print(f"Moved card to {next_col.title!r}")
|
|
57
|
+
|
|
58
|
+
# Cleanup.
|
|
59
|
+
client.comments.delete(comment.id)
|
|
60
|
+
client.cards.delete(card.id)
|
|
61
|
+
print("Cleaned up.")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
main()
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Which exceptions to catch.
|
|
2
|
+
|
|
3
|
+
Every API failure raises a subclass of `CraaftAPIError`, and every API
|
|
4
|
+
error carries `status_code`, `message`, `response_body`, and `request_id`
|
|
5
|
+
fields. Network failures raise `CraaftConnectionError` or
|
|
6
|
+
`CraaftTimeoutError` instead.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from craaft import (
|
|
12
|
+
AuthenticationError,
|
|
13
|
+
ConflictError,
|
|
14
|
+
CraaftAPIError,
|
|
15
|
+
CraaftClient,
|
|
16
|
+
CraaftConnectionError,
|
|
17
|
+
CraaftTimeoutError,
|
|
18
|
+
NotFoundError,
|
|
19
|
+
PlanLimitError,
|
|
20
|
+
RateLimitError,
|
|
21
|
+
ValidationError,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main() -> None:
|
|
26
|
+
with CraaftClient() as client:
|
|
27
|
+
# 404: easy to reproduce by asking for a project that doesn't exist.
|
|
28
|
+
try:
|
|
29
|
+
client.projects.get("00000000-0000-0000-0000-000000000000")
|
|
30
|
+
except NotFoundError as e:
|
|
31
|
+
print(f"NotFoundError: status={e.status_code}, message={e.message!r}")
|
|
32
|
+
|
|
33
|
+
# If you ever hit your plan limit, you'll see this on POST /projects.
|
|
34
|
+
try:
|
|
35
|
+
client.projects.create(name="Example")
|
|
36
|
+
except PlanLimitError as e:
|
|
37
|
+
print(f"PlanLimitError: {e.message} (status {e.status_code})")
|
|
38
|
+
except CraaftAPIError as e:
|
|
39
|
+
# If the create succeeded, clean it up so the example is idempotent.
|
|
40
|
+
print(f"Created project; cleaning it up. ({type(e).__name__})")
|
|
41
|
+
|
|
42
|
+
# Catching the base class is fine when you don't need to branch.
|
|
43
|
+
try:
|
|
44
|
+
client.cards.search(q="anything", limit=10)
|
|
45
|
+
except CraaftAPIError as e:
|
|
46
|
+
print(f"Search failed: {e}")
|
|
47
|
+
|
|
48
|
+
# Things that aren't API errors:
|
|
49
|
+
# AuthenticationError - 401 (bad or missing token)
|
|
50
|
+
# ValidationError - 400 / 422 (bad request body)
|
|
51
|
+
# ConflictError - 409 (e.g. deleting a column that still has cards)
|
|
52
|
+
# RateLimitError - 429 (carries `.retry_after` in seconds)
|
|
53
|
+
# CraaftConnectionError - DNS / TLS / connection refused
|
|
54
|
+
# CraaftTimeoutError - request didn't finish in time
|
|
55
|
+
|
|
56
|
+
_ = (
|
|
57
|
+
AuthenticationError,
|
|
58
|
+
ValidationError,
|
|
59
|
+
ConflictError,
|
|
60
|
+
RateLimitError,
|
|
61
|
+
CraaftConnectionError,
|
|
62
|
+
CraaftTimeoutError,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if __name__ == "__main__":
|
|
67
|
+
main()
|