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.
Files changed (37) hide show
  1. craaft-1.0.0/.github/workflows/publish.yml +53 -0
  2. craaft-1.0.0/.github/workflows/test.yml +36 -0
  3. craaft-1.0.0/.gitignore +12 -0
  4. craaft-1.0.0/PKG-INFO +175 -0
  5. craaft-1.0.0/README.md +146 -0
  6. craaft-1.0.0/examples/README.md +28 -0
  7. craaft-1.0.0/examples/advanced_client.py +72 -0
  8. craaft-1.0.0/examples/card_lifecycle.py +65 -0
  9. craaft-1.0.0/examples/error_handling.py +67 -0
  10. craaft-1.0.0/examples/quickstart.py +49 -0
  11. craaft-1.0.0/examples/retries.py +63 -0
  12. craaft-1.0.0/examples/searching.py +46 -0
  13. craaft-1.0.0/pyproject.toml +65 -0
  14. craaft-1.0.0/src/craaft/__init__.py +44 -0
  15. craaft-1.0.0/src/craaft/_http.py +244 -0
  16. craaft-1.0.0/src/craaft/_version.py +1 -0
  17. craaft-1.0.0/src/craaft/client.py +81 -0
  18. craaft-1.0.0/src/craaft/exceptions.py +82 -0
  19. craaft-1.0.0/src/craaft/models.py +212 -0
  20. craaft-1.0.0/src/craaft/resources/__init__.py +0 -0
  21. craaft-1.0.0/src/craaft/resources/_base.py +12 -0
  22. craaft-1.0.0/src/craaft/resources/cards.py +76 -0
  23. craaft-1.0.0/src/craaft/resources/columns.py +35 -0
  24. craaft-1.0.0/src/craaft/resources/comments.py +17 -0
  25. craaft-1.0.0/src/craaft/resources/me.py +29 -0
  26. craaft-1.0.0/src/craaft/resources/projects.py +117 -0
  27. craaft-1.0.0/tests/__init__.py +0 -0
  28. craaft-1.0.0/tests/conftest.py +0 -0
  29. craaft-1.0.0/tests/test_cards.py +200 -0
  30. craaft-1.0.0/tests/test_client.py +123 -0
  31. craaft-1.0.0/tests/test_columns.py +64 -0
  32. craaft-1.0.0/tests/test_comments.py +35 -0
  33. craaft-1.0.0/tests/test_exceptions.py +75 -0
  34. craaft-1.0.0/tests/test_http.py +250 -0
  35. craaft-1.0.0/tests/test_me.py +55 -0
  36. craaft-1.0.0/tests/test_models.py +259 -0
  37. 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
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .ruff_cache/
7
+ .venv/
8
+ build/
9
+ dist/
10
+ htmlcov/
11
+ .coverage
12
+ docs/
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()