# Python SDK

> The typed tasksmate client — install, quickstart, pagination, errors, pandas and webhook verification.

**What this is:** `tasksmate`, a typed client generated from the same contract as this site. **When you need it:** any Python that talks to TasksMate — scripts, services, notebooks.

```python
from tasksmate import TasksMate

tm = TasksMate(token="tm_live_…")                   # or set TASKSMATE_TOKEN and call TasksMate()
for task in tm.tasks.list(org_id="O0020"):           # every task, page after page
    print(task.task_id, task.status, task.title)
tm.views.rows("V123456").to_dataframe()               # a saved view as a pandas DataFrame
```

-   **Token:** mint it in TasksMate → **Developers → Tokens** (it is shown once).
-   **OAuth apps:** the authorization flow (`/oauth/authorize`, `/oauth/token`, `/.well-known/*`) is not wrapped here — run it with any OAuth 2.1 library; the access token it returns is a token like any other: `TasksMate(token=…)`.
-   **Reach:** a token belongs to one organization and carries **scopes** (`tasks:read`, `tasks:write`, `projects:read`, …); each method's docstring names the scope it needs.
-   **Try it:** `examples/quickstart.py` runs a full round trip — create, conditional update, filtered list, DataFrame, delete — and leaves nothing behind.

## Install

```bash
pip install tasksmate                  # the client (Python 3.10+)
pip install "tasksmate[pandas]"        # + DataFrames
pip install "tasksmate[cli]"           # + the `tm` command line
```

_Not on PyPI yet: until the first release, install from a checkout — `pip install -e ".[pandas,cli]"` — or from a built wheel (`python -m build`, then `pip install "dist/tasksmate-<version>-py3-none-any.whl[pandas]"`)._

## Lists and pagination

Every list returns a `Page`: `.data` (this page), `.next_cursor`, and iteration over **every** item, following the cursor until it is `None` (a page may be short, or even empty, while more follow — iteration handles it).

```python
page = tm.tasks.list(org_id="O0020", filter={"status": ["in_progress", "blocked"], "search": "invoice"},
                     sort_by="due_date", limit=200)
page.data, page.next_cursor          # the first page
for task in page: ...                # all of them
for p in page.pages(): ...           # page by page
page.envelope                        # the full response model (some lists carry sums beside `data`)
```

`filter={key: value}` becomes `filter[key]=value` (a list is comma-joined); an unknown key raises `ValueError` naming the allowed ones. The grammar — keys, sorts, limits — is the API's [list grammar](https://developers.tasksmate.indrasol.com/guides/pagination-and-filters/).

## Errors

Every API error is `application/problem+json`, and every problem type is its own exception, all subclasses of `tasksmate.TasksMateError` (itself an `APIError`) with `.status`, `.type`, `.title`, `.detail`, `.request_id`, `.errors`:

```python
from tasksmate import InsufficientScopeError, NotFoundError, PreconditionFailedError, TasksMateError

try:
    tm.tasks.update("T123456", {"status": "completed"})
except InsufficientScopeError as exc:
    print("this token needs", exc.required_scope)          # e.g. tasks:write
except NotFoundError:
    ...
except TasksMateError as exc:
    print(exc.status, exc.detail, "— quote request_id", exc.request_id)
```

| Status | Exceptions |
| --- | --- |
| 400 | `BadRequestError` · `InvalidParameterError` · `IdempotencyKeyInvalidError` |
| 401 | `AuthenticationError` · `TokenInvalidError` · `TokenExpiredError` · `TokenRevokedError` |
| 403 | `ForbiddenError` · `InsufficientScopeError` · `TestTokenReadOnlyError` · `TokenPolicyError` (also 422) |
| 404 · 409 · 412 | `NotFoundError` · `ConflictError` (`IdempotencyKeyInFlightError`) · `PreconditionFailedError` |
| 422 | `UnprocessableEntityError` · `ValidationError` · `IdempotencyKeyReusedError` · `UrlRefusedError` |
| 429 · 5xx | `RateLimitedError` (`.retry_after`) · `InternalError` |
| — | `APIConnectionError` / `APITimeoutError` (no HTTP answer) · `ResponseValidationError` (a 2xx this SDK version cannot read — upgrade) |

## pandas

```python
tm.tasks.list(org_id="O0020").to_dataframe()             # every page; to_dataframe(all_pages=False) for one
tm.views.rows("V123456").to_dataframe()
```

One row per item, with the API's own field names:

-   a nested object becomes dotted columns (`type_data.severity`);
-   a list of scalars becomes one comma-separated string (`tags` → `"api, backend"`);
-   `*_at` columns are timezone-aware datetimes, `*_date` columns `datetime.date` (missing → `None`).

Needs the `pandas` extra — without it you get an `ImportError` saying so.

## Webhooks

```python
from tasksmate import webhooks

def receive(headers, raw_body: bytes):
    if not webhooks.verify(SECRET, headers, raw_body):     # the RAW body, not re-serialized JSON
        return 400
    event = webhooks.parse(raw_body)                         # WebhookEvent: .id .type_ .org_id .data.resource_id …
    ...                                                      # dedupe on event.id — delivery is at-least-once
    return 200
```

`verify` implements the Standard Webhooks recipe TasksMate signs with:

-   **Signature:** `v1,` HMAC-SHA256 over `id.timestamp.body`, keyed by the base64-decoded part of the `whsec_…` secret.
-   **Replay window:** timestamps more than `tolerance` seconds (300) away are refused.
-   **Rotation, both ways:** during the 24 h after a secret rotation TasksMate sends two signatures, and you may pass both secrets — `verify([new, old], …)` — while you switch over.
-   **Errors:** `raise_on_failure=True` raises `WebhookVerificationError` with the reason instead of returning `False`.

Every method, generated per resource: the [API reference](https://developers.tasksmate.indrasol.com/reference/) shows each operation’s Python call.

---
Source: https://developers.tasksmate.indrasol.com/sdks/python/
