calendry-client 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. calendry_client-0.1.0/.github/workflows/ci.yml +50 -0
  2. calendry_client-0.1.0/.github/workflows/publish.yml +50 -0
  3. calendry_client-0.1.0/.gitignore +222 -0
  4. calendry_client-0.1.0/PKG-INFO +197 -0
  5. calendry_client-0.1.0/README.md +183 -0
  6. calendry_client-0.1.0/pyproject.toml +37 -0
  7. calendry_client-0.1.0/scripts/import_xlsx.py +419 -0
  8. calendry_client-0.1.0/src/calendry_client/__init__.py +180 -0
  9. calendry_client-0.1.0/src/calendry_client/_generic.py +64 -0
  10. calendry_client-0.1.0/src/calendry_client/_version.py +24 -0
  11. calendry_client-0.1.0/src/calendry_client/access_roles.py +56 -0
  12. calendry_client-0.1.0/src/calendry_client/calendar_periods.py +48 -0
  13. calendry_client-0.1.0/src/calendry_client/client.py +113 -0
  14. calendry_client-0.1.0/src/calendry_client/constraints.py +80 -0
  15. calendry_client-0.1.0/src/calendry_client/equipment.py +32 -0
  16. calendry_client-0.1.0/src/calendry_client/exceptions.py +26 -0
  17. calendry_client-0.1.0/src/calendry_client/groups.py +104 -0
  18. calendry_client-0.1.0/src/calendry_client/offerings.py +139 -0
  19. calendry_client-0.1.0/src/calendry_client/persons.py +100 -0
  20. calendry_client-0.1.0/src/calendry_client/roles.py +33 -0
  21. calendry_client-0.1.0/src/calendry_client/rooms.py +77 -0
  22. calendry_client-0.1.0/src/calendry_client/session_kinds.py +49 -0
  23. calendry_client-0.1.0/src/calendry_client/terms.py +49 -0
  24. calendry_client-0.1.0/src/calendry_client/time_grids.py +95 -0
  25. calendry_client-0.1.0/swagger.json +3910 -0
  26. calendry_client-0.1.0/tests/test_client.py +52 -0
@@ -0,0 +1,50 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python-version: ["3.9", "3.11", "3.12"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ with:
17
+ fetch-depth: 0
18
+
19
+ - uses: actions/setup-python@v5
20
+ with:
21
+ python-version: ${{ matrix.python-version }}
22
+
23
+ - name: Install package with dev dependencies
24
+ run: pip install -e ".[xlsx]" pytest
25
+
26
+ - name: Run tests
27
+ run: pytest -q
28
+
29
+ build:
30
+ runs-on: ubuntu-latest
31
+ needs: test
32
+ steps:
33
+ - uses: actions/checkout@v4
34
+ with:
35
+ fetch-depth: 0
36
+
37
+ - uses: actions/setup-python@v5
38
+ with:
39
+ python-version: "3.11"
40
+
41
+ - name: Install build tool
42
+ run: pip install build
43
+
44
+ - name: Build sdist and wheel
45
+ run: python -m build
46
+
47
+ - uses: actions/upload-artifact@v4
48
+ with:
49
+ name: dist
50
+ path: dist/
@@ -0,0 +1,50 @@
1
+ name: Publish package
2
+
3
+ # Builds the pure-function client library into a python package and publishes
4
+ # it to PyPI whenever a version tag (vX.Y.Z) is pushed.
5
+ on:
6
+ push:
7
+ tags:
8
+ - "v*.*.*"
9
+ workflow_dispatch:
10
+
11
+ jobs:
12
+ build:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ with:
17
+ fetch-depth: 0
18
+
19
+ - uses: actions/setup-python@v5
20
+ with:
21
+ python-version: "3.11"
22
+
23
+ - name: Install build tool
24
+ run: pip install build
25
+
26
+ - name: Build sdist and wheel
27
+ run: python -m build
28
+
29
+ - uses: actions/upload-artifact@v4
30
+ with:
31
+ name: dist
32
+ path: dist/
33
+
34
+ publish:
35
+ needs: build
36
+ runs-on: ubuntu-latest
37
+ environment: pypi
38
+ permissions:
39
+ id-token: write # required for PyPI trusted publishing
40
+ steps:
41
+ - uses: actions/download-artifact@v4
42
+ with:
43
+ name: dist
44
+ path: dist/
45
+
46
+ - name: Publish to PyPI
47
+ uses: pypa/gh-action-pypi-publish@release/v1
48
+ # Uses PyPI's Trusted Publisher (OIDC) setup for this repo/environment.
49
+ # If trusted publishing isn't configured, add a PYPI_API_TOKEN secret
50
+ # and pass `with: { password: ${{ secrets.PYPI_API_TOKEN }} }` instead.
@@ -0,0 +1,222 @@
1
+ # hatch-vcs generated version file
2
+ src/calendry_client/_version.py
3
+
4
+ # Byte-compiled / optimized / DLL files
5
+ __pycache__/
6
+ *.py[codz]
7
+ *$py.class
8
+
9
+ # C extensions
10
+ *.so
11
+
12
+ # Distribution / packaging
13
+ .Python
14
+ build/
15
+ develop-eggs/
16
+ dist/
17
+ downloads/
18
+ eggs/
19
+ .eggs/
20
+ lib/
21
+ lib64/
22
+ parts/
23
+ sdist/
24
+ var/
25
+ wheels/
26
+ share/python-wheels/
27
+ *.egg-info/
28
+ .installed.cfg
29
+ *.egg
30
+ MANIFEST
31
+
32
+ # PyInstaller
33
+ # Usually these files are written by a python script from a template
34
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
35
+ *.manifest
36
+ *.spec
37
+
38
+ # Installer logs
39
+ pip-log.txt
40
+ pip-delete-this-directory.txt
41
+
42
+ # Unit test / coverage reports
43
+ htmlcov/
44
+ .tox/
45
+ .nox/
46
+ .coverage
47
+ .coverage.*
48
+ .cache
49
+ nosetests.xml
50
+ coverage.xml
51
+ *.cover
52
+ *.py.cover
53
+ .hypothesis/
54
+ .pytest_cache/
55
+ cover/
56
+
57
+ # Translations
58
+ *.mo
59
+ *.pot
60
+
61
+ # Django stuff:
62
+ *.log
63
+ local_settings.py
64
+ db.sqlite3
65
+ db.sqlite3-journal
66
+
67
+ # Flask stuff:
68
+ instance/
69
+ .webassets-cache
70
+
71
+ # Scrapy stuff:
72
+ .scrapy
73
+
74
+ # Sphinx documentation
75
+ docs/_build/
76
+
77
+ # PyBuilder
78
+ .pybuilder/
79
+ target/
80
+
81
+ # Jupyter Notebook
82
+ .ipynb_checkpoints
83
+
84
+ # IPython
85
+ profile_default/
86
+ ipython_config.py
87
+
88
+ # pyenv
89
+ # For a library or package, you might want to ignore these files since the code is
90
+ # intended to run in multiple environments; otherwise, check them in:
91
+ # .python-version
92
+
93
+ # pipenv
94
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
95
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
96
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
97
+ # install all needed dependencies.
98
+ # Pipfile.lock
99
+
100
+ # UV
101
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
102
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
103
+ # commonly ignored for libraries.
104
+ # uv.lock
105
+
106
+ # poetry
107
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
108
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
109
+ # commonly ignored for libraries.
110
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
111
+ # poetry.lock
112
+ # poetry.toml
113
+
114
+ # pdm
115
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
116
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
117
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
118
+ # pdm.lock
119
+ # pdm.toml
120
+ .pdm-python
121
+ .pdm-build/
122
+
123
+ # pixi
124
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
125
+ # pixi.lock
126
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
127
+ # in the .venv directory. It is recommended not to include this directory in version control.
128
+ .pixi
129
+
130
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
131
+ __pypackages__/
132
+
133
+ # Celery stuff
134
+ celerybeat-schedule
135
+ celerybeat.pid
136
+
137
+ # Redis
138
+ *.rdb
139
+ *.aof
140
+ *.pid
141
+
142
+ # RabbitMQ
143
+ mnesia/
144
+ rabbitmq/
145
+ rabbitmq-data/
146
+
147
+ # ActiveMQ
148
+ activemq-data/
149
+
150
+ # SageMath parsed files
151
+ *.sage.py
152
+
153
+ # Environments
154
+ .env
155
+ .envrc
156
+ .venv
157
+ env/
158
+ venv/
159
+ ENV/
160
+ env.bak/
161
+ venv.bak/
162
+
163
+ # Spyder project settings
164
+ .spyderproject
165
+ .spyproject
166
+
167
+ # Rope project settings
168
+ .ropeproject
169
+
170
+ # mkdocs documentation
171
+ /site
172
+
173
+ # mypy
174
+ .mypy_cache/
175
+ .dmypy.json
176
+ dmypy.json
177
+
178
+ # Pyre type checker
179
+ .pyre/
180
+
181
+ # pytype static type analyzer
182
+ .pytype/
183
+
184
+ # Cython debug symbols
185
+ cython_debug/
186
+
187
+ # PyCharm
188
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
189
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
190
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
191
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
192
+ # .idea/
193
+
194
+ # Abstra
195
+ # Abstra is an AI-powered process automation framework.
196
+ # Ignore directories containing user credentials, local state, and settings.
197
+ # Learn more at https://abstra.io/docs
198
+ .abstra/
199
+
200
+ # Visual Studio Code
201
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
202
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
203
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
204
+ # you could uncomment the following to ignore the entire vscode folder
205
+ # .vscode/
206
+ # Temporary file for partial code execution
207
+ tempCodeRunnerFile.py
208
+
209
+ # Ruff stuff:
210
+ .ruff_cache/
211
+
212
+ # PyPI configuration file
213
+ .pypirc
214
+
215
+ # Marimo
216
+ marimo/_static/
217
+ marimo/_lsp/
218
+ __marimo__/
219
+
220
+ # Streamlit
221
+ .streamlit/secrets.toml
222
+ *.xlsx
@@ -0,0 +1,197 @@
1
+ Metadata-Version: 2.5
2
+ Name: calendry-client
3
+ Version: 0.1.0
4
+ Summary: Python client library for the Calendry scheduling API
5
+ Project-URL: Homepage, https://github.com/Calendry-de/Calendry-Importlib
6
+ Project-URL: Repository, https://github.com/Calendry-de/Calendry-Importlib
7
+ Author: Calendry
8
+ License: MIT
9
+ Requires-Python: >=3.9
10
+ Requires-Dist: requests>=2.28
11
+ Provides-Extra: xlsx
12
+ Requires-Dist: openpyxl>=3.1; extra == 'xlsx'
13
+ Description-Content-Type: text/markdown
14
+
15
+ # calendry-client
16
+
17
+ [![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)
18
+ [![PyPI](https://img.shields.io/pypi/v/calendry-client.svg)](https://pypi.org/project/calendry-client/)
19
+ [![Python versions](https://img.shields.io/pypi/pyversions/calendry-client.svg)](https://pypi.org/project/calendry-client/)
20
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](#license)
21
+
22
+ 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.
23
+
24
+ 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.
25
+
26
+ ## Contents
27
+
28
+ - [Features](#features)
29
+ - [Installation](#installation)
30
+ - [Quick start](#quick-start)
31
+ - [Configuring the server URL and token](#configuring-the-server-url-and-token)
32
+ - [API coverage](#api-coverage)
33
+ - [Working with relations](#working-with-relations)
34
+ - [Error handling](#error-handling)
35
+ - [Importing an xlsx planning workbook](#importing-an-xlsx-planning-workbook)
36
+ - [Development](#development)
37
+ - [Publishing](#publishing)
38
+ - [License](#license)
39
+
40
+ ## Features
41
+
42
+ - One small `CalendryClient` HTTP wrapper — no ORM, no hidden state, no generated boilerplate.
43
+ - A plain function per operation, taking the client as its first argument and returning the parsed JSON response.
44
+ - Optional keyword arguments left as `None` are simply omitted from the request body (never sent as JSON `null`).
45
+ - 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`.
46
+ - Server URL and auth token are runtime configuration everywhere (constructor args, `CalendryClient.from_env()`, or CLI flags on the import script) — never hard-coded.
47
+ - 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.
48
+
49
+ ## Installation
50
+
51
+ ```bash
52
+ pip install calendry-client
53
+
54
+ # with openpyxl, needed only for scripts/import_xlsx.py:
55
+ pip install "calendry-client[xlsx]"
56
+ ```
57
+
58
+ Requires Python 3.9+. The only runtime dependency is [`requests`](https://pypi.org/project/requests/).
59
+
60
+ ## Quick start
61
+
62
+ ```python
63
+ from calendry_client import CalendryClient, add_person, add_offering, set_offering_lecturers
64
+
65
+ client = CalendryClient(base_url="https://calendry.example.com", token="...")
66
+
67
+ person = add_person(client, "Ada", "Lovelace", email="ada@example.com")
68
+ offering = add_offering(client, term_id="term-1", kind_id="kind-1", title="Intro to CS")
69
+ set_offering_lecturers(client, offering["id"], [{"person_id": person["id"]}])
70
+ ```
71
+
72
+ 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.
73
+
74
+ ## Configuring the server URL and token
75
+
76
+ Both values can be passed explicitly:
77
+
78
+ ```python
79
+ client = CalendryClient(base_url="https://calendry.example.com", token="my-token")
80
+ ```
81
+
82
+ or read from the environment (`CALENDRY_SERVER_URL` / `CALENDRY_API_TOKEN` by default, both overridable):
83
+
84
+ ```python
85
+ client = CalendryClient.from_env()
86
+ client = CalendryClient.from_env(url_var="MY_URL_VAR", token_var="MY_TOKEN_VAR")
87
+ ```
88
+
89
+ `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>`.
90
+
91
+ `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).
92
+
93
+ ## API coverage
94
+
95
+ 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.
96
+
97
+ | Module (`calendry_client.*`) | Resource | Create | Relations |
98
+ |---|---|---|---|
99
+ | `persons` | `persons` | `add_person` (alias: `add_lecturer`) | `roles`, `groups`, `access-roles` |
100
+ | `roles` | `roles` | `add_role` | — |
101
+ | `groups` | `groups` | `add_group`, `add_subgroup` (sets `parent_group_id`) | `terms`, `sources`, `availability` |
102
+ | `rooms` | `rooms` | `add_room` | `equipment` |
103
+ | `equipment` | `equipment` | `add_equipment` | — |
104
+ | `offerings` | `offerings` | `add_offering` | `groups`, `lecturers`, `equipment` |
105
+ | `terms` | `terms` | `add_term` | — |
106
+ | `time_grids` | `time-grids` | `add_time_grid` | `breaks` |
107
+ | `session_kinds` | `session-kinds` | `add_session_kind` | — |
108
+ | `calendar_periods` | `calendar-periods` | `add_calendar_period` | — |
109
+ | `constraints` | `constraints` | `add_constraint` | `scopes` |
110
+ | `access_roles` | `access-roles` | `add_access_role` | — |
111
+
112
+ 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.
113
+
114
+ For anything not covered by a named convenience function (an unusual filter, a resource-specific field), the generic building blocks are always available:
115
+
116
+ ```python
117
+ from calendry_client import CalendryClient
118
+ from calendry_client._generic import list_rows, create_row, update_row, delete_row, get_relation, set_relation
119
+
120
+ list_rows(client, "offerings", term_id="term-1")
121
+ create_row(client, "offerings", {"termId": "term-1", "kindId": "kind-1", "title": "Ad-hoc offering"})
122
+ ```
123
+
124
+ ## Working with relations
125
+
126
+ Relation setters replace the **entire** membership set in one call — there's no per-row add/remove, matching the API's `PUT` semantics:
127
+
128
+ ```python
129
+ from calendry_client import add_group, add_subgroup, set_offering_groups, set_person_roles
130
+
131
+ cohort = add_group(client, "dWI24-A")
132
+ section = add_subgroup(client, cohort["id"], "dWI24-A1", expected_size=28)
133
+
134
+ set_offering_groups(client, offering["id"], [section["id"]])
135
+ set_person_roles(client, person["id"], [lecturer_role["id"]])
136
+ ```
137
+
138
+ ## Error handling
139
+
140
+ Any non-2xx response raises `CalendryAPIError`, carrying the HTTP status code and the parsed error payload (when the response was JSON):
141
+
142
+ ```python
143
+ from calendry_client import CalendryAPIError, add_room
144
+
145
+ try:
146
+ add_room(client, code="R-101", name="Room 101")
147
+ except CalendryAPIError as exc:
148
+ print(exc.status_code, exc.payload)
149
+ ```
150
+
151
+ ## Importing an xlsx planning workbook
152
+
153
+ `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.
154
+
155
+ ```bash
156
+ python scripts/import_xlsx.py \
157
+ --server-url https://calendry.example.com \
158
+ --token "$CALENDRY_API_TOKEN" \
159
+ --input Test_anonymized.xlsx \
160
+ --dry-run # preview only; drop this flag to actually write
161
+ ```
162
+
163
+ | Flag | Required | Default | Purpose |
164
+ |---|---|---|---|
165
+ | `--input PATH` | yes | — | Workbook to import |
166
+ | `--server-url URL` | yes* | `$CALENDRY_SERVER_URL` | Calendry server base URL |
167
+ | `--token TOKEN` | no | `$CALENDRY_API_TOKEN` | Bearer token; omitted requests are sent unauthenticated with a warning |
168
+ | `--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 |
169
+ | `--default-frequency` | no | `1` | Sessions/week for created offerings (not present in the sheet) |
170
+ | `--default-duration-blocks` | no | `1` | Duration in grid blocks for created offerings (not present in the sheet) |
171
+ | `--dry-run` | no | off | Print the plan without calling the API |
172
+
173
+ \* required unless set via the environment variable.
174
+
175
+ What gets imported, from which columns:
176
+
177
+ - **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).
178
+ - **People** — the `Name` (lecturer) column, deduplicated by name across all rows.
179
+ - **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`).
180
+ - **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`.
181
+
182
+ 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.
183
+
184
+ ## Development
185
+
186
+ ```bash
187
+ pip install -e ".[xlsx]" pytest
188
+ pytest
189
+ ```
190
+
191
+ ## Publishing
192
+
193
+ 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.
194
+
195
+ ## License
196
+
197
+ MIT.