# Treetop Client Python - AI Agent Instructions

## Project Overview

This is a dataclass-based HTTPX client for the [Treetop REST API](https://github.com/terjekv/treetop-rest), a Cedar-based policy authorization server. The client provides both sync and async methods to check authorization decisions.

**Key Constraint**: Python ≥ 3.12 only. Zero runtime dependencies beyond HTTPX.

## Architecture

### Singleton Pattern
[client.py](../src/treetop_client/client.py) implements `TreeTopClient` as a singleton via metaclass `_Singleton`. This ensures only one HTTPX client instance exists per process. Always call `.close()` or `.aclose()` to properly reset the singleton, especially in tests.

### Dual API Pattern
The client provides paired sync/async methods:
- `check()` / `acheck()` - Brief responses (decision only)
- `check_detailed()` / `acheck_detailed()` - Includes matched policy in Cedar and JSON formats

### Data Model Structure
[models.py](../src/treetop_client/models.py) follows a strict hierarchy:
- `Request` → contains `Principal` (User|Group), `Action`, `Resource`
- `QualifiedId` → namespace-aware identifiers (e.g., `DNS::User::"alice"`)
- `ResourceAttribute` → typed values (STRING, NUMBER, BOOLEAN, IP)
- `PolicyVersion` → tracks policy snapshots (`hash: str`, `loaded_at: datetime`)

**Critical**: Colons (`:`) are forbidden in IDs and namespaces (enforced by `_no_colon()` validation). Cedar uses `::` as namespace separator.

## Type Conversion Semantics

When constructing `ResourceAttribute.to_api()`:
- `BOOLEAN` strings → actual Python booleans (`"true"` → `True`)
- `NUMBER` strings → Python floats (`"42"` → `42.0`)
- `STRING` and `IP` → remain as strings

Example from README:
```python
attrs["ip"] = ResourceAttribute.new("10.0.0.1", ResourceAttributeType.IP)
attrs["name"] = ResourceAttribute.new("myhost.example.com", ResourceAttributeType.STRING)
```

## Testing Strategy

### Test Markers
Uses pytest markers for test separation:
- **Unit tests** (default): Mock HTTPX with `pytest-httpx`
- **Integration tests** (`@pytest.mark.integration`): Spin up real Docker Compose stack

Run commands:
```bash
pytest                  # Unit tests only (default)
pytest -m integration   # Integration tests (requires Docker)
```

### Integration Test Setup
[test_integration.py](../tests/test_integration.py#L20-L42) uses `docker_compose_up_down` fixture:
1. Spins up [docker-compose.integration.yml](../docker-compose.integration.yml) with two services:
   - `miniserve` serving Cedar policies from `./testdata/`
   - `treetop-rest` server (port 10101) fetching policies from miniserve
2. Polls `/api/v1/status` endpoint until `policies.size != 0`
3. Tears down on test completion

### Version Tracking
`CheckResponse` includes an optional `version` field containing `PolicyVersion` (hash + timestamp). Both Allow and Deny decisions may include version metadata for tracking policy changes. The field is `None` when connecting to older API versions (< 0.0.12) that don't support version tracking.

Access version data via helper methods:
- `version_hash()` → `str | None` - SHA-256 hash of policy text
- `version_loaded_at()` → `datetime | None` - Timezone-aware datetime of policy load

## Development Conventions

### Dataclass Patterns
All models use `@dataclass(slots=True, frozen=True)` for immutability and memory efficiency.

### Factory Methods
Prefer `.new()` class methods for ergonomic construction:
```python
User.new("alice", ["DNS"], ["admins"])  # Instead of manual QualifiedId/Group construction
```

### API Conversion
The `as_api()` helper function handles both `Request` objects and raw dicts, supporting flexible input types.

## Common Pitfalls

1. **Singleton Reset**: Always use `client_cleanup` fixture in tests ([test_client.py](../tests/test_client.py#L14-L19)) to reset singleton state between tests.

2. **Empty Resource Attributes**: `Resource.attrs` cannot be empty (enforced in `__post_init__`). Always provide at least one attribute.

3. **Namespace Assumptions**: `User.new()` assumes groups share the user's namespace. For cross-namespace groups, construct manually.

4. **Async Event Loop**: Integration tests manually create event loops ([test_client.py](../tests/test_client.py#L120-L123)). Use proper async test fixtures in production code.

## Build & Package Management

Uses Poetry for dependency management. Key config in [pyproject.toml](../pyproject.toml):
- Build backend: `poetry-core>=2.0.0`
- Test addopts: `-m 'not integration'` (excludes integration tests by default)

When adding dependencies, update both `dependencies` and relevant `tool.poetry.group.dev.dependencies` sections.
