Metadata-Version: 2.4
Name: pyclann
Version: 0.1.0
Summary: Python client library for the Clann family-tree API
License: MIT
Project-URL: Homepage, https://github.com/ullav-dev/pyclann
Project-URL: Documentation, https://ullav-dev.github.io/pyclann/
Project-URL: Bug Tracker, https://github.com/ullav-dev/pyclann/issues
Keywords: clann,genealogy,family-tree,ullav
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: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.28
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: pytest-cov>=4; extra == "dev"
Requires-Dist: responses>=0.23; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Requires-Dist: mypy; extra == "dev"
Requires-Dist: types-requests; extra == "dev"
Provides-Extra: docs
Requires-Dist: mkdocs-material>=9; extra == "docs"
Requires-Dist: mkdocstrings[python]>=0.25; extra == "docs"

# pyclann

Python client library for the [Clann](https://github.com/colinmanning/clann-server) family-tree API.

## Installation

```bash
pip install pyclann
```

## Quick start

```python
from pyclann import ClannClient

client = ClannClient(
    api_url="http://localhost:8090",
    auth_url="http://localhost:8081",   # ullav-user-management service
)
client.login(email="user@example.com", password="secret")

# Family trees
trees = client.trees.list(owner="alice")
tree = client.trees.create("walsh-family", "Walsh Family", owner="alice")

# Persons
father = client.persons.create(
    "Walsh", "Patrick", "Male",
    trees=["walsh-family"],
    date_of_birth="1820-06-01",
    created_by="alice",
)
son = client.persons.create(
    "Walsh", "Michael", "Male",
    trees=["walsh-family"],
    created_by="alice",
)

# Relationships
client.relationships.add(son.id, "Father", father.id)
rels = client.relationships.get(son.id)       # RelationshipsResponse

# Life events
client.life_events.create(
    father.id, "Born in Galway", "Birth",
    date="1820-06-01",
    created_by="alice",
)

# Research notes
note = client.notes.create(
    "Walsh Family Research",
    trees=["walsh-family"],
    body="Found records at Galway archives.",
    is_shared=True,
    created_by="alice",
)
client.notes.create_reply(note.id, "Also check Dublin records.", created_by="alice")

# Profile picture
with open("patrick.jpg", "rb") as f:
    client.persons.upload_image(father.id, f.read(), "image/jpeg")

image_bytes = client.persons.get_image(father.id)   # public endpoint, no auth needed
```

## Authentication

`ClannClient` authenticates against the `ullav-user-management` service, which issues
the JWT accepted by the Clann server.

- `api_url` — Clann server base URL (e.g. `http://clann-server:8090`)
- `auth_url` — auth service base URL (e.g. `http://ullav-user-management:8081`);
  omit if both services are behind the same proxy

Call `client.login(email, password)` before any other method.  Tokens expire according
to the server's configuration; call `login()` again to refresh.

## Resource clients

| Attribute | Resource |
|---|---|
| `client.trees` | Family tree CRUD, primary flag, team assignment, avatar image |
| `client.persons` | Person CRUD, tree membership, profile/life-story media |
| `client.relationships` | Add/remove father/mother/sibling/spouse edges, family-tree view |
| `client.life_events` | Life event CRUD per person |
| `client.notes` | Research note CRUD, folder assignment, replies |
| `client.folders` | Research folder CRUD |
| `client.chat` | AI chat session and message management |
| `client.ai_settings` | Per-user AI provider settings (encrypted BYOK) |

## Error handling

```python
from pyclann import ClannAuthError, ClannNotFoundError, ClannValidationError

try:
    tree = client.trees.get("non-existent-tree")
except ClannNotFoundError:
    print("tree not found")
except ClannAuthError:
    client.login(email, password)   # token expired — re-authenticate
except ClannValidationError as e:
    print("bad request:", e)
```

| Exception | HTTP status |
|---|---|
| `ClannAuthError` | 401 / 403, or `login()` not called |
| `ClannNotFoundError` | 404 |
| `ClannValidationError` | 400 |
| `ClannServerError` | 5xx |
| `ClannError` | base class |

## Record IDs

The Clann API uses SurrealDB record IDs of the form `"table:ulid"`, e.g.
`"person:01jd4a8xyz"`.  All client methods that take an `*_id` parameter accept
either the full form or the bare ULID — the library strips the table prefix when
building URL paths.

```python
person = client.persons.get("person:01jd4a8xyz")   # full ID
person = client.persons.get("01jd4a8xyz")           # bare ULID — both work
```

Note: when specifying a `related_id` in relationship calls, always pass the **full**
record ID (`"person:01jd4a8xyz"`), since the server uses it verbatim in URL paths.

## Relationship types

```python
from pyclann import RelationshipType, SiblingType

# Add a father
client.relationships.add(child.id, RelationshipType.FATHER, father.id)

# Add a sibling — sibling_type is required
client.relationships.add(
    person.id,
    RelationshipType.SIBLING,
    sibling.id,
    sibling_type=SiblingType.BROTHER,
)

# Add a spouse with dates
client.relationships.add(
    person.id,
    RelationshipType.SPOUSE,
    spouse.id,
    spouse_from="1845-09-14",
)

# Remove a relationship
client.relationships.remove(child.id, RelationshipType.FATHER, father.id)
```

## Image uploads

Profile pictures accept JPEG or PNG only (max 2 MB).  Life-story media accepts
the same image formats on Individual/Family plans; Professional/Enterprise plans
also allow video (MP4, MOV, WebM), audio (MP3, WAV, OGG), and PDF.

```python
# Upload
with open("profile.jpg", "rb") as f:
    client.persons.upload_image(person.id, f.read(), "image/jpeg")

# Download — no login() required
data = client.persons.get_image(person.id)
with open("downloaded.jpg", "wb") as f:
    f.write(data)
```

## Licence

MIT
