# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

Python client library for the Duffel travel booking API (flights, stays, payments, notifications, identity). The API is documented at https://duffel.com/docs/api/overview/welcome — there is no official OpenAPI spec, so we maintain one generated from the docs.

## Commands

```bash
# Install (uses uv)
uv pip install -e ".[dev]"

# Run all tests
.venv/bin/pytest tests -v

# Run a single test file
.venv/bin/pytest tests/test_resources/test_orders.py -v

# Run a single test
.venv/bin/pytest tests/test_resources/test_orders.py::test_create_order -v

# Lint
.venv/bin/ruff check src/ tests/
```

## Architecture

### Package layout (`src/duffelbag/`)

- `client.py` — Sync `Duffel` class. Entry point that exposes all resource groups as `@cached_property` attributes (e.g. `client.offers`, `client.stays_bookings`).
- `async_client.py` — Async `AsyncDuffel` class. Same API surface but with `async`/`await` and `async with` support.
- `_base.py` — Core transport and base classes for both sync and async: `HttpTransport`/`AsyncHttpTransport`, `PageIterator`/`AsyncPageIterator`, `BaseResource`/`AsyncBaseResource`. Shared `unwrap_*` helpers.
- `_exceptions.py` — Exception hierarchy mapped to HTTP status codes. `raise_for_response()` parses error bodies into typed exceptions. Shared by sync and async.
- `models/` — Pydantic v2 data models. `_generated.py` is auto-generated from the OpenAPI spec; `__init__.py` re-exports with cleaned-up names (renames `Type1`→`PlaceType`, etc.). Shared by sync and async.
- `resources/` — One module per API section. Each file contains both sync and async resource classes (e.g. `Orders` and `AsyncOrders`). Resources are thin — they build params/body dicts and delegate to base class helpers.

### Python version targeting

Targets Python 3.12+. Does NOT use `from __future__ import annotations`. Instead, forward references and self-references are quoted as strings (e.g. `-> "Duffel":`, `city: "City | None"`). In resource classes that define a `list()` method, all subsequent `list[...]` annotations in the same class must be quoted (e.g. `users: "list[str]"`) to avoid shadowing the builtin `list` with the method.

### Key design patterns

- **Two API hosts**: Main API at `api.duffel.com`, card endpoints at `api.duffel.cards` (PCI). The `Cards` resource passes `base_url=CARDS_BASE_URL` to transport calls.
- **Envelope pattern**: All Duffel requests wrap body in `{"data": {...}}`, responses return `{"data": ...}` or `{"data": [...], "meta": {...}}`. Handled by `unwrap_data()`/`unwrap_list()` in `_base.py`.
- **Sync/async parity**: Every resource has both sync and async variants. Sync uses `httpx.Client`; async uses `httpx.AsyncClient`. Models, exceptions, and unwrap functions are shared. Async pagination uses `AsyncPageIterator[T]` with `async for`.
- **Pagination**: `PageIterator[T]` / `AsyncPageIterator[T]` lazily fetches pages. Supports both full iteration (`for item in client.airlines.list()`) and page-at-a-time (`iterator.page()`).
- **Timeouts**: Default 30s, but order/booking creation endpoints use 130s (`LONG_TIMEOUT`) per Duffel's recommendation.
- **Rate limiting**: Auto-retries once on 429 if `ratelimit-reset` header indicates a reasonable wait.

### Testing

Tests use `httpx`'s transport layer for mocking — no network calls. `MockTransport` (sync) and `AsyncMockTransport` (async) in `conftest.py` record requests and return queued responses. Tests verify both request construction (method, path, body) and response deserialization into Pydantic models. Async tests use `pytest-asyncio` with `mode=auto`.
