Metadata-Version: 2.5
Name: calendry-client
Version: 0.1.0
Summary: Python client library for the Calendry scheduling API
Project-URL: Homepage, https://github.com/Calendry-de/Calendry-Importlib
Project-URL: Repository, https://github.com/Calendry-de/Calendry-Importlib
Author: Calendry
License: MIT
Requires-Python: >=3.9
Requires-Dist: requests>=2.28
Provides-Extra: xlsx
Requires-Dist: openpyxl>=3.1; extra == 'xlsx'
Description-Content-Type: text/markdown

# calendry-client

[![CI](https://github.com/Calendry-de/Calendry-Importlib/actions/workflows/ci.yml/badge.svg)](https://github.com/Calendry-de/Calendry-Importlib/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/calendry-client.svg)](https://pypi.org/project/calendry-client/)
[![Python versions](https://img.shields.io/pypi/pyversions/calendry-client.svg)](https://pypi.org/project/calendry-client/)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](#license)

Python client library for the [Calendry](https://github.com/Calendry-de) scheduling API. It wraps every core resource (persons, groups, rooms, offerings, equipment, roles, terms, time grids, session kinds, calendar periods, constraints, access roles) as a small set of **pure functions** — `add_person`, `add_offering`, `set_offering_lecturers`, and so on — plus a ready-to-run script that imports a full Offerings/Rooms/People/Groups planning workbook.

The full HTTP surface is documented in [`swagger.json`](swagger.json); the library's functions build the request bodies described there. There is no code generation step — the swagger file is the reference the modules were written against, not something they're built from.

## Contents

- [Features](#features)
- [Installation](#installation)
- [Quick start](#quick-start)
- [Configuring the server URL and token](#configuring-the-server-url-and-token)
- [API coverage](#api-coverage)
- [Working with relations](#working-with-relations)
- [Error handling](#error-handling)
- [Importing an xlsx planning workbook](#importing-an-xlsx-planning-workbook)
- [Development](#development)
- [Publishing](#publishing)
- [License](#license)

## Features

- One small `CalendryClient` HTTP wrapper — no ORM, no hidden state, no generated boilerplate.
- A plain function per operation, taking the client as its first argument and returning the parsed JSON response.
- Optional keyword arguments left as `None` are simply omitted from the request body (never sent as JSON `null`).
- Relation endpoints (`offerings/lecturers`, `groups/terms`, `rooms/equipment`, ...) get typed `get_*`/`set_*` helpers; `set_*` always **replaces** the whole membership set, mirroring the API's `PUT`.
- Server URL and auth token are runtime configuration everywhere (constructor args, `CalendryClient.from_env()`, or CLI flags on the import script) — never hard-coded.
- Includes a reference import script (`scripts/import_xlsx.py`) demonstrating the library against a real Offerings/Rooms/People/Groups workbook, with a `--dry-run` mode. It's a plain script, not part of the distributed `calendry-client` package — run it from a checkout of this repo.

## Installation

```bash
pip install calendry-client

# with openpyxl, needed only for scripts/import_xlsx.py:
pip install "calendry-client[xlsx]"
```

Requires Python 3.9+. The only runtime dependency is [`requests`](https://pypi.org/project/requests/).

## Quick start

```python
from calendry_client import CalendryClient, add_person, add_offering, set_offering_lecturers

client = CalendryClient(base_url="https://calendry.example.com", token="...")

person = add_person(client, "Ada", "Lovelace", email="ada@example.com")
offering = add_offering(client, term_id="term-1", kind_id="kind-1", title="Intro to CS")
set_offering_lecturers(client, offering["id"], [{"person_id": person["id"]}])
```

Every function returns the raw dict (or list of dicts) the API responds with — e.g. `person["id"]` is the newly created row's id.

## Configuring the server URL and token

Both values can be passed explicitly:

```python
client = CalendryClient(base_url="https://calendry.example.com", token="my-token")
```

or read from the environment (`CALENDRY_SERVER_URL` / `CALENDRY_API_TOKEN` by default, both overridable):

```python
client = CalendryClient.from_env()
client = CalendryClient.from_env(url_var="MY_URL_VAR", token_var="MY_TOKEN_VAR")
```

`token=None` sends unauthenticated requests, which is only useful against a server/route that doesn't require one. The token is sent as `Authorization: Bearer <token>`.

`scripts/import_xlsx.py` exposes the same configuration as `--server-url`/`--token` CLI flags, also falling back to the environment variables above — see [Importing an xlsx planning workbook](#importing-an-xlsx-planning-workbook).

## API coverage

Every resource module lives directly under `calendry_client` and follows the same shape: `add_x` (create), `get_x`, `list_xs`, `update_x`, `delete_x`, plus relation helpers where the API exposes a `/…/{id}/{relation}` route.

| Module (`calendry_client.*`) | Resource | Create | Relations |
|---|---|---|---|
| `persons` | `persons` | `add_person` (alias: `add_lecturer`) | `roles`, `groups`, `access-roles` |
| `roles` | `roles` | `add_role` | — |
| `groups` | `groups` | `add_group`, `add_subgroup` (sets `parent_group_id`) | `terms`, `sources`, `availability` |
| `rooms` | `rooms` | `add_room` | `equipment` |
| `equipment` | `equipment` | `add_equipment` | — |
| `offerings` | `offerings` | `add_offering` | `groups`, `lecturers`, `equipment` |
| `terms` | `terms` | `add_term` | — |
| `time_grids` | `time-grids` | `add_time_grid` | `breaks` |
| `session_kinds` | `session-kinds` | `add_session_kind` | — |
| `calendar_periods` | `calendar-periods` | `add_calendar_period` | — |
| `constraints` | `constraints` | `add_constraint` | `scopes` |
| `access_roles` | `access-roles` | `add_access_role` | — |

Every one of these is also re-exported from the top-level `calendry_client` package, so `from calendry_client import add_room, set_room_equipment` works without knowing which module it lives in.

For anything not covered by a named convenience function (an unusual filter, a resource-specific field), the generic building blocks are always available:

```python
from calendry_client import CalendryClient
from calendry_client._generic import list_rows, create_row, update_row, delete_row, get_relation, set_relation

list_rows(client, "offerings", term_id="term-1")
create_row(client, "offerings", {"termId": "term-1", "kindId": "kind-1", "title": "Ad-hoc offering"})
```

## Working with relations

Relation setters replace the **entire** membership set in one call — there's no per-row add/remove, matching the API's `PUT` semantics:

```python
from calendry_client import add_group, add_subgroup, set_offering_groups, set_person_roles

cohort = add_group(client, "dWI24-A")
section = add_subgroup(client, cohort["id"], "dWI24-A1", expected_size=28)

set_offering_groups(client, offering["id"], [section["id"]])
set_person_roles(client, person["id"], [lecturer_role["id"]])
```

## Error handling

Any non-2xx response raises `CalendryAPIError`, carrying the HTTP status code and the parsed error payload (when the response was JSON):

```python
from calendry_client import CalendryAPIError, add_room

try:
    add_room(client, code="R-101", name="Room 101")
except CalendryAPIError as exc:
    print(exc.status_code, exc.payload)
```

## Importing an xlsx planning workbook

`scripts/import_xlsx.py` is a **reference script**, not an installable package or a `calendry-client` entry point — it's kept in this repo purely to demonstrate the library end-to-end and isn't published to PyPI. It reads a Calendry planning workbook — Offerings, Rooms, People, Groups and sub-groups, in the shape of [`Test_anonymized.xlsx`](Test_anonymized.xlsx) — and creates every row through the library above. It locates sheets by header row (not by name), so re-ordering or renaming sheets is fine as long as the columns are there.

```bash
python scripts/import_xlsx.py \
    --server-url https://calendry.example.com \
    --token "$CALENDRY_API_TOKEN" \
    --input Test_anonymized.xlsx \
    --dry-run   # preview only; drop this flag to actually write
```

| Flag | Required | Default | Purpose |
|---|---|---|---|
| `--input PATH` | yes | — | Workbook to import |
| `--server-url URL` | yes* | `$CALENDRY_SERVER_URL` | Calendry server base URL |
| `--token TOKEN` | no | `$CALENDRY_API_TOKEN` | Bearer token; omitted requests are sent unauthenticated with a warning |
| `--term-start-date` / `--term-end-date` | no | derived from the weeks sheet | Override the term's dates; only valid when the sheet has a single `Semester` value |
| `--default-frequency` | no | `1` | Sessions/week for created offerings (not present in the sheet) |
| `--default-duration-blocks` | no | `1` | Duration in grid blocks for created offerings (not present in the sheet) |
| `--dry-run` | no | off | Print the plan without calling the API |

\* required unless set via the environment variable.

What gets imported, from which columns:

- **Rooms** — `Name Raum`, `Anzahl Personen` (capacity), `Prio` (ranking), `Anzeigen Grid` (`isActive`). A room named `Online` is marked `isVirtual`. An `Ausstattung` value creates/links an `equipment` row via `rooms/equipment`. The `Buchung verursacht Raumkonflikte` column has no equivalent Calendry resource and is intentionally skipped (logged, not imported).
- **People** — the `Name` (lecturer) column, deduplicated by name across all rows.
- **Groups & sub-groups** — the `Planungsgruppe` column; codes sharing a common non-numeric prefix (e.g. `dWI24-A1`..`dWI24-A17`) become sub-groups of a shared parent group (`dWI24-A`).
- **Offerings** — one per (course, group) row, titled `"<Veranstaltung Abk> - <Planungsgruppe>"`, linked to its group and lecturer. `Ist_vorlesung` selects (and creates on first use) a `Vorlesung`/`Übung` session kind; `Pax` becomes `requiredCapacity`; `Online` becomes `allowOnline`. The term is looked up/created from the `Semester` column, with start/end dates derived from the weeks sheet (matching `Wochennummer` values against the `Semester` string) unless overridden with `--term-start-date`/`--term-end-date`.

The script is idempotent: it lists existing rows before creating anything and reuses matches (by name/code/key, as appropriate), so re-running it against the same server does not create duplicates.

## Development

```bash
pip install -e ".[xlsx]" pytest
pytest
```

## Publishing

Pushing a `vX.Y.Z` tag runs [`.github/workflows/publish.yml`](.github/workflows/publish.yml), which builds the sdist/wheel and publishes them to PyPI via [Trusted Publishing](https://docs.pypi.org/trusted-publishers/) (configure a `pypi` GitHub environment as the trusted publisher for this repository, or swap in a `PYPI_API_TOKEN` secret and pass it to the publish step). [`.github/workflows/ci.yml`](.github/workflows/ci.yml) runs the test suite and a build check on every push and pull request. The package version is derived from git tags (via `hatch-vcs`) — there is no version number to bump by hand.

## License

MIT.
