Metadata-Version: 2.4
Name: gramps-web-api-client
Version: 0.2
Summary: A simple Python client to interact with Gramps Web API
Author-email: "David M. Straub" <straub@protonmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/DavidMStraub/gramps-web-api-client
Project-URL: Repository, https://github.com/DavidMStraub/gramps-web-api-client
Project-URL: Issues, https://github.com/DavidMStraub/gramps-web-api-client/issues
Keywords: gramps,gramps-web,genealogy,api,client
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Sociology :: Genealogy
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.31.0

# Gramps Web API Client


A simple Python client based on `requests` for interacting with a [Gramps Web API](https://github.com/gramps-project/gramps-webapi/) server.

## Warning

This is an experimental project for advanced users. The goal is to allow powerful batch operations on Gramps objects via the API. But only use it if you know what you are doing &ndash; you can easily break your Gramps database with this tool!

## Install

```bash
pip install gramps-web-api-client
```

## Quick start

First, instantiate the API instance:

```python
from gramps_web_api_client import API

api = API(
    host="https://my-gramps-web-instance.com",
    basic_auth=("my_user", "my_password")
)
```

The client reuses a single HTTP session. It can also be used as a context manager, which closes the session when done:

```python
with API(host="https://my-gramps-web-instance.com", basic_auth=("my_user", "my_password")) as api:
    print(api.get_metadata())
```

Optional arguments are `timeout` (seconds per request, default 30) and `session` (a preconfigured `requests.Session`). Access tokens are fetched on first use and renewed automatically when they expire.

Python 3.10 or newer is required.

## Methods

For each object type, the client has methods to iterate over, get, create, update and delete objects:

| Object type  | Iterate                  | Get by handle / Gramps ID                                        | Create                    | Update                           | Delete                      |
|--------------|--------------------------|------------------------------------------------------------------|---------------------------|----------------------------------|-----------------------------|
| Citation     | `iter_citations()`       | `get_citation(handle)`, `get_citation_by_gramps_id(gramps_id)`     | `create_citation(data)`   | `update_citation(handle, data)`   | `delete_citation(handle)`   |
| Event        | `iter_events()`          | `get_event(handle)`, `get_event_by_gramps_id(gramps_id)`           | `create_event(data)`      | `update_event(handle, data)`      | `delete_event(handle)`      |
| Family       | `iter_families()`        | `get_family(handle)`, `get_family_by_gramps_id(gramps_id)`         | `create_family(data)`     | `update_family(handle, data)`     | `delete_family(handle)`     |
| Media object | `iter_media()`           | `get_media(handle)`, `get_media_by_gramps_id(gramps_id)`           | –                         | `update_media(handle, data)`      | `delete_media(handle)`      |
| Note         | `iter_notes()`           | `get_note(handle)`, `get_note_by_gramps_id(gramps_id)`             | `create_note(data)`       | `update_note(handle, data)`       | `delete_note(handle)`       |
| Person       | `iter_people()`          | `get_person(handle)`, `get_person_by_gramps_id(gramps_id)`         | `create_person(data)`     | `update_person(handle, data)`     | `delete_person(handle)`     |
| Place        | `iter_places()`          | `get_place(handle)`, `get_place_by_gramps_id(gramps_id)`           | `create_place(data)`      | `update_place(handle, data)`      | `delete_place(handle)`      |
| Repository   | `iter_repositories()`    | `get_repository(handle)`, `get_repository_by_gramps_id(gramps_id)` | `create_repository(data)` | `update_repository(handle, data)` | `delete_repository(handle)` |
| Source       | `iter_sources()`         | `get_source(handle)`, `get_source_by_gramps_id(gramps_id)`         | `create_source(data)`     | `update_source(handle, data)`     | `delete_source(handle)`     |
| Tag          | `iter_tags()`            | `get_tag(handle)`                                                | `create_tag(data)`        | `update_tag(handle, data)`        | `delete_tag(handle)`        |

In addition:

| Method                              | Description                             |
|-------------------------------------|-----------------------------------------|
| `search(query)`                     | Iterate over full-text search results   |
| `get_metadata(include_surnames)`    | Get metadata about the current instance |

Uploading and downloading media files is not supported yet, which is why there is no `create_media`.

### Getting objects

`get_*` methods return a single object as a dictionary and raise a `GrampsWebAPIError` (status 404) if the handle does not exist. `get_*_by_gramps_id` methods return `None` if no object has that Gramps ID.

`iter_*` methods are generators that fetch results page by page. Their keyword arguments are passed as query parameters to the API; see the [API documentation](https://gramps-project.github.io/gramps-web-api/) for all options. Commonly used ones are:

- `gramps_id`: only return the object with this Gramps ID
- `rules`: a filter as a dictionary, using Gramps filter rule names
- `extend`: include referenced objects, e.g. `["event_ref_list", "citation_list"]` or `"all"`
- `profile`: include a human-readable summary, e.g. `"self"` or `"all"`
- `keys` / `skipkeys`: only return or leave out the given keys
- `strip`: leave out empty values

Lists are sent comma-separated, dictionaries as JSON and booleans as `true`/`false`:

```python
for person in api.iter_people(rules={"rules": [{"name": "IsMale"}]}, keys=["handle", "gramps_id"]):
    ...
```

`search(query)` iterates over full-text search results. Each result is a dictionary with the keys `handle`, `object_type` and `object`. It accepts the same kind of keyword arguments, e.g. `type=["person", "note"]` to limit the object types.

### Creating, updating and deleting objects

`data` is always a dictionary following the Gramps Web API JSON conventions.

`update_*` methods fetch the object, merge `data` into it and save the result. **The merge only works on top-level keys:** a key you pass replaces the whole existing value. For example, passing `{"primary_name": {"first_name": "John"}}` replaces the entire primary name, deleting the surname and all other name fields. To change a nested value, fetch the object, modify it and pass the complete top-level value:

```python
person = api.get_person("somehandle")
name = person["primary_name"]
name["first_name"] = "John"
api.update_person("somehandle", {"primary_name": name})
```

`create_*`, `update_*` and `delete_*` methods return the database transaction: a list of changes, each a dictionary with the keys `type` (`"add"`, `"update"` or `"delete"`), `_class`, `handle`, `old` and `new`. To get the handle of a newly created object:

```python
transaction = api.create_note({"text": {"_class": "StyledText", "string": "Hello"}})
handle = transaction[0]["handle"]
```

## Errors

If the server returns an error, a `GrampsWebAPIError` is raised. It is a subclass of `requests.HTTPError`, with the HTTP status in `status_code` and the decoded JSON error body (if any) in `data`:

```python
from gramps_web_api_client import GrampsWebAPIError

try:
    api.get_person("nonexistent")
except GrampsWebAPIError as e:
    print(e.status_code, e.data)
```

## Examples

Removing all citations from an existing person:

```python
api.update_person("somehandle", {"citation_list": []})
```

In this example, all other properties of the person (except citations) will remain the same.

Creating a new place:

```python
api.create_place({
    "name": {
        "_class": "PlaceName",
        "value": "Gotham City"
    }
})
```

Iterating over people and adding a citation if a condition is satisfied:

```python
for person in api.iter_people():
    try:
        surname = person["primary_name"]["surname_list"][0]["surname"]
    except (KeyError, IndexError):
        continue
    if surname == "Garner":
        api.update_person(
            person["handle"],
            {"citation_list": person["citation_list"] + ["some_citation_handle"]}
        )    
```

## Development

This project uses [uv](https://docs.astral.sh/uv/):

```bash
uv sync                    # install the package and dev dependencies
uv run pytest              # run the tests
uv run ruff check src tests
uv run ruff format src tests
uv run mypy
```
