Metadata-Version: 2.4
Name: craaft
Version: 1.0.0
Summary: Python client for the Craaft API.
Project-URL: Homepage, https://craaft.io
Project-URL: Source, https://github.com/craaft/python-sdk
Project-URL: Issues, https://github.com/craaft/python-sdk/issues
Author-email: Sean Nieuwoudt <sean@underwulf.com>
License-Expression: MIT
Keywords: api,client,craaft,sdk
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.10
Requires-Dist: requests>=2.32
Provides-Extra: dev
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: responses>=0.25; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Requires-Dist: types-requests; extra == 'dev'
Description-Content-Type: text/markdown

# Craaft Python SDK

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.

## Install

```bash
pip install craaft
```

Python 3.10 or newer.

## Quickstart

```python
from datetime import datetime, timedelta, timezone

from craaft import CraaftClient

# Reads CRAAFT_API_TOKEN (and optionally CRAAFT_BASE_URL) from the environment.
with CraaftClient() as client:
    me = client.me.get()
    print(f"Hi {me.name}")

    project = client.projects.create(name="Demo", description="A new board")

    card = client.projects.create_card(
        project.id,
        title="Ship the SDK",
        column="todo",
        position=1.0,
        description="all the bits",
    )

    # Some fields (priority, due_date, size) are best set via PATCH after
    # the card exists, since POST drops them on some server builds.
    client.cards.update(
        card.id,
        priority="high",
        due_date=datetime.now(timezone.utc) + timedelta(days=7),
    )

    client.cards.add_comment(card.id, body="lgtm")

    # upcoming() and search() return CardSummary previews, not full cards.
    for summary in client.cards.upcoming():
        print(summary.title, summary.due_date, summary.project_name)
```

## Examples

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:

| File | What it shows |
|------|---------------|
| [`quickstart.py`](examples/quickstart.py) | Sign in, create a card, leave a comment. |
| [`card_lifecycle.py`](examples/card_lifecycle.py) | Create, set priority and due date via PATCH, comment, move between columns, delete. |
| [`error_handling.py`](examples/error_handling.py) | Which exceptions to catch and what fields they carry. |
| [`retries.py`](examples/retries.py) | Tuning `RetryConfig` and reacting to `RateLimitError` yourself. |
| [`searching.py`](examples/searching.py) | `cards.search()` and `cards.upcoming()`, both returning `CardSummary`. |
| [`advanced_client.py`](examples/advanced_client.py) | Custom session, alternate base URL, user-agent, debug logging. |

Set `CRAAFT_API_TOKEN` (and optionally `CRAAFT_BASE_URL`) in your environment, then `python examples/quickstart.py`.

## Configuration

```python
from craaft import CraaftClient, RetryConfig

client = CraaftClient(
    api_key="cra_...",                     # or CRAAFT_API_TOKEN env var
    base_url="https://craaft.io/api/v1",   # or CRAAFT_BASE_URL env var (default: prod)
    timeout=30.0,                          # seconds, or (connect, read) tuple
    retry=RetryConfig(max_attempts=5),     # or retry=None to disable
    user_agent="my-app/1.0",
)
```

## Resources

| Sub-client          | Methods |
|---------------------|---------|
| `client.me`         | `get()`, `update(name=, email=, username=)` |
| `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=)` |
| `client.cards`      | `update(id, ...)`, `delete(id)`, `upcoming()`, `search(q=, limit=20)`, `list_comments(id)`, `add_comment(id, body=)` |
| `client.comments`   | `update(id, body=)`, `delete(id)` |
| `client.columns`    | `update(id, ...)`, `delete(id)` |

`upcoming()` and `search()` return `list[CardSummary]` - lightweight previews with `project_name`, `column_key`, and `column_title`. Every other read returns a full `Card`.

## Models

Frozen dataclasses, keyword-only:

- `User` - id, email, name, username, avatar_url, has_password
- `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
- `Column` - id, key, title, color, position, is_done, card_limit
- `Card` - id, project_id, column, title, position, description, due_date, assigned_user_id, size, priority, created_by, attachment_count, created_at, updated_at
- `CardSummary` - id, project_id, project_name, column_key, column_title, title, description, due_date, assigned_user_id, priority, updated_at
- `Comment` - id, card_id, author_id, body, created_at, updated_at

`due_date` is a timezone-aware `datetime`. Naive datetimes you pass in are treated as UTC.

## Errors

```python
from craaft import CraaftAPIError, NotFoundError, RateLimitError

try:
    client.projects.get("missing")
except NotFoundError:
    ...
except RateLimitError as e:
    sleep(e.retry_after or 1)
except CraaftAPIError as e:
    print(e.status_code, e.message, e.request_id)
```

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`.

## Retries

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.

## Logging

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.

```python
import logging
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("craaft").setLevel(logging.DEBUG)
```

## Development

```bash
pip install -e ".[dev]"
pytest
ruff check
mypy
```

## License

MIT
