# CLAUDE.md — 3CX Python SDK

## Project overview

Python SDK for the 3CX XAPI (`/xapi/v1`).  The source is `swagger.yaml` (OpenAPI 3.0.4, OData v4, generated by Microsoft.OpenApi.OData).  The SDK lives in `threecx/`.

## Development setup

```bash
pip install -e ".[dev]"   # install editable with dev extras
```

Dependencies: `httpx>=0.27`, `pydantic>=2.0`.  Dev extras add `pytest`, `pytest-httpx`, `ruff`, `mypy`.

## Commands to run after changes

```bash
ruff check threecx/          # lint
mypy threecx/                # type-check
pytest                       # tests (once a tests/ dir exists)
```

No build step is required; the package is pure Python.

## Architecture — one rule per layer

| Layer | Where | Rule |
|---|---|---|
| HTTP + auth | `auth.py`, `services/base.py` | All HTTP goes through `BaseService._get/_post/_patch/_delete`. Never call `self._http` directly in a service subclass. |
| OData queries | `odata.py` | `ODataQuery` is the only way to build query params. Never construct `$filter` strings inside service methods. |
| Models | `models/*.py` | Pydantic v2. Field aliases are PascalCase (matching the API). Attributes are snake_case. Always use `model_dump(by_alias=True, exclude_none=True)` when serialising for the wire. |
| Services | `services/*.py` | Each service takes only `httpx.Client` in `__init__`. No business logic — translate Python calls to HTTP and return typed objects. |
| Entry point | `client.py` | `ThreeCXClient` owns the `httpx.Client` lifetime. All services are instantiated there. |

## Adding a new service

1. Add a Pydantic model in `threecx/models/<domain>.py` (see existing files for the pattern).
2. Export it from `threecx/models/__init__.py`.
3. Create `threecx/services/<domain>.py` subclassing `BaseService`.
4. Register it in `ThreeCXClient.__init__` as `self.<name> = <Service>(self._http)`.
5. Export the service from `threecx/services/__init__.py`.

## Coding conventions

- `from __future__ import annotations` at the top of every file.
- No comments explaining what the code does — only `# WHY:` comments for non-obvious constraints.
- No `model_dump()` without `by_alias=True, exclude_none=True` on the wire.
- OData path parameters go directly into the URL string, not as query params.
- `list()` returns a single page; `iterate()` yields all pages via `_paginate()`.
- Report endpoints return `List[Dict[str, Any]]` — raw dicts, not typed models, because the report schemas are highly variable.

## Things to avoid

- Do not add `async` variants — the SDK is sync only for now.
- Do not add retry logic inside services; leave that to the caller or an httpx transport.
- Do not create new top-level files outside `threecx/` without discussing first.
- Do not import from `swagger.yaml` at runtime — the file is a reference only.

## Swagger reference

`swagger.yaml` in the project root is the authoritative API spec.  It is 39 000+ lines and 1.1 MB — use `grep -n` or the Explore agent to look things up; do not read it whole.

Key patterns in the spec:
- Collection GET: `GET /Resource` → returns `{"value": [...], "@odata.nextLink": "..."}`
- Single entity: `GET /Resource({Id})` — `Id` is always an `int32`
- By extension number: `GET /Resource(Number='{Number}')`
- Actions (POST side-effects, no body required): `POST /Resource({Id})/Pbx.ActionName`
- Functions (GET with path params): `GET /Resource/Pbx.FunctionName(param={param})`
- All collections accept: `$top`, `$skip`, `$count`, `$filter`, `$search`, `$orderby`, `$select`, `$expand`
