Repository: anomaly/gallagher (language: python)

Issue:
Provide a `textual` based terminal user interface for the command centre
A completely unnecessary but cool request but following from #13 working so nicely and @textualize offering cool libraries like [trogon](https://github.com/Textualize/trogon) and [textual](https://github.com/Textualize/textual) I thought would't it be nice to provide an additional terminal based interface which isn't exactly a CLI but not a traditional GUI.

Moreover this will extend out Python skills on the CLI and allow us to value add the library.

The *why* of this request really is around bringing richer CLI tools for the Gallagher ecosystem.

Note that this request and the CLI should be additional poetry installables. The API client should be a product of its own.
Handle feature not licensed errors
Gallagher API licenses features by the piece, if a feature isn't available the server responds with a `403` with the following `json`:
```
HTTP/1.1 403 Forbidden
Cache-Control: no-cache
Content-Length: 34
Content-Type: application/json; charset=utf-8
Date: Sat, 10 Jun 2023 05:08:12 GMT

{
    "message": "Feature not licensed"
}
```
the API client should have the ability to handle these responses and throw an exception
Restructure packages to avoid circular dependencies
This is a major refactor of the file structure to ensure the library scales
- #21 
Handle feature not licensed errors
Gallagher API licenses features by the piece, if a feature isn't available the server responds with a `403` with the following `json`:
```
HTTP/1.1 403 Forbidden
Cache-Control: no-cache
Content-Length: 34
Content-Type: application/json; charset=utf-8
Date: Sat, 10 Jun 2023 05:08:12 GMT

{
    "message": "Feature not licensed"
}
```
the API client should have the ability to handle these responses and throw an exception
Consider moving to `asyncio` before the library gets too large
We're getting to a stage where the library design and patterns are maturing. Tests are running consistently and passing as expected. 

It would thus be time to full flesh out the entire library before we move into some enterprise features and use this in integrations and products.

Most of Anomaly's projects have moved to using `asyncio` and it would thus be unwise to not make this library have `asyncio` support. We would have to consider:

- [Using httpx in async mode](https://www.python-httpx.org/async/) 
- Running tests using async io
Separate interfaces and API client to be separate installables
Since the initial project was started I've expanded the scope to include a #13 and now #25 `terminal` based interface to interact with the Gallagher ecosystem.

While this is super cool we should pollute the install base with unnecessary items if the all the user wants is the API client.

We propose to split the installables into three (or as many as required into the future):
- `gallagher`, installs the API client only
- `gallagher[cli]`, installs the CLI which will require the API Client
- `gallagher[gui]`, installs the textual based interfaces
- `gallagher[all]` installs everything we offer

> Note this requires a refactor of the `pyproject.toml` file


Example from @taskiq-python projects can be [found here](https://github.com/taskiq-python/taskiq-faststream/blob/develop/pyproject.toml)
Provide a `textual` based terminal user interface for the command centre
A completely unnecessary but cool request but following from #13 working so nicely and @textualize offering cool libraries like [trogon](https://github.com/Textualize/trogon) and [textual](https://github.com/Textualize/textual) I thought would't it be nice to provide an additional terminal based interface which isn't exactly a CLI but not a traditional GUI.

Moreover this will extend out Python skills on the CLI and allow us to value add the library.

The *why* of this request really is around bringing richer CLI tools for the Gallagher ecosystem.

Note that this request and the CLI should be additional poetry installables. The API client should be a product of its own.
Design a mechanism to parse Personal Data Fields (PDF) from API responses for Cardholders using `pydantic`
Gallagher's REST APIs return configurable fields (per instance of the server) called Personal Data Fields (PDFs). These are additional fields that are relevant to the organisation and are not common. These key of the fields are always prefixed with an `@` symbol e.g `@Student ID` in an example from their docs:
```json
{
  "href": "https://localhost:8904/api/cardholders/325",
  "id": "325",
  "firstName": "Algernon",
  "lastName": "Boothroyd",
  "shortName": "Q",
  "description": "Quartermaster",
  "authorised": true,
  "lastSuccessfulAccessTime": "2004-11-18T19:21:52Z",
  "lastSuccessfulAccessZone": {
    "href": "https://localhost:8904/api/access_zones/333",
    "name": "Twilight zone"
  },
  "serverDisplayName": "ruatoria.satellite.int",
  "division": {
    "href": "https://localhost:8904/api/divisions/2"
  },
  "@Student ID": "8904640"
}
```

We are using `pyndatic` schemas to parse the API response. The aim would be to use event based methods to dynamically parse these and make them available as a dictionary:
```python
cardholder.pdfs["StudentId"]
```

for the API user to access these fields in a Pythonic manner.

Note that the response has a field called `personalDataDefinitions` which contains references to each one of the PDF definitions.


Discussion / hints:

Fix patch:
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 00000000..dd84ea78
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,38 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Desktop (please complete the following information):**
+ - OS: [e.g. iOS]
+ - Browser [e.g. chrome, safari]
+ - Version [e.g. 22]
+
+**Smartphone (please complete the following information):**
+ - Device: [e.g. iPhone6]
+ - OS: [e.g. iOS8.1]
+ - Browser [e.g. stock browser, safari]
+ - Version [e.g. 22]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 00000000..bbcbbe7d
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/README.md b/README.md
index 75ab96f7..acb14499 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,6 @@
-> Currently under heavy development, please check for stable release before use
+# Gallagher Python Toolkit
 
-# Gallagher Command Centre REST API Client
-
-> Python idiomatic client for Gallagher Command Centre API
+> Python idiomatic REST API client, a command line interface and a text based console for Gallagher Command Centre API
 
 Gallagher Security manufacture a variety of [security products](https://security.gallagher.com) all of which are controlled by their [Command Centre](https://products.security.gallagher.com/security/au/en_AU/products/software/command-centre/p/C201311) software. Traditionally Command Centre has been a Windows based server product. Version `8.6` introduced a REST API which allows you to interact with the system via HTTP requests. Gallagher also provide a [Cloud API Gateway](https://gallaghersecurity.github.io/docs/Command%20Centre%20Cloud%20Api%20Gateway%20TIP.pdf) which allows third party integrations to securely communicate with the Command Centre on site.
 
@@ -19,8 +17,7 @@ from gallagher import cc, const
 
 cc.api_key = "GH_"
 
-cc.discover()
-cc.Customer.create()
+cc.Customer.list()
 ```
 
 > Note this project is **NOT** officially affiliated with Gallagher Security
@@ -131,7 +128,7 @@ We use [Taskfile](https://taskfile.dev) to automate running tasks.
 
 The project provides a comprehensive set of tests which can be run with `task test`. These tests do create objects in the Command Centre, we advice you to obtain a test license.
 
-**DO NOT** run the tests against a production system.
+> It's **not recommended** to run tests against a production system.
 
 ### Data Transfer Objects
 
@@ -155,7 +152,7 @@ Resources are `fetchable`, `queryable`, `creatable`, `updatable` and `deletable`
 
 Responses can be the object itself or a response layout
 
-# Configuring the Command Centre
+## Configuring the Command Centre
 
 The following requires you to have an understanding of the Gallagher Command Centre and how to configure it. If you are unsure, please contact your Gallagher representative.
 
@@ -183,6 +180,6 @@ To check your API key:
 
 ![Command Centre Cloud Connections](assets/gallagher-rest-properties.png)
 
-# License
+## License
 
 Distributed under the MIT License.
diff --git a/TODO.md b/TODO.md
index 965b368a..6bb851c5 100644
--- a/TODO.md
+++ b/TODO.md
@@ -2,38 +2,37 @@
 
 The following is tracking which endpoints are available along with their test cases
 
-- [ ] Authentication
-- [ ] Base API route design
+- [x] Authentication
+- [x] Base API route design
 
 ## Alarms, events, and non-cardholder items
 
 - [ ] Alarm search
 - [ ] Alarm updates
-- [ ] Alarm summary
+- [x] Alarm summary
 - [ ] Alarm detail
 - [ ] Alarm history entry
 - [ ] Alarm update request
 - [ ] Event search
-- [ ] Event summary
+- [x] Event summary
 - [ ] Event detail
 - [ ] Event POST body
-- [ ] Event groups
-- [ ] Divisions
-- [ ] Division
+- [x] Event groups
+- [x] Divisions
+- [x] Division
 - [ ] Division PATCH and POST example
 - [ ] Item search
-- [X] Item summary
-- [X] Item detail
-- [X] Item types
+- [x] Item summary
+- [x] Item detail
+- [x] Item types
 - [ ] Item update
 - [ ] Item update subscription
 
-
 ## Cardholders and related items
 
 - [ ] Cardholder search
-- [ ] Cardholder summary
-- [ ] Cardholder detail
+- [x] Cardholder summary
+- [x] Cardholder detail
 - [ ] Cardholder POST example
 - [ ] Cardholder Update Location POST example
 - [ ] Cardholder PATCH example
@@ -64,13 +63,13 @@ The following is tracking which endpoints are available along with their test ca
 - [ ] Competency summary
 - [ ] Competency detail
 - [ ] Card type search
-- [ ] Card type
+- [x] Card type
 - [ ] Operator group search
 - [ ] Operator group summary
 - [ ] Operator group detail
 - [ ] Operator group membership
 - [ ] PDF definition search
-- [ ] PDF definition
+- [x] PDF definition
 - [ ] Reception search
 - [ ] Reception
 - [ ] Redaction
@@ -94,7 +93,7 @@ The following is tracking which endpoints are available along with their test ca
 - [ ] Alarm Zone summary
 - [ ] Alarm Zone detail
 - [ ] Day category search
-- [X] Day category
+- [x] Day category
 - [ ] Door search
 - [ ] Door summary
 - [ ] Door detail
@@ -117,12 +116,11 @@ The following is tracking which endpoints are available along with their test ca
 - [ ] Output summary
 - [ ] Output detail
 - [ ] Schedule search
-- [X] Schedule summary
+- [x] Schedule summary
 - [ ] Schedule detail
 - [ ] Schedule POST and PATCH
 - [ ] Override end time
 
-
 ## PIV cards
 
 - [ ] PIV card GET
@@ -130,4 +128,4 @@ The following is tracking which endpoints are available along with their test ca
 - [ ] PIV card create example
 - [ ] PIV card update example
 - [ ] PIV card data
-- [ ] CHUID
\ No newline at end of file
+- [ ] CHUID
diff --git a/Taskfile.yml b/Taskfile.yml
index a9d3ed20..d6141f68 100644
--- a/Taskfile.yml
+++ b/Taskfile.yml
@@ -9,10 +9,28 @@ tasks:
     cmds:
       - poetry build
   test:
-    desc: runs tests inside the server container
+    desc: runs tests inside the virtualenv
+    summary:
+      runs all the tests inside the virtualenv, optionally
+      provide the name of the test as an argument to run a single test
     cmds:
-      - poetry run coverage run -m pytest -s
+      - poetry run coverage run -m pytest -s --tap tests/{{.CLI_ARGS}}
+  test:list:
+    desc: lists the available tests
+    summary:
+      runs collect only on pytest to list the tests available
+    cmds:
+      - poetry run pytest --co
   test:coverreport:
     desc: runs coverage inside the server container
     cmds:
       - poetry run coverage report -m
+  dev:textual:
+    desc: runs the textual cli
+    cmds:
+      - poetry run textual --
+  dev:gcon:
+    desc: runs text gallagher console in dev mode
+    cmds:
+      - poetry run textual run --dev gallagher.console:main
+
diff --git a/docs/docs/cli.md b/docs/docs/cli.md
new file mode 100644
index 00000000..c74a96d7
--- /dev/null
+++ b/docs/docs/cli.md
@@ -0,0 +1,47 @@
+# Command Line Interface
+
+We provide a command line interface to interact with the Gallagher Command Centre. It uses the API client to communicate with the server, which doubly serves as a reference example of how to use the API client.
+
+> We use [typer](https://typer.tiangolo.com) to construct the CLI, which in turn uses [click](https://click.palletsprojects.com). We also use [rich](https://rich.readthedocs.io/en/stable/) to make the output nicer. The CLI is decoupled from the API client, and is not install by default.
+
+We follow a `git` like `command`, `sub-command` pattern, so it should feel quite familiar.
+
+poetry will install the alias `gal` for you to interact with the CLI. You can ask for help with:
+
+```
+gal --help
+```
+
+which will list the available commands:
+
+```
+ Usage: gal [OPTIONS] COMMAND [ARGS]...
+
+╭─ Options ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
+│ --install-completion          Install completion for the current shell.                                                        │
+│ --show-completion             Show completion for the current shell, to copy it or customize the installation.                 │
+│ --help                        Show this message and exit.                                                                      │
+╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
+╭─ Commands ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
+│ alarms             list or query alarms in the command centre                                                                  │
+│ ch                 query or manage cardholders                                                                                 │
+│ events             query command centre events                                                                                 │
+╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
+```
+
+A simple example to get a list of cardholders looks like:
+
+```
+(gallagher-py3.11) ➜  gallagher git:(dto-implementation) ✗ gal ch list
+                    Cardholders
+┏━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
+┃ Id   ┃ First name  ┃ Last name     ┃ Authorised ┃
+┡━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
+│ 8246 │ Johnetta    │ Abdallah      │ yes        │
+│ 7936 │ Socorro     │ Abrahams      │ yes        │
+│ 8374 │ Geoffrey    │ Acey          │ yes        │
+│ 8370 │ Weldon      │ Acuff         │ yes        │
+│ 7922 │ Rusty       │ Adelsperger   │ yes        │
+```
+
+you can also ask each sub command to give you in
diff --git a/docs/docs/design.md b/docs/docs/design.md
new file mode 100644
index 00000000..90509957
--- /dev/null
+++ b/docs/docs/design.md
@@ -0,0 +1,58 @@
+# Design
+
+A central feature to this client is it's detailed design, focused on a superior developer experience and performance. We also ensure that we follow design patterns outlined by Gallagher
+
+## Data Transfer Objects
+
+This a central part of our design. There are three types of schema definitions, each one of them suffixed with their intent:
+
+- **Ref** are `References` to other objects, they using contain a `href` and possibly additional meta data such as a `name` or `id`
+- **Summary** is what is returned by the Gallagher API in operations such as [searches](https://gallaghersecurity.github.io/cc-rest-docs/ref/cardholders.html), these are generally a subset of the full object
+- **Detail** are the full object found at a particular `href`, they compound on the `Summary` schema and add additional attributes
+
+I additional we have classes that defined responses which are suffixed with **Response**, these wrap structures which returns `hrefs` for `next` and `previous` responses and usually have a collection to hold the response.
+
+Ensure that each Endpoint defines their own DTOs so you can test them for authenticity. Avoid writing generic classes.
+
+While `Refs`, `Summary` and `Detail` responses have fields, and it would make sense from an efficiency point of view to inherit e.g `Summary` builds on `Ref`, this should be avoided so logically an instance of a `Ref` class doesn't assert true for `isinstance` of a `Summary` class.
+
+## API Client Core
+
+The `core` package in `cc` provides two important classes:
+
+- `APIEndpoint` which all endpoint consumers configuration must inherit from
+- `EndpointConfig` an instance of which each class must return as a result of the `get_config` method
+
+Every Endpoint Consumer Class is expected to return an instance of `EndpointConfig` from the `get_config` method. Each configuration provides references to paths that are dynamically discovered as part of our bootstrapping process.
+
+Never hard code URLs as this violates the [HATEOAS](https://en.wikipedia.org/wiki/HATEOAS) design principle.
+
+Additionally each configuration will provide references to DTO classes that is used to parse responses, and details of the body.
+
+```
+class Alarms(
+    APIEndpoint
+):
+    """ Alarms
+    """
+
+    @classmethod
+    async def get_config(cls) -> EndpointConfig:
+        return EndpointConfig(
+            endpoint=Capabilities.CURRENT.features.alarms.alarms,
+            dto_list=AlarmResponse,
+            dto_retrieve=AlarmZoneSummary,
+        )
+```
+
+The above example shows the `Alarms` class which is a consumer of the `alarms` endpoint. It nominates `AlarmResponse` as the class the infrastructure will use to parse `list` responses and `AlarmZoneSummary` as the class to parse `retrieve` responses.
+
+It references the `Capabilities.CURRENT` singleton which is a `Capabilities` instance that is bootstrapped at runtime. This is a singleton that is used to provide references to all endpoints.
+
+If a command centre does not have a certain capability then the objects are set to `None` and accessing the feature raises an exception (more on this in other sections).
+
+### Designing Endpoints
+
+## Layout
+
+Layout of our files
diff --git a/docs/docs/index.md b/docs/docs/index.md
index 2556b46c..79a9a107 100644
--- a/docs/docs/index.md
+++ b/docs/docs/index.md
@@ -1,17 +1,39 @@
 # Welcome to Gallagher Python
 
-This project maintains an idiomatic client for Python
+This project maintains an idiomatic client for Python. The project began as a requirement for us to reliably work with the Gallagher API. While Gallagher publish their [API reference](https://gallaghersecurity.github.io/cc-rest-docs/ref/index.html) which is built from an OpenAPI spec with [Spectacle](https://github.com/sourcey/spectacle) documentation generator flavours. The OpenAPI spec is maintained by hand and [can be found on Github](https://github.com/GallagherSecurity/cc-rest-docs/tree/master/swagger).
+
+Our assumption is that this is for security and technical reasons. In the spirit of building good quality software we embarked on building a Python idiomatic client with full test coverage.
+
+The API client draws inspiration from companies like [Stripe](https://stripe.com) or projects like [pyndatic](https://pydantic.dev) who are known for stellar developer experience. Our aim is to provide a similar quality of developer experience for Gallagher projects.
+
+The design pattern of the API client is opinionated from our experience as software engineers. We goto great lengths to document and justify our thought process so others can see where we are coming from.
+
+> **Note:** This project is not affiliated with Gallagher in any way.
+
+## Features
+
+- pydantic models for all API payloads
+- HTTP transport using httpx
+- Full test coverage
+- A completely python based interface to interacting with Gallagher Command Centre
+
+## Developing the client
+
+This library uses [httpx](https://www.python-httpx.org) as the HTTP transport and [pydantic](https://pydantic.dev) to construct and ingest payloads. We use [taskfile](https://taskfile.dev) to run tasks. Our test suite is setup using `pytest`.
+
+Anomaly has a demo Command Centre set up in the cloud that we run tests against. This is populate using a sample site configuration. There are no real security controllers connected to this instance. Upon a PR being lodged, Github actions is configured to run the entire test suite against our demo instance.
+
+To contribute to the library, please fork this repository and lodge a pull request for us to accept your changes.
 
 ## Contributing to the documentation
 
-* `mkdocs new [dir-name]` - Create a new project.
-* `mkdocs serve` - Start the live-reloading docs server.
-* `mkdocs build` - Build the documentation site.
-* `mkdocs -h` - Print help message and exit.
+The documentation is build using [mkdocs](https://www.mkdocs.org) and hosted on [Github pages](https://anomaly.github.io/gallagher/). The project repository is configured to build and publish the documentation on every commit to the `master` branch.
+
+Some handy commands to get you started:
 
-## Project layout
+- `mkdocs new [dir-name]` - Create a new project.
+- `mkdocs serve -a localhost:8003` - Start the live-reloading docs server, `-a` allows you to provide a custom address.
+- `mkdocs build` - Build the documentation site.
+- `mkdocs -h` - Print help message and exit.
 
-    mkdocs.yml    # The configuration file.
-    docs/
-        index.md  # The documentation homepage.
-        ...       # Other markdown pages, images and other files.
+To start contributing please fork this repository, make the changes you desire and submit a pull request for us to merge your changes in. Alternatively consider [starting a discussion](https://github.com/anomaly/gallagher/discussions) or [raising an issue](https://github.com/anomaly/gallagher/issues). Be kind to our maintainers and check to see if a similar discussion is already in place and join the thread.
diff --git a/docs/docs/usage.md b/docs/docs/usage.md
new file mode 100644
index 00000000..e2fab4ec
--- /dev/null
+++ b/docs/docs/usage.md
@@ -0,0 +1,11 @@
+# Usage
+
+## Installation
+
+We recommend installation via `PyPI`:
+
+```bash
+poetry add gallagher
+```
+
+## Understanding the interface
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index 3f03ce07..540da967 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -3,4 +3,4 @@ site_url: https://anomaly.github.io/gallagher/
 repo_url: https://github.com/anomaly/gallagher/
 site_author: Dev Mukherjee
 theme:
-  name: terminal
+  name: readthedocs
diff --git a/gallagher/__init__.py b/gallagher/__init__.py
index f6f23fb1..9398a6aa 100644
--- a/gallagher/__init__.py
+++ b/gallagher/__init__.py
@@ -7,5 +7,4 @@
  Distributed under the terms of the MIT License.
 """
 
-__version__ = "0.1.0"
-
+__version__ = "0.1.0-alpha.3"
diff --git a/gallagher/cc/__init__.py b/gallagher/cc/__init__.py
index 47f62125..ca1a02e7 100644
--- a/gallagher/cc/__init__.py
+++ b/gallagher/cc/__init__.py
@@ -24,6 +24,7 @@
 
 from ..const import URL
 
+
 # Follow the instructions in the Gallagher documentation
 # to obtain an API key
 api_key: str = None
@@ -38,4 +39,3 @@
 # By default connections are sent straight to the server
 # should you wish to use a proxy, set this to the proxy URL
 proxy: Optional[str] = None
-
diff --git a/gallagher/cc/alarms/__init__.py b/gallagher/cc/alarms/__init__.py
index 1f4c63ae..e9cb0185 100644
--- a/gallagher/cc/alarms/__init__.py
+++ b/gallagher/cc/alarms/__init__.py
@@ -1,4 +1,33 @@
+""" Alarms
+
+
 """
 
+from ..core import (
+    APIEndpoint,
+    EndpointConfig,
+    Capabilities
+)
+
+from ...dto.summary import (
+    AlarmZoneSummary
+)
+
+from ...dto.response import (
+    AlarmResponse,
+)
+
+
+class Alarms(
+    APIEndpoint
+):
+    """ Alarms
+    """
 
-"""
\ No newline at end of file
+    @classmethod
+    async def get_config(cls) -> EndpointConfig:
+        return EndpointConfig(
+            endpoint=Capabilities.CURRENT.features.alarms.alarms,
+            dto_list=AlarmResponse,
+            dto_retrieve=AlarmZoneSummary,
+        )
diff --git a/gallagher/cc/alarms/day_category.py b/gallagher/cc/alarms/day_category.py
index 3913a7e6..021d26bb 100644
--- a/gallagher/cc/alarms/day_category.py
+++ b/gallagher/cc/alarms/day_category.py
@@ -3,22 +3,26 @@
 
 """
 
-from ..utils import (
-    APIBase,
+from ..core import (
+    Capabilities,
+    APIEndpoint,
     EndpointConfig
 )
 
-from ...dto.day_category import (
+from ...dto.response import (
     DayCategoryResponse
 )
 
 
-class DayCategory(APIBase):
+class DayCategory(APIEndpoint):
     """ Day Categories
     """
 
-    __config__ = EndpointConfig(
-        endpoint="day_categories",
-        dto_list=DayCategoryResponse,
-        dto_retrieve=DayCategoryResponse,
-    )
+    @classmethod
+    async def get_config(cls) -> EndpointConfig:
+        return EndpointConfig(
+            endpoint=Capabilities.CURRENT.features.day_categories
+            .day_categories,
+            dto_list=DayCategoryResponse,
+            dto_retrieve=DayCategoryResponse,
+        )
diff --git a/gallagher/cc/alarms/divisions.py b/gallagher/cc/alarms/divisions.py
index 7d00b0e5..661c7e44 100644
--- a/gallagher/cc/alarms/divisions.py
+++ b/gallagher/cc/alarms/divisions.py
@@ -2,13 +2,22 @@
 
 """
 
-from ..utils import (
-    APIBase,
+from ..core import (
+    Capabilities,
+    APIEndpoint,
     EndpointConfig
 )
 
+from ...dto.detail import (
+    DivisionDetail,
+)
+
+from ...dto.response import (
+    DivisionDetailResponse,
+)
+
 
-class Division(APIBase):
+class Division(APIEndpoint):
     """
     Gallagher advises against hardcoding the URLs for divisions, and instead
     recommends using the /api endpoint to discover the URLs from 
@@ -16,6 +25,10 @@ class Division(APIBase):
 
     """
 
-    __config__ = EndpointConfig(
-        endpoint="divisions",
-    )
+    @classmethod
+    async def get_config(cls) -> EndpointConfig:
+        return EndpointConfig(
+            endpoint=Capabilities.CURRENT.features.divisions.divisions,
+            dto_list=DivisionDetailResponse,
+            dto_retrieve=DivisionDetail,
+        )
diff --git a/gallagher/cc/alarms/events.py b/gallagher/cc/alarms/events.py
new file mode 100644
index 00000000..54daef02
--- /dev/null
+++ b/gallagher/cc/alarms/events.py
@@ -0,0 +1,51 @@
+""" Events that the Command Centre
+
+Command Centre has about 80 event types that occur when somebody 
+authenticates at a device, usually by badging a card.
+"""
+
+from ..core import (
+    Capabilities,
+    APIEndpoint,
+    EndpointConfig
+)
+
+from ...dto.detail import (
+    EventDetail,
+)
+
+from ...dto.response import (
+    EventTypeResponse,
+    EventSummaryResponse,
+)
+
+
+class Event(
+    APIEndpoint
+):
+    """ Event 
+
+    """
+
+    @classmethod
+    async def get_config(cls) -> EndpointConfig:
+        return EndpointConfig(
+            endpoint=Capabilities.CURRENT.features.events.events,
+            dto_list=EventSummaryResponse,
+            dto_retrieve=EventDetail,
+        )
+
+
+class EventType(
+    APIEndpoint
+):
+    """ EventType
+
+    """
+
+    @classmethod
+    async def get_config(cls) -> EndpointConfig:
+        return EndpointConfig(
+            endpoint=Capabilities.CURRENT.features.events.event_groups,
+            dto_list=EventTypeResponse,
+        )
diff --git a/gallagher/cc/alarms/items.py b/gallagher/cc/alarms/items.py
index 93036a4b..62175e71 100644
--- a/gallagher/cc/alarms/items.py
+++ b/gallagher/cc/alarms/items.py
@@ -1,29 +1,38 @@
+""" Items
+
+
 """
-"""
-from ..utils import (
-    APIBase,
-    EndpointConfig
+
+from ..core import (
+    Capabilities,
+    APIEndpoint,
+    EndpointConfig,
 )
 
-from ...dto.items import (
+from ...dto.summary import (
+    ItemSummary,
+)
+from ...dto.response import (
     ItemTypesResponse,
     ItemsSummaryResponse,
-    ItemDetail
 )
 
 
-class ItemsTypes(APIBase):
+class ItemsTypes(APIEndpoint):
     """
      Gallagher
     """
 
-    __config__ = EndpointConfig(
-        endpoint="items/types",
-        dto_list=ItemTypesResponse,
-    )
+    @classmethod
+    async def get_config(cls) -> EndpointConfig:
+        return EndpointConfig(
+            endpoint=Capabilities.CURRENT.features.items.item_types,
+            dto_list=ItemTypesResponse,
+            dto_retrieve=ItemTypesResponse,
+        )
 
 
-class Item(APIBase):
+class Item(APIEndpoint):
     """
     Gallagher advises against hardcoding the URLs for divisions, and instead
     recommends using the /api endpoint to discover the URLs from 
@@ -31,8 +40,10 @@ class Item(APIBase):
 
     """
 
-    __config__ = EndpointConfig(
-        endpoint="items",
-        dto_list=ItemsSummaryResponse,
-        dto_retrieve=ItemDetail,
-    )
+    @classmethod
+    async def get_config(cls) -> EndpointConfig:
+        return EndpointConfig(
+            endpoint=Capabilities.CURRENT.features.items.items,
+            dto_list=ItemsSummaryResponse,
+            dto_retrieve=ItemSummary,
+        )
diff --git a/gallagher/cc/alarms/schedule.py b/gallagher/cc/alarms/schedule.py
index ba1cc0ef..49dceeda 100644
--- a/gallagher/cc/alarms/schedule.py
+++ b/gallagher/cc/alarms/schedule.py
@@ -2,21 +2,24 @@
 
 """
 
-from ..utils import (
-    APIBase,
+from ..core import (
+    Capabilities,
+    APIEndpoint,
     EndpointConfig
 )
 
-from ...dto.schedule import (
+from ...dto.response import (
     ScheduleSummaryResponse
 )
 
 
-class Schedule(APIBase):
+class Schedule(APIEndpoint):
     """ Schedules
     """
 
-    __config__ = EndpointConfig(
-        endpoint="schedules",
-        dto_list=ScheduleSummaryResponse,
-    )
+    @classmethod
+    async def get_config(cls) -> EndpointConfig:
+        return EndpointConfig(
+            endpoint=Capabilities.CURRENT.features.schedules.schedules,
+            dto_list=ScheduleSummaryResponse,
+        )
diff --git a/gallagher/cc/card/__init__.py b/gallagher/cc/card/__init__.py
deleted file mode 100644
index 1f4c63ae..00000000
--- a/gallagher/cc/card/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-"""
-
-
-"""
\ No newline at end of file
diff --git a/gallagher/cc/card/card_type.py b/gallagher/cc/card/card_type.py
deleted file mode 100644
index 4574daab..00000000
--- a/gallagher/cc/card/card_type.py
+++ /dev/null
@@ -1,25 +0,0 @@
-"""
-
-"""
-from ..utils import (
-    APIBase,
-    EndpointConfig
-)
-
-from ...dto.card_type import (
-    CardTypeResponse
-)
-
-
-class CardType(APIBase):
-    """ Card Types provide a list of support card types for the instance.
-
-    These can vary between using physical cards, mobile credentials, or
-    biometrics. The card type is used to dynamically determine the types 
-    of credentials available on this particular instance.  
-    """
-
-    __config__ = EndpointConfig(
-        endpoint="card_types",
-        dto_list=CardTypeResponse,
-    )
diff --git a/gallagher/cc/cardholder/__init__.py b/gallagher/cc/cardholders/__init__.py
similarity index 100%
rename from gallagher/cc/cardholder/__init__.py
rename to gallagher/cc/cardholders/__init__.py
diff --git a/gallagher/cc/cardholders/card_type.py b/gallagher/cc/cardholders/card_type.py
new file mode 100644
index 00000000..2aece3f9
--- /dev/null
+++ b/gallagher/cc/cardholders/card_type.py
@@ -0,0 +1,29 @@
+"""
+
+"""
+from ..core import (
+    Capabilities,
+    APIEndpoint,
+    EndpointConfig
+)
+
+from ...dto.response import (
+    CardTypeResponse
+)
+
+
+class CardType(APIEndpoint):
+    """ Card Types provide a list of support card types for the instance.
+
+    These can vary between using physical cards, mobile credentials, or
+    biometrics. The card type is used to dynamically determine the types 
+    of credentials available on this particular instance.  
+    """
+
+    @classmethod
+    async def get_config(cls) -> EndpointConfig:
+        return EndpointConfig(
+            endpoint=Capabilities.CURRENT.features.card_types.card_types,
+            dto_list=CardTypeResponse,
+            dto_retrieve=CardTypeResponse,
+        )
diff --git a/gallagher/cc/cardholders/cardholders.py b/gallagher/cc/cardholders/cardholders.py
new file mode 100644
index 00000000..6a96164e
--- /dev/null
+++ b/gallagher/cc/cardholders/cardholders.py
@@ -0,0 +1,69 @@
+"""
+
+"""
+from ..core import (
+    Capabilities,
+    APIEndpoint,
+    EndpointConfig
+)
+
+from ...dto.detail import (
+    CardholderDetail,
+    PdfDetail,
+)
+
+from ...dto.response import (
+    CardholderSummaryResponse,
+    PdfResponse,
+)
+
+
+class Cardholder(APIEndpoint):
+    """ Cardholder endpoints allow you to search for and retrieve cardholder details.
+
+    Cardholders are the users of the system and are the entities that are 
+    granted access to doors. Cardholders can be people, vehicles, or other 
+    entities that require access to the site. 
+    """
+
+    @classmethod
+    async def get_config(cls) -> EndpointConfig:
+        return EndpointConfig(
+            endpoint=Capabilities.CURRENT.features.cardholders.cardholders,
+            dto_list=CardholderSummaryResponse,
+            dto_retrieve=CardholderDetail,
+        )
+
+    @classmethod
+    async def search(cls,
+                     name: str,
+                     sort: str = "id",
+                     top: int = 100
+                     ):
+        pass
+
+
+class PdfDefinition(APIEndpoint):
+    """ PDF Definitions provide a list of support PDF definitions for the instance.
+
+    These can vary between using physical cards, mobile credentials, or
+    biometrics. The card type is used to dynamically determine the types 
+    of credentials available on this particular instance.  
+    """
+
+    @classmethod
+    async def get_config(cls) -> EndpointConfig:
+        return EndpointConfig(
+            endpoint=Capabilities.CURRENT.features.
+            personal_data_fields.personal_data_fields,
+            dto_list=PdfResponse,
+            dto_retrieve=PdfDetail,
+        )
+
+    @classmethod
+    async def search(cls,
+                     name: str,
+                     sort: str = "id",
+                     top: int = 100
+                     ):
+        pass
diff --git a/gallagher/cc/core.py b/gallagher/cc/core.py
new file mode 100644
index 00000000..8a8cd422
--- /dev/null
+++ b/gallagher/cc/core.py
@@ -0,0 +1,329 @@
+""" Utilities for the Gallagher Command Centre API
+
+This package provides a set of utilities that are used that each endpoint
+uses to communicate with the Gallagher Command Centre API.
+
+Every endpoint inherits from the APIEndpoint class and must define
+a configuration that is assigned to the variable __config__.
+
+The endpoint variable in the EndpointConfig should be assigned to a reference
+to the endpoint in the DiscoveryResponse object. When initialised the
+endpoint will be assigned to None but will self heal as part of 
+the bootstrapping process.
+"""
+from typing import Optional
+from datetime import datetime
+from dataclasses import dataclass
+
+import httpx
+
+from gallagher.exception import (
+    UnlicensedFeatureException
+)
+
+from ..dto.detail import (
+    FeaturesDetail,
+)
+
+from ..dto.response import (
+    DiscoveryResponse,
+)
+
+
+def check_api_key_format(api_key):
+    """ Validates that the Gallagher Key is in the right format.
+
+    It's not possible for the API client to validate the key against
+    Gallagher servers but it will validate that the key is in the
+    right format.
+    """
+    api_tokens = api_key.split('-')
+    return (api_tokens.count() == 8)
+
+
+def get_authorization_headers():
+    """ Creates an authorization header for Gallagher API calls
+
+    The server expects an Authorization header with GGL-API-KEY
+    set to the API key provided by Gallagher Command Centre.
+
+    This API key is what allows the cloud proxy to determine where
+    the request should be routed to.
+
+    See the Authorization section here for more information
+    https://gallaghersecurity.github.io/cc-rest-docs/ref/events.html
+
+    from gallagher import cc, const
+
+    cc.api_key = "GH_"
+
+    """
+    from . import api_key
+    return {
+        'Authorization': f'GGL-API-KEY {api_key}'
+    }
+
+
+@dataclass
+class EndpointConfig:
+    """ A configuration for an API endpoint
+
+    Each API endpoint has a configuration that defines how 
+    the APIBase should attempt to parse responses from the
+    Gallagher servers.
+
+    Additionally it provides defaults for various parameters
+    that are commonly accepted by endpoints.
+
+    Not assigning a configuration to an endpoint will result
+    in APIBase raising an exception.
+
+    The configuration is assigned a special variable called
+    __config__ which is set to None by default.
+
+    """
+    endpoint: str  # partial path to the endpoint e.g. day_category
+    dto_list: Optional[any] = None  # DTO to be used for list requests
+    dto_retrieve: Optional[any] = None  # DTO to be used for retrieve requests
+
+    top: Optional[int] = 10  # Number of response to download
+    sort: Optional[str] = "id"  # Can be set to id or -id
+    # fields: list[str] = []  # Optional list of fields
+
+    @classmethod
+    async def validate_endpoint(cls):
+        """ Check to see if the feature is licensed and available
+
+        Gallagher REST API is licensed per feature, if a feature is not
+        the endpoint is set to none and we should throw an exception
+        """
+        if not cls.endpoint:
+            raise UnlicensedFeatureException(
+                "Endpoint not defined"
+            )
+
+
+class Capabilities:
+
+    # Discover response object, each endpoint will reference
+    # one of the instance variable Href property to get the
+    # path to the endpoint.
+    #
+    # Gallagher recommends that the endpoints not be hardcoded
+    # into the client and instead be discovered at runtime.
+    #
+    # Note that if a feature has not been licensed by a client
+    # then the path will be set to None, if the client attempts
+    # to access the endpoint then the library will throw an exception
+    #
+    # This value is memoized and should perform
+    CURRENT = DiscoveryResponse(
+        version="0.0.0",  # Indicates that it's not been discovered
+        features=FeaturesDetail()
+    )
+
+
+class APIEndpoint:
+    """ Base class for all API objects
+
+    All API endpoints must inherit from this class and provide a Config class
+    that automates the implementation of many of the API methods.
+
+    If the endpoints provide additional methods then they are to implement them
+    based on the same standards as this base class.
+
+    """
+
+    # Do not set this variable in your class, this is set by the
+    # lifecycle methods and use to cache the configuration object
+    __config__ = None
+
+    @classmethod
+    async def get_config(cls) -> EndpointConfig:
+        """ Returns the configuration for the endpoint
+
+        This method can be overridden by the child class to
+        provide additional configuration options.
+        """
+        raise NotImplementedError(
+            "get_config method not implemented"
+        )
+
+    @classmethod
+    async def _discover(cls):
+        """ The Command Centre root API endpoint 
+
+        Much of Gallagher's API documentation suggests that we don't
+        hard code the URL, but instead use the discovery endpoint by 
+        calling the root endpoint. 
+
+        This should be a singleton which is instantiated upon initialisation
+        and then used across the other endpoints.
+
+        For example features.events.events.href is the endpoint for the events
+        where as features.events.events.updates is the endpoint for getting
+        updates to the changes to events.
+
+        This differs per endpoint that we work with.
+
+        Note that references to Capabilities.CURRENT as a singleton, while
+        cls.method when executing a class method.
+        """
+
+        if Capabilities.CURRENT.version != "0.0.0" and\
+                type(Capabilities.CURRENT.good_known_since) is datetime:
+            # We've already discovered the endpoints as per HATEOAS
+            # design requirement, however because the endpoint configuration is
+            # dynamically populated, we have to call the get_config method
+            cls.__config__ = await cls.get_config()
+            return
+
+        # Auto-discovery of the API endpoints, this will
+        # be called as part of the bootstrapping process
+        from . import api_base
+        async with httpx.AsyncClient() as _httpx_async:
+            response = await _httpx_async.get(
+                api_base,
+                headers=get_authorization_headers(),
+            )
+
+            await _httpx_async.aclose()
+
+            parsed_obj = DiscoveryResponse.model_validate(
+                response.json()
+            )
+
+            # Assign the capabilities to the class, this should
+            # result in the endpoint
+            #
+            # With the refactored initialisation of the pydantic
+            # models, the values for the unavailable endpoints
+            # should be set to None
+            Capabilities.CURRENT = parsed_obj
+
+            # Set this so the configuration is only discovered
+            # once per endpoint
+            #
+            # If we assign the __config__ variable in the class
+            # that inherits from this class, the instance of EndpointConfig
+            # will copy the None values from the Capabilities.CURRENT
+            # object, this primarily because Capabilities.CURRENT is
+            # an instance of a pyndatic object and all values are thus
+            # copied not referenced.
+            cls.__config__ = await cls.get_config()
+
+    @classmethod
+    async def list(cls, skip=0):
+        """ For a list of objects for the given resource
+
+        Most resources can be searched which is exposed by this method.
+        Resources also allow pagination which can be controlled by the skip
+        """
+        await cls._discover()
+
+        async with httpx.AsyncClient() as _httpx_async:
+
+            response = await _httpx_async.get(
+                f'{cls.__config__.endpoint.href}',
+                headers=get_authorization_headers(),
+            )
+
+            await _httpx_async.aclose()
+
+            parsed_obj = cls.__config__.dto_list.model_validate(
+                response.json()
+            )
+
+            return parsed_obj
+
+    @classmethod
+    async def retrieve(cls, id):
+        """ Retrieve a single object for the given resource
+
+        Most objects have an ID which is numeral or UUID. 
+        Each resource also provides a href and pagination for
+        children.
+        """
+        await cls._discover()
+
+        async with httpx.AsyncClient() as _httpx_async:
+
+            response = await _httpx_async.get(
+                f'{cls.__config__.endpoint.href}/{id}',
+                headers=get_authorization_headers(),
+            )
+
+            await _httpx_async.aclose()
+
+            parsed_obj = cls.__config__.dto_retrieve.model_validate(
+                response.json()
+            )
+
+            return parsed_obj
+
+    @classmethod
+    async def modify(cls):
+        """
+
+        """
+        pass
+
+    @classmethod
+    async def create(cls, **params):
+        """
+
+        """
+        cls._discover()
+
+    @classmethod
+    async def delete(cls):
+        """
+
+        """
+        cls._discover()
+
+    @classmethod
+    async def search(cls,
+                     top: int = 100,
+                     sort: str = 'id',
+                     fields: str = 'defaults',
+                     **kwargs
+                     ):
+        """ Search wrapper for most objects to dynamically search content
+
+        Each object has a set of fields that you can query for, most searches
+        also allow you to search for a partial string.
+
+        :param int top: Number of results to return
+        :param str sort: Sort order, can be set to id or -id
+        :param str fields: List of fields to return
+        :param kwargs: Fields to search for
+
+        """
+        await cls._discover()
+
+        params = {
+            'top': top,
+            'sort': sort,
+            'fields': fields,
+        }
+
+        # Adds arbitrary fields to the search, these will be different
+        # for each type of object that calls the base function
+        params.update(kwargs)
+
+        async with httpx.AsyncClient() as _httpx_async:
+
+            response = await _httpx_async.get(
+                f'{cls.__config__.endpoint.href}',
+                params=params,
+                headers=get_authorization_headers(),
+            )
+
+            await _httpx_async.aclose()
+
+            parsed_obj = cls.__config__.dto_list.model_validate(
+                response.json()
+            )
+
+            return parsed_obj
diff --git a/gallagher/cc/status_overrides/__init__.py b/gallagher/cc/status_overrides/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/cc/utils.py b/gallagher/cc/utils.py
deleted file mode 100644
index 86936cbc..00000000
--- a/gallagher/cc/utils.py
+++ /dev/null
@@ -1,155 +0,0 @@
-""" Utilities for the Gallagher Command Centre API
-
-"""
-from typing import Optional
-from dataclasses import dataclass
-
-import httpx
-
-
-def check_api_key_format(api_key):
-    """ Validates that the Gallagher Key is in the right format.
-
-    It's not possible for the API client to validate the key against
-    Gallagher servers but it will validate that the key is in the
-    right format.
-    """
-    api_tokens = api_key.split('-')
-    return (api_tokens.count() == 8)
-
-
-def get_authorization_headers():
-    """ Creates an authorization header for Gallagher API calls
-
-    The server expects an Authorization header with GGL-API-KEY
-    set to the API key provided by Gallagher Command Centre.
-
-    This API key is what allows the cloud proxy to determine where
-    the request should be routed to.
-
-    See the Authorization section here for more information
-    https://gallaghersecurity.github.io/cc-rest-docs/ref/events.html
-
-    from gallagher import cc, const
-
-    cc.api_key = "GH_"
-
-    """
-    from . import api_key
-    return {
-        'Authorization': f'GGL-API-KEY {api_key}'
-    }
-
-
-@dataclass
-class EndpointConfig:
-    """ A configuration for an API endpoint
-
-    Each API endpoint has a configuration that defines how 
-    the APIBase should attempt to parse responses from the
-    Gallagher servers.
-
-    Additionally it provides defaults for various parameters
-    that are commonly accepted by endpoints.
-
-    Not assigning a configuration to an endpoint will result
-    in APIBase raising an exception.
-
-    The configuration is assigned a special variable called
-    __config__ which is set to None by default.
-
-    """
-    endpoint: str  # partial path to the endpoint e.g. day_category
-    dto_list: Optional[any] = None  # DTO to be used for list requests
-    dto_retrieve: Optional[any] = None  # DTO to be used for retrieve requests
-
-    top: Optional[int] = 10  # Number of response to download
-    sort: Optional[str] = "id"  # Can be set to id or -id
-    # fields: list[str] = []  # Optional list of fields
-
-
-class APIBase():
-    """ Base class for all API objects
-
-    All API endpoints must inherit from this class and provide a Config class
-    that automates the implementation of many of the API methods.
-
-    If the endpoints provide additional methods then they are to implement them
-    based on the same standards as this base class.
-
-    """
-
-    # This must be overridden by each child class that inherits
-    # from this base class.
-    __config__ = None
-
-    @classmethod
-    def _discover(cls):
-        pass
-
-    @classmethod
-    def list(cls, skip=0):
-        """ For a list of objects for the given resource
-
-        Most resources can be searched which is exposed by this method.
-        Resources also allow pagination which can be controlled by the skip
-        """
-        from . import api_base
-        response = httpx.get(
-            f'{api_base}{cls.__config__.endpoint}',
-            headers=get_authorization_headers(),
-        )
-
-        parsed_obj = cls.__config__.dto_list.model_validate(
-            response.json()
-        )
-
-        return parsed_obj
-
-    @classmethod
-    def retrieve(cls, id):
-        """ Retrieve a single object for the given resource
-
-        Most objects have an ID which is numeral or UUID. 
-        Each resource also provides a href and pagination for
-        children.
-        """
-        from . import api_base
-        response = httpx.get(
-            f'{api_base}{cls.__config__.endpoint}/{id}',
-            headers=get_authorization_headers(),
-        )
-
-        parsed_obj = cls.__config__.dto_retrieve.model_validate(
-            response.json()
-        )
-
-        return parsed_obj
-
-    @classmethod
-    def modify(cls):
-        """
-
-        """
-        pass
-
-    @classmethod
-    def create(cls, **params):
-        """
-
-        """
-        pass
-
-    @classmethod
-    def delete(cls):
-        """
-
-        """
-        pass
-
-    @classmethod
-    def search(cls):
-        """
-
-        """
-        pass
diff --git a/gallagher/cli/__init__.py b/gallagher/cli/__init__.py
new file mode 100644
index 00000000..01b52b4c
--- /dev/null
+++ b/gallagher/cli/__init__.py
@@ -0,0 +1,41 @@
+""" CLI entry point
+
+gal is the gallagher command line, which is constructed using Typer.
+At the moment typer does not support async functions, so we have to
+use a wrapper to make it work. See utils.py for the AsyncTyper class.
+
+The cli should be pretty self documenting, however see docs/ for
+official documentation.
+"""
+import os
+
+from gallagher import (
+    cc,
+    __version__
+)
+
+from .utils import AsyncTyper
+
+from .alarms import app as alarms_app
+from .cardholders import app as cardholders_app
+from .events import app as events_app
+
+# Load the API key for the package so all entry points
+# can use it to query the service
+api_key = os.environ.get("GACC_API_KEY")
+cc.api_key = api_key
+
+# Main Typer app use to create the CLI
+app = AsyncTyper()
+# Load up all sub commands
+app.add_typer(alarms_app, name="alarms")
+app.add_typer(cardholders_app, name="ch")
+app.add_typer(events_app, name="events")
+
+if __name__ == "__main__":
+    """ In case you are invoking this via Python directly
+
+    This is probably never actually used but it is here for completeness.
+    You'd execute this by running `python -m gallagher.cli`
+    """
+    app()
diff --git a/gallagher/cli/alarms.py b/gallagher/cli/alarms.py
new file mode 100644
index 00000000..4fd3ce4c
--- /dev/null
+++ b/gallagher/cli/alarms.py
@@ -0,0 +1,12 @@
+""" Alarms
+
+
+"""
+
+
+from .utils import AsyncTyper
+
+
+app = AsyncTyper(
+    help="list or query alarms in the command centre"
+)
diff --git a/gallagher/cli/cardholders.py b/gallagher/cli/cardholders.py
new file mode 100644
index 00000000..a2c3c00b
--- /dev/null
+++ b/gallagher/cli/cardholders.py
@@ -0,0 +1,45 @@
+""" Cardholder cli commands mounted at ch
+
+"""
+from rich import print as rprint
+from rich.console import Console
+from rich.table import Table
+
+from .utils import AsyncTyper
+
+from gallagher.cc.cardholders.cardholders import (
+    Cardholder
+)
+
+app = AsyncTyper(
+    help="query or manage cardholders"
+)
+
+
+@app.command("list")
+async def list():
+    """ list all cardholders
+    """
+    console = Console()
+    with console.status(
+        "[bold green]Fetching cardholders...",
+        spinner="clock"
+    ):
+        cardholders = await Cardholder.list()
+
+        table = Table(title="Cardholders")
+        for header in cardholders.cli_header:
+            table.add_column(header)
+
+        for row in cardholders.__rich_repr__():
+            table.add_row(*row)
+
+        console.print(table)
+
+
+@app.command("get")
+async def get(id: int):
+    """ get a cardholder by id
+    """
+    cardholder = await Cardholder.retrieve(id)
+    [rprint(r) for r in cardholder.__rich_repr__()]
diff --git a/gallagher/cli/events.py b/gallagher/cli/events.py
new file mode 100644
index 00000000..5ea2b24f
--- /dev/null
+++ b/gallagher/cli/events.py
@@ -0,0 +1,12 @@
+""" Cardholder events
+
+
+"""
+
+
+from .utils import AsyncTyper
+
+
+app = AsyncTyper(
+    help="query command centre events"
+)
diff --git a/gallagher/cli/utils.py b/gallagher/cli/utils.py
new file mode 100644
index 00000000..d2733375
--- /dev/null
+++ b/gallagher/cli/utils.py
@@ -0,0 +1,49 @@
+""" Asyncio patches for Typer
+
+There's a thread open on the typer repo about this:
+    https://github.com/tiangolo/typer/issues/88
+
+Essentially, Typer doesn't support async functions. This is an issue
+post migration to async, @csheppard points out that there's a work
+around using a decorator on the click repository:
+https://github.com/tiangolo/typer/issues/88#issuecomment-612687289
+
+@gilcu2 posted a similar solution on the typer repo:
+https://github.com/tiangolo/typer/issues/88#issuecomment-1732469681
+
+this particular one uses asyncer to run the async function in a thread
+we're going in with this with the hope that the official solution is
+closer to this than a decorator per command.
+"""
+import inspect
+
+from functools import (
+    partial,
+    wraps,
+)
+
+import asyncer
+from typer import Typer
+
+
+class AsyncTyper(Typer):
+    @staticmethod
+    def maybe_run_async(decorator, f):
+        if inspect.iscoroutinefunction(f):
+
+            @wraps(f)
+            def runner(*args, **kwargs):
+                return asyncer.runnify(f)(*args, **kwargs)
+
+            decorator(runner)
+        else:
+            decorator(f)
+        return f
+
+    def callback(self, *args, **kwargs):
+        decorator = super().callback(*args, **kwargs)
+        return partial(self.maybe_run_async, decorator)
+
+    def command(self, *args, **kwargs):
+        decorator = super().command(*args, **kwargs)
+        return partial(self.maybe_run_async, decorator)
diff --git a/gallagher/const.py b/gallagher/const.py
index 550b66b0..f45764dd 100644
--- a/gallagher/const.py
+++ b/gallagher/const.py
@@ -1,11 +1,29 @@
 """ Constants for the Gallagher Cloud ecosystem 
+
+Gallagher publishes a set of networking constants for their cloud gateways
+these should be updated as the documentation is updated.
 """
+
+
 class URL:
+    """ DNS names for the cloud gateways
+
+    These are published by Gallagher on Github
+    https://gallaghersecurity.github.io/commandcentre-cloud-api-gateway.html
+    """
+
+    CLOUD_GATEWAY_AU: str = \
+        "https://commandcentre-api-au.security.gallagher.cloud/api/"
+    CLOUD_GATEWAY_US: str = \
+        "https://commandcentre-api-us.security.gallagher.cloud/api/"
 
-    CLOUD_GATEWAY_AU: str = "https://commandcentre-api-au.security.gallagher.cloud/api/"
-    CLOUD_GATEWAY_US: str = "https://commandcentre-api-us.security.gallagher.cloud/api/"
 
 class IP_ADDR:
+    """ IP addresses for the cloud gateways
+
+    These are published by Gallagher on Github
+    https://gallaghersecurity.github.io/commandcentre-cloud-api-gateway.html
+    """
 
     CLOUD_GATEWAY_AU = [
         "3.106.1.6",
diff --git a/gallagher/dto/__init__.py b/gallagher/dto/__init__.py
index 1a680b80..7e2a207e 100644
--- a/gallagher/dto/__init__.py
+++ b/gallagher/dto/__init__.py
@@ -1,7 +1,14 @@
-"""
+""" Data Transfer Object
+
+This package defines the data transfer objects (DTOs) to parse
+the data from the Gallagher API into Python objects.
+
+We use pyndatic to define the DTOs. pydantic is able to validate
+the data types and values of the DTOs.
 
- Resources:
+Our ultimate aim is define rules as outlined by their documentation.
+
+Resources:
   - https://bit.ly/3UkhQhS
   
 """
-
diff --git a/gallagher/dto/alarm.py b/gallagher/dto/alarm.py
deleted file mode 100644
index 1ea73cb4..00000000
--- a/gallagher/dto/alarm.py
+++ /dev/null
@@ -1,27 +0,0 @@
-"""
-
-"""
-
-
-from .utils import (
-    AppBaseModel,
-    HrefMixin
-)
-class AlarmZoneRef(
-    AppBaseModel,
-    HrefMixin
-):
-    """ AccessZone represents
-    """
-    name: str
-
-class AlarmZoneSummary(
-    AppBaseModel,
-):
-    """ #TODO: Revise this if it shows up in other places
-
-    I have literally named this class to model the alarm_zones
-    property in the access_group schema. I don't know if this
-    is appropriate
-    """
-    alarm_zone: AlarmZoneRef
\ No newline at end of file
diff --git a/gallagher/dto/cardholder.py b/gallagher/dto/cardholder.py
deleted file mode 100644
index 4f72c2c4..00000000
--- a/gallagher/dto/cardholder.py
+++ /dev/null
@@ -1,38 +0,0 @@
-"""
-
-"""
-
-from datetime import datetime
-
-from .utils import (
-    AppBaseModel,
-    IdentityMixin,
-)
-
-
-class CardholderSummary(
-    AppBaseModel,
-    IdentityMixin
-):
-    """
-    The cardholder search at /api/cardholders returns an array of these. 
-    It is a subset of what you get from a cardholder's detail page at 
-    /api/cardholders/{id} 
-    
-    (linked as the href in this object), to be more suitable for large result sets.
-    """
-    
-    first_name: str
-    last_name: str
-    short_name: str
-    description: str
-    authorised: str
-
-
-class CardholderDetail(
-    CardholderSummary
-):
-    """
-    
-    """
-    last_successful_access_time: datetime
diff --git a/gallagher/dto/detail/__init__.py b/gallagher/dto/detail/__init__.py
new file mode 100644
index 00000000..e0df6b56
--- /dev/null
+++ b/gallagher/dto/detail/__init__.py
@@ -0,0 +1,36 @@
+"""
+
+
+"""
+
+from .access_group import (
+    AccessGroupDetail,
+)
+
+from .card_type import (
+    CardTypeDetail,
+)
+
+from .cardholder import (
+    CardholderDetail,
+)
+
+from .discover import (
+    FeaturesDetail,
+)
+
+from .division import (
+    DivisionDetail,
+)
+
+from .event import (
+    EventDetail,
+)
+
+from .pdf import (
+    PdfDetail,
+)
+
+from .role import (
+    RoleDetail,
+)
diff --git a/gallagher/dto/access_group.py b/gallagher/dto/detail/access_group.py
similarity index 61%
rename from gallagher/dto/access_group.py
rename to gallagher/dto/detail/access_group.py
index 26017b06..00b9baf5 100644
--- a/gallagher/dto/access_group.py
+++ b/gallagher/dto/detail/access_group.py
@@ -1,56 +1,38 @@
-
 from typing import Optional
 
-from .utils import (
+from ..utils import (
     AppBaseModel,
     IdentityMixin,
     HrefMixin
 )
+from ..summary import (
+    AlarmZoneSummary,
+    AccessSummary,
+    SaltoAccessItemSummary,
+)
+from ..ref import (
+    AccessGroupRef,
+    PDFRef,
+)
 
-from .division import DivisionDetail
-from .pdf import PDFRef
-from .schedule import ScheduleRef
-from .zone import AccessZoneRef
-from .salto import SaltoAccessItemSummary
-from .alarm import AlarmZoneSummary
-
-class AccessSummary(
-    AppBaseModel
-):
-    """ Access is zone paired with a schedule
-    """
-    access_zone: AccessZoneRef
-    schedule: ScheduleRef
+from .division import (
+    DivisionDetail
+)
 
 
-class AccessGroupRef(
+class AccessGroupDetail(
     AppBaseModel,
-    HrefMixin
+    HrefMixin,
 ):
-    """ Access Groups is what a user is assigned to to provide access to doors
     """
-    name: str
-
-class AccessGroupSummary(
-    AccessGroupRef
-):
-    """ AccessGroup Summary is what the API returns on searches
-
-    This builds on the Ref class to add the summary fields and is
-    extended by the Detail class to add the fully remainder of
-    the fields
     """
+    name: str
     description: Optional[str]
     parent: Optional[AccessGroupRef]
     division: IdentityMixin
     cardholders: Optional[HrefMixin]
     server_display_name: Optional[str]
 
-class AccessGroupDetail(
-    AccessGroupSummary
-):
-    """
-    """
     description: Optional[str]
     parent: Optional[AccessGroupRef]
     division: DivisionDetail
diff --git a/gallagher/dto/detail/alarm.py b/gallagher/dto/detail/alarm.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/card_type.py b/gallagher/dto/detail/card_type.py
similarity index 59%
rename from gallagher/dto/card_type.py
rename to gallagher/dto/detail/card_type.py
index fd8c9267..0efb4861 100644
--- a/gallagher/dto/card_type.py
+++ b/gallagher/dto/detail/card_type.py
@@ -1,20 +1,15 @@
-"""
-
-
-"""
-
 from typing import Optional
 
-from .utils import (
+from ..utils import (
     AppBaseModel,
+    HrefMixin,
     IdentityMixin,
-    HrefMixin
 )
 
-class CardExpiryType(
-    AppBaseModel
-):
-    expiry_type: Optional[str] = None
+from ..summary import (
+    CardExpiryTypeSummary
+)
+
 
 class CardTypeDetail(
     AppBaseModel,
@@ -30,14 +25,6 @@ class CardTypeDetail(
     facility_code: str
     credential_class: str
     available_card_states: list[str] = None
-    default_expiry: Optional[CardExpiryType] = None
+    default_expiry: Optional[CardExpiryTypeSummary] = None
     send_registration_email: Optional[bool] = False
     send_registration_sms: Optional[bool] = False
-
-class CardTypeResponse(
-    AppBaseModel,
-):
-    """ Card Types are cards mobile or physical that are supported at a site
-    """
-    results: list[CardTypeDetail]
-    next: Optional[HrefMixin] = None
diff --git a/gallagher/dto/detail/cardholder.py b/gallagher/dto/detail/cardholder.py
new file mode 100644
index 00000000..f4057af6
--- /dev/null
+++ b/gallagher/dto/detail/cardholder.py
@@ -0,0 +1,58 @@
+from typing import Optional
+
+from ..utils import (
+    AppBaseModel,
+    IdentityMixin,
+    HrefMixin,
+)
+
+from ..ref import (
+    DivisionRef,
+)
+
+
+class CardholderDetail(
+    AppBaseModel,
+    IdentityMixin,
+):
+    """ Displays a table of cardholders
+
+    Gallagher command centre offers a summary of all the cardholders
+    provisioned on the system. This command presents a summary table
+    with the aim of using the identifier to get detailed information.
+
+    """
+    first_name: str
+    last_name: str
+    short_name: Optional[str] = None
+    description: Optional[str] = None
+    authorised: bool
+
+    disable_cipher_pad: bool = False
+    division: DivisionRef
+    edit: HrefMixin
+
+    operator_login_enabled: bool = False
+    operator_password_expired: bool = False
+
+    update_location: HrefMixin
+    updates: HrefMixin
+
+    user_extended_access_time: bool = False
+    windows_login_enabled: bool = False
+
+    def __rich_repr__(self):
+        return [
+            f"[blue bold] person",
+            f"{'id':>20} {self.id}",
+            f"{'first_name':>20} {self.first_name}",
+            f"{'last_name':>20} {self.last_name}",
+            f"{'short_name':>20} {self.short_name}",
+            f"{'description':>20} {self.description}",
+            f"{'authorised':>20} {'yes' if self.authorised else 'no'}",
+            f"",
+            f"{'disable_cipher_pad':>20} {'yes' if self.disable_cipher_pad else 'no'}",
+            f"{'division':>20} {self.division.id}",
+            f"[blue bold] hrefs",
+            f"{'edit':>20} [link={self.edit.href}]edit[/link]",
+        ]
diff --git a/gallagher/dto/detail/day_category.py b/gallagher/dto/detail/day_category.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/detail/discover.py b/gallagher/dto/detail/discover.py
new file mode 100644
index 00000000..bbfe613d
--- /dev/null
+++ b/gallagher/dto/detail/discover.py
@@ -0,0 +1,99 @@
+from typing import (
+    Optional
+)
+
+from ..utils import (
+    AppBaseModel,
+)
+
+from ..ref import (
+    FeatureAccessGroupsRef,
+    FeatureAccessZonesRef,
+    FeatureAlarmZonesRef,
+    FeatureAlarmsRef,
+    FeatureCardTypesRef,
+    FeatureCardholdersRef,
+    FeatureCompetenciesRef,
+    FeatureDayCategoriesRef,
+    FeatureDivisionsRef,
+    FeatureDoorsRef,
+    FeatureElevatorsRef,
+    FeatureEventsRef,
+    FeatureFenceZonesRef,
+    FeatureInputsRef,
+    FeatureInterlockGroupsRef,
+    FeatureItemsRef,
+    FeatureLockerBanksRef,
+    FeatureMacrosRef,
+    FeatureOperatorGroupsRef,
+    FeatureOutputsRef,
+    FeaturePersonalDataFieldsRef,
+    FeatureReceptionsRef,
+    FeatureRolesRef,
+    FeatureSchedulesRef,
+    FeatureVisitsRef,
+)
+
+
+class FeaturesDetail(
+    AppBaseModel,
+):
+    """ A detailed list of features that are available on the server.
+
+    All features are marked as Optional, which means that by default
+    it's assumed that they are not available on the server. Upon discovery
+    if a feature is enabled on the server then we receive a href which
+    indicates to the client that the feature is available.
+
+    If a feature is unavailable the API client will throw an exception.
+    """
+    access_groups: Optional[FeatureAccessGroupsRef]\
+        = FeatureAccessGroupsRef()
+    access_zones: Optional[FeatureAccessZonesRef]\
+        = FeatureAccessZonesRef()
+    alarm_zones: Optional[FeatureAlarmZonesRef]\
+        = FeatureAlarmZonesRef()
+    alarms: Optional[FeatureAlarmsRef]\
+        = FeatureAlarmsRef()
+    card_types: Optional[FeatureCardTypesRef]\
+        = FeatureCardTypesRef()
+    cardholders: Optional[FeatureCardholdersRef]\
+        = FeatureCardholdersRef()
+    competencies: Optional[FeatureCompetenciesRef]\
+        = FeatureCompetenciesRef()
+    day_categories: Optional[FeatureDayCategoriesRef]\
+        = FeatureDayCategoriesRef()
+    divisions: Optional[FeatureDivisionsRef]\
+        = FeatureDivisionsRef()
+    doors: Optional[FeatureDoorsRef]\
+        = FeatureDoorsRef()
+    elevators: Optional[FeatureElevatorsRef]\
+        = FeatureElevatorsRef()
+    events: Optional[FeatureEventsRef]\
+        = FeatureEventsRef()
+    fence_zones: Optional[FeatureFenceZonesRef]\
+        = FeatureFenceZonesRef()
+    inputs: Optional[FeatureInputsRef]\
+        = FeatureInputsRef()
+    interlock_groups: Optional[FeatureInterlockGroupsRef]\
+        = FeatureInterlockGroupsRef()
+    items: Optional[FeatureItemsRef]\
+        = FeatureItemsRef()
+    locker_banks: Optional[FeatureLockerBanksRef]\
+        = FeatureLockerBanksRef()
+    macros: Optional[FeatureMacrosRef]\
+        = FeatureMacrosRef()
+    operator_groups: Optional[FeatureOperatorGroupsRef]\
+        = FeatureOperatorGroupsRef()
+    outputs: Optional[FeatureOutputsRef]\
+        = FeatureOutputsRef()
+    personal_data_fields: Optional[FeaturePersonalDataFieldsRef]\
+        = FeaturePersonalDataFieldsRef()
+    receptions: Optional[FeatureReceptionsRef]\
+        = FeatureReceptionsRef()
+    roles: Optional[FeatureRolesRef]\
+        = FeatureRolesRef()
+    schedules: Optional[FeatureSchedulesRef]\
+        = FeatureSchedulesRef()
+    visits: Optional[FeatureVisitsRef]\
+        = FeatureVisitsRef()
diff --git a/gallagher/dto/detail/division.py b/gallagher/dto/detail/division.py
new file mode 100644
index 00000000..fba21564
--- /dev/null
+++ b/gallagher/dto/detail/division.py
@@ -0,0 +1,25 @@
+from typing import Optional
+
+from ..utils import (
+    AppBaseModel,
+    IdentityMixin,
+    HrefMixin
+)
+
+
+class DivisionDetail(
+    AppBaseModel,
+    IdentityMixin,
+    HrefMixin
+):
+    """
+    """
+
+    name: str
+    description: Optional[str] = None
+    server_display_name: Optional[str] = None
+    parent: Optional[HrefMixin] = None
+
+    # TODO: Looks like we don't have access to visitor management
+    # on our test instance at the moment
+    # visitor_management: visitor.VisitorManagementSummary
diff --git a/gallagher/dto/detail/door.py b/gallagher/dto/detail/door.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/detail/event.py b/gallagher/dto/detail/event.py
new file mode 100644
index 00000000..85d933c6
--- /dev/null
+++ b/gallagher/dto/detail/event.py
@@ -0,0 +1,46 @@
+from typing import Optional
+from datetime import datetime
+
+from ..utils import (
+    AppBaseModel,
+    HrefMixin,
+    IdentityMixin,
+)
+
+from ..ref import (
+    AlarmRef,
+    CardholderRef,
+    DoorRef,
+    AccessZoneRef,
+    DivisionRef,
+    ItemRef,
+)
+
+
+class EventDetail(
+    AppBaseModel,
+    HrefMixin,
+    IdentityMixin,
+):
+    """ Details of an event that took place on a server
+    """
+    server_display_name: str
+    time: datetime
+    message: Optional[str]
+    occurrences: int
+    priority: int
+    alarm: AlarmRef
+
+    operator: CardholderRef
+    source: ItemRef
+    # group: EventGroupSummary
+    # type: EventTypeSummary
+    # event_type: EventTypeSummary
+    # division: DivisionRef
+    # cardholder: CardholderSummary
+    entry_access_zone: AccessZoneRef
+    exit_access_zone: AccessZoneRef
+    door: DoorRef
+    access_group: HrefMixin
+    # card: str
+    # modified_item: str
diff --git a/gallagher/dto/detail/features.py b/gallagher/dto/detail/features.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/detail/items.py b/gallagher/dto/detail/items.py
new file mode 100644
index 00000000..9f490728
--- /dev/null
+++ b/gallagher/dto/detail/items.py
@@ -0,0 +1,3 @@
+from ..utils import (
+    AppBaseModel,
+)
diff --git a/gallagher/dto/detail/locker.py b/gallagher/dto/detail/locker.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/detail/operator.py b/gallagher/dto/detail/operator.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/pdf.py b/gallagher/dto/detail/pdf.py
similarity index 50%
rename from gallagher/dto/pdf.py
rename to gallagher/dto/detail/pdf.py
index c1d9f58f..a72fe814 100644
--- a/gallagher/dto/pdf.py
+++ b/gallagher/dto/detail/pdf.py
@@ -1,25 +1,19 @@
-""" Personal Data Fields (not to be confused with PDF files)
-
-"""
-
 from typing import Optional
 
-from .utils import AppBaseModel, IdentityMixin,\
-    HrefMixin
+from ..utils import (
+    AppBaseModel,
+    IdentityMixin,
+    HrefMixin,
+)
+
 
-class PDFRef(
+class PdfDetail(
     AppBaseModel,
     HrefMixin
 ):
     """ Personal Data Fields are custom fields for a card holder
     """
     name: str
-
-class PDFDetail(
-    PDFRef
-):
-    """ Personal Data Fields are custom fields for a card holder
-    """
     description: Optional[str]
     division: IdentityMixin
     server_display_name: Optional[str]
diff --git a/gallagher/dto/detail/reception.py b/gallagher/dto/detail/reception.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/role.py b/gallagher/dto/detail/role.py
similarity index 81%
rename from gallagher/dto/role.py
rename to gallagher/dto/detail/role.py
index c79c0c49..91fd587d 100644
--- a/gallagher/dto/role.py
+++ b/gallagher/dto/detail/role.py
@@ -1,15 +1,15 @@
-"""
-
-"""
 from typing import Optional
 
-from .utils import (
+from ..utils import (
     AppBaseModel,
     IdentityMixin,
     HrefMixin
 )
 
-from .division import DivisionRef
+from ..ref import (
+    DivisionRef
+)
+
 
 class RoleDetail(
     AppBaseModel,
@@ -22,4 +22,3 @@ class RoleDetail(
     server_display_name: str
     description: Optional[str]
     division: DivisionRef
-
diff --git a/gallagher/dto/detail/salto.py b/gallagher/dto/detail/salto.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/detail/schedule.py b/gallagher/dto/detail/schedule.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/detail/visit.py b/gallagher/dto/detail/visit.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/detail/visitor.py b/gallagher/dto/detail/visitor.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/detail/zone.py b/gallagher/dto/detail/zone.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/discover.py b/gallagher/dto/discover.py
deleted file mode 100644
index 9f3457f2..00000000
--- a/gallagher/dto/discover.py
+++ /dev/null
@@ -1,34 +0,0 @@
-"""
-
-"""
-
-from .utils import (
-    AppBaseModel,
-    HrefMixin
-)
-
-class FeatureAccessGroups(
-    AppBaseModel,
-):
-    access_groups: HrefMixin
-
-class APIFeatures(
-    AppBaseModel,
-):
-    access_groups: FeatureAccessGroups
-
-class API(
-    AppBaseModel,
-):
-    """
-    The API obje
-    """
-
-    version: str
-    features: APIFeatures
-
-    @property
-    def get_sem_ver(self):
-        """ Get a SemVer tuple from the version string
-        """
-        return self.version.split(".")
diff --git a/gallagher/dto/division.py b/gallagher/dto/division.py
deleted file mode 100644
index 7ad5f91b..00000000
--- a/gallagher/dto/division.py
+++ /dev/null
@@ -1,40 +0,0 @@
-"""
-
-"""
-
-from typing import Optional
-
-from .utils import (
-    AppBaseModel,
-    IdentityMixin,
-    HrefMixin
-)
-
-# import visitor.VisitorManagementSummary
-
-class DivisionRef(
-    AppBaseModel,
-    IdentityMixin,
-    HrefMixin
-):
-    """ Division reference is used to link to a division
-
-    The Mixins cover all the fields that are returned in the
-    summary, hence nothing has to be declared in the body
-    """
-    pass
-
-    
-class DivisionDetail(
-    AppBaseModel,
-    IdentityMixin,
-):
-    """
-    """
-
-    name: str
-    description: Optional[str]
-    server_display_name: Optional[str]
-    parent: Optional[HrefMixin]
-
-    # visitor_management: visitor.VisitorManagementSummary
diff --git a/gallagher/dto/features.py b/gallagher/dto/features.py
deleted file mode 100644
index c4626c05..00000000
--- a/gallagher/dto/features.py
+++ /dev/null
@@ -1,11 +0,0 @@
-""" Represents the capabilities of a Gallagher Command Centre server.
-
- 
-
-"""
-
-from .utils import (
-    AppBaseModel,
-    IdentityMixin,
-    HrefMixin
-)
diff --git a/gallagher/dto/items.py b/gallagher/dto/items.py
deleted file mode 100644
index 8e3b7eb2..00000000
--- a/gallagher/dto/items.py
+++ /dev/null
@@ -1,79 +0,0 @@
-"""
-
-
-"""
-
-from typing import Optional
-
-from .utils import (
-    AppBaseModel,
-    IdentityMixin,
-    HrefMixin
-)
-
-from .division import DivisionRef
-
-
-class ItemTypeDetail(
-    AppBaseModel,
-):
-    """ Items Types only provide the name and id
-
-    This is used by the discovery endpoint and is used by
-    the ItemTypesResponse
-    """
-    id: str
-    name: str
-    canonical_type_name: str
-
-
-class ItemRef(
-    AppBaseModel,
-    HrefMixin
-):
-    """ Reference to an ItemType
-    """
-    name: str
-
-
-class ItemSummary(
-    ItemRef,
-    IdentityMixin,
-):
-    """ Summary of an Item which adds the notes and 
-    server_display_name, this is used by the item summary response
-    """
-    type: ItemTypeDetail
-
-
-class ItemDetail(
-    ItemSummary,
-):
-    """ All attributes of the Summary plus the division
-
-    While running our tests we found that for some system level
-    objects the divison can be optional, this attribute is hence
-    marked optional, please test for availability before using it.
-    """
-    division: Optional[DivisionRef] = None
-
-
-class ItemsSummaryResponse(
-    AppBaseModel,
-):
-    """  ItemsResponse is the list of items from the API
-    it provides the summary of all Items Summary
-    """
-    results: list[ItemSummary]
-    next: Optional[HrefMixin] = None
-
-
-class ItemTypesResponse(
-    AppBaseModel,
-):
-    """ Every security centre can provide a list of item types
-
-    While the response is rather abridged, this is the detail form
-    of the Item Types.
-    """
-    item_types: list[ItemTypeDetail]
diff --git a/gallagher/dto/locker.py b/gallagher/dto/locker.py
deleted file mode 100644
index 33f91f06..00000000
--- a/gallagher/dto/locker.py
+++ /dev/null
@@ -1,11 +0,0 @@
-"""
-
-
-"""
-
-
-from .utils import (
-    AppBaseModel,
-    IdentityMixin,
-    HrefMixin
-)
diff --git a/gallagher/dto/operator.py b/gallagher/dto/operator.py
deleted file mode 100644
index 33f91f06..00000000
--- a/gallagher/dto/operator.py
+++ /dev/null
@@ -1,11 +0,0 @@
-"""
-
-
-"""
-
-
-from .utils import (
-    AppBaseModel,
-    IdentityMixin,
-    HrefMixin
-)
diff --git a/gallagher/dto/reception.py b/gallagher/dto/reception.py
deleted file mode 100644
index 33f91f06..00000000
--- a/gallagher/dto/reception.py
+++ /dev/null
@@ -1,11 +0,0 @@
-"""
-
-
-"""
-
-
-from .utils import (
-    AppBaseModel,
-    IdentityMixin,
-    HrefMixin
-)
diff --git a/gallagher/dto/ref/__init__.py b/gallagher/dto/ref/__init__.py
new file mode 100644
index 00000000..8530250a
--- /dev/null
+++ b/gallagher/dto/ref/__init__.py
@@ -0,0 +1,92 @@
+""" Reference classes
+
+Our design outlines three types of DTO classes, Refs are ones that are
+used to reference other objects, think of them as interlinks between
+objects. They usually contain a href and some identifying information
+such as a name or id.
+
+This package was introduced in reference to this issue 
+https://github.com/anomaly/gallagher/issues/21
+
+which identified race conditions with circular imports. This is caused
+mostly because of the nature of the data that the command centre exposes.
+"""
+
+from .access_group import (
+    AccessGroupRef
+)
+
+from .alarm import (
+    AlarmRef,
+    AlarmZoneRef,
+)
+
+from .cardholder import (
+    CardholderRef,
+    CardholderEventRef,
+)
+
+from .day_category import (
+    DayCategoryRef,
+)
+
+from .discover import (
+    FeatureAccessGroupsRef,
+    FeatureAccessZonesRef,
+    FeatureAlarmZonesRef,
+    FeatureAlarmsRef,
+    FeatureCardTypesRef,
+    FeatureCardholdersRef,
+    FeatureCompetenciesRef,
+    FeatureDayCategoriesRef,
+    FeatureDivisionsRef,
+    FeatureDoorsRef,
+    FeatureElevatorsRef,
+    FeatureEventsRef,
+    FeatureFenceZonesRef,
+    FeatureInputsRef,
+    FeatureInterlockGroupsRef,
+    FeatureItemsRef,
+    FeatureLockerBanksRef,
+    FeatureMacrosRef,
+    FeatureOperatorGroupsRef,
+    FeatureOutputsRef,
+    FeaturePersonalDataFieldsRef,
+    FeatureReceptionsRef,
+    FeatureRolesRef,
+    FeatureSchedulesRef,
+    FeatureVisitsRef,
+)
+
+from .division import (
+    DivisionRef,
+)
+
+from .door import (
+    DoorRef,
+)
+
+from .event import (
+    EventGroupRef,
+)
+
+from .items import (
+    ItemRef,
+)
+
+from .pdf import (
+    PDFRef,
+)
+
+from .salto import (
+    SaltoItemTypeRef,
+    SaltoItemRef,
+)
+
+from .schedule import (
+    ScheduleRef,
+)
+
+from .zone import (
+    AccessZoneRef,
+)
diff --git a/gallagher/dto/ref/access_group.py b/gallagher/dto/ref/access_group.py
new file mode 100644
index 00000000..b198cc62
--- /dev/null
+++ b/gallagher/dto/ref/access_group.py
@@ -0,0 +1,13 @@
+from ..utils import (
+    AppBaseModel,
+    HrefMixin
+)
+
+
+class AccessGroupRef(
+    AppBaseModel,
+    HrefMixin
+):
+    """ Access Groups is what a user is assigned to to provide access to doors
+    """
+    name: str
diff --git a/gallagher/dto/ref/alarm.py b/gallagher/dto/ref/alarm.py
new file mode 100644
index 00000000..6241f051
--- /dev/null
+++ b/gallagher/dto/ref/alarm.py
@@ -0,0 +1,22 @@
+from ..utils import (
+    AppBaseModel,
+    HrefMixin,
+)
+
+
+class AlarmRef(
+    AppBaseModel,
+    HrefMixin
+):
+    """ AlarmRef represents a single alarm
+    """
+    state: str
+
+
+class AlarmZoneRef(
+    AppBaseModel,
+    HrefMixin
+):
+    """ AccessZone represents
+    """
+    name: str
diff --git a/gallagher/dto/ref/card_type.py b/gallagher/dto/ref/card_type.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/ref/cardholder.py b/gallagher/dto/ref/cardholder.py
new file mode 100644
index 00000000..1043cf79
--- /dev/null
+++ b/gallagher/dto/ref/cardholder.py
@@ -0,0 +1,33 @@
+from typing import Optional
+
+from ..utils import (
+    AppBaseModel,
+    HrefMixin,
+    IdentityMixin,
+    OptionalHref,
+)
+
+
+class CardholderRef(
+    AppBaseModel,
+    HrefMixin,
+):
+    """ Reference to a Cardholder
+    """
+    name: str
+
+
+class CardholderEventRef(
+    AppBaseModel,
+    IdentityMixin,
+    OptionalHref,
+):
+    """ Cardholder ref used with events
+
+    Events seem to send partial information where href is missing at times
+    (this is contrary to the documentation), name and id seem to always
+    be present. This is likely because this is a summary
+    """
+    name: str
+    first_name: Optional[str] = None
+    last_name: Optional[str] = None
diff --git a/gallagher/dto/day_category.py b/gallagher/dto/ref/day_category.py
similarity index 52%
rename from gallagher/dto/day_category.py
rename to gallagher/dto/ref/day_category.py
index 0dceb5e3..9f3a4cdf 100644
--- a/gallagher/dto/day_category.py
+++ b/gallagher/dto/ref/day_category.py
@@ -3,9 +3,7 @@
 The calendar determines the days of the year that fall into a day category, 
 and the schedule determines what happens at certain times on those days.
 """
-from typing import Optional
-
-from .utils import (
+from ..utils import (
     AppBaseModel,
     HrefMixin
 )
@@ -22,24 +20,3 @@ class DayCategoryRef(
     """
 
     name: str
-
-
-class DayCategory(
-    DayCategoryRef,
-):
-    """ Represents a single entry from the response
-    """
-
-    name: str
-    description: Optional[str]
-    notes: Optional[str]
-
-
-class DayCategoryResponse(
-    AppBaseModel,
-):
-    """ The response has a list of results and a link to the next page
-    """
-
-    results: list[DayCategoryRef]
-    next: Optional[HrefMixin] = None
diff --git a/gallagher/dto/ref/discover.py b/gallagher/dto/ref/discover.py
new file mode 100644
index 00000000..194bd667
--- /dev/null
+++ b/gallagher/dto/ref/discover.py
@@ -0,0 +1,174 @@
+""" Command Centre API discovery
+
+The Command Centre API has a discovery endpoint that allows
+
+"""
+
+from typing import (
+    Optional,
+)
+
+from ..utils import (
+    AppBaseModel,
+    OptionalHref,
+)
+
+
+class FeatureAccessGroupsRef(
+    AppBaseModel,
+):
+    access_groups: Optional[OptionalHref] = OptionalHref()
+
+
+class FeatureAccessZonesRef(
+    AppBaseModel,
+):
+    access_zones: Optional[OptionalHref] = OptionalHref()
+
+
+class FeatureAlarmZonesRef(
+    AppBaseModel,
+):
+    alarm_zones: Optional[OptionalHref] = OptionalHref()
+
+
+class FeatureAlarmsRef(
+    AppBaseModel,
+):
+    alarms: Optional[OptionalHref] = OptionalHref()
+    divisions: Optional[OptionalHref] = OptionalHref()
+    updates: Optional[OptionalHref] = OptionalHref()
+
+
+class FeatureCardTypesRef(
+    AppBaseModel,
+):
+    assign: Optional[OptionalHref] = OptionalHref()
+    card_types: Optional[OptionalHref] = OptionalHref()
+
+
+class FeatureCardholdersRef(
+    AppBaseModel,
+):
+    cardholders: Optional[OptionalHref] = OptionalHref()
+    changes: Optional[OptionalHref] = OptionalHref()
+    update_location_access_zones: Optional[OptionalHref] = OptionalHref()
+
+
+class FeatureCompetenciesRef(
+    AppBaseModel,
+):
+    competencies: Optional[OptionalHref] = OptionalHref()
+
+
+class FeatureDayCategoriesRef(
+    AppBaseModel,
+):
+    day_categories: Optional[OptionalHref] = OptionalHref()
+
+
+class FeatureDivisionsRef(
+    AppBaseModel,
+):
+    divisions: Optional[OptionalHref] = OptionalHref()
+
+
+class FeatureDoorsRef(
+    AppBaseModel,
+):
+    doors: Optional[OptionalHref] = OptionalHref()
+
+
+class FeatureElevatorsRef(
+    AppBaseModel,
+):
+    elevator_groups: Optional[OptionalHref] = OptionalHref()
+
+
+class FeatureEventsRef(
+    AppBaseModel,
+):
+    divisions: Optional[OptionalHref] = OptionalHref()
+    event_groups: Optional[OptionalHref] = OptionalHref()
+    events: Optional[OptionalHref] = OptionalHref()
+    updates: Optional[OptionalHref] = OptionalHref()
+
+
+class FeatureFenceZonesRef(
+    AppBaseModel,
+):
+    fence_zones: Optional[OptionalHref] = OptionalHref()
+
+
+class FeatureInputsRef(
+    AppBaseModel,
+):
+    inputs: Optional[OptionalHref] = OptionalHref()
+
+
+class FeatureInterlockGroupsRef(
+    AppBaseModel,
+):
+    interlock_groups: Optional[OptionalHref] = OptionalHref()
+
+
+class FeatureItemsRef(
+    AppBaseModel,
+):
+    item_types: Optional[OptionalHref] = OptionalHref()
+    items: Optional[OptionalHref] = OptionalHref()
+    updates: Optional[OptionalHref] = OptionalHref()
+
+
+class FeatureLockerBanksRef(
+    AppBaseModel,
+):
+    locker_banks: Optional[OptionalHref] = OptionalHref()
+
+
+class FeatureMacrosRef(
+    AppBaseModel,
+):
+    macros: Optional[OptionalHref] = OptionalHref()
+
+
+class FeatureOperatorGroupsRef(
+    AppBaseModel,
+):
+    operator_groups: Optional[OptionalHref] = OptionalHref()
+
+
+class FeatureOutputsRef(
+    AppBaseModel,
+):
+    outputs: Optional[OptionalHref] = OptionalHref()
+
+
+class FeaturePersonalDataFieldsRef(
+    AppBaseModel,
+):
+    personal_data_fields: Optional[OptionalHref] = OptionalHref()
+
+
+class FeatureReceptionsRef(
+    AppBaseModel,
+):
+    receptions: Optional[OptionalHref] = OptionalHref()
+
+
+class FeatureRolesRef(
+    AppBaseModel,
+):
+    roles: Optional[OptionalHref] = OptionalHref()
+
+
+class FeatureSchedulesRef(
+    AppBaseModel,
+):
+    schedules: Optional[OptionalHref] = OptionalHref()
+
+
+class FeatureVisitsRef(
+    AppBaseModel,
+):
+    visits: Optional[OptionalHref] = OptionalHref()
diff --git a/gallagher/dto/ref/division.py b/gallagher/dto/ref/division.py
new file mode 100644
index 00000000..10a33d17
--- /dev/null
+++ b/gallagher/dto/ref/division.py
@@ -0,0 +1,18 @@
+from ..utils import (
+    AppBaseModel,
+    IdentityMixin,
+    HrefMixin
+)
+
+
+class DivisionRef(
+    AppBaseModel,
+    IdentityMixin,
+    HrefMixin
+):
+    """ Division reference is used to link to a division
+
+    The Mixins cover all the fields that are returned in the
+    summary, hence nothing has to be declared in the body
+    """
+    pass
diff --git a/gallagher/dto/ref/door.py b/gallagher/dto/ref/door.py
new file mode 100644
index 00000000..ad91b6d3
--- /dev/null
+++ b/gallagher/dto/ref/door.py
@@ -0,0 +1,14 @@
+from ..utils import (
+    AppBaseModel,
+    HrefMixin
+)
+
+
+class DoorRef(
+    AppBaseModel,
+    HrefMixin
+):
+    """ Door 
+
+    """
+    name: str
diff --git a/gallagher/dto/ref/event.py b/gallagher/dto/ref/event.py
new file mode 100644
index 00000000..df4312bd
--- /dev/null
+++ b/gallagher/dto/ref/event.py
@@ -0,0 +1,18 @@
+from typing import Optional
+
+from ..utils import (
+    AppBaseModel,
+    IdentityMixin,
+)
+
+
+class EventGroupRef(
+    AppBaseModel,
+    IdentityMixin,
+):
+    """ Event Group Reference
+
+    This is a reference to an event group, it is used in the
+    event type response.
+    """
+    name: str
diff --git a/gallagher/dto/ref/features.py b/gallagher/dto/ref/features.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/ref/items.py b/gallagher/dto/ref/items.py
new file mode 100644
index 00000000..78eb5186
--- /dev/null
+++ b/gallagher/dto/ref/items.py
@@ -0,0 +1,14 @@
+
+from ..utils import (
+    AppBaseModel,
+    HrefMixin
+)
+
+
+class ItemRef(
+    AppBaseModel,
+    HrefMixin
+):
+    """ Reference to an ItemType
+    """
+    name: str
diff --git a/gallagher/dto/ref/locker.py b/gallagher/dto/ref/locker.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/ref/operator.py b/gallagher/dto/ref/operator.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/ref/pdf.py b/gallagher/dto/ref/pdf.py
new file mode 100644
index 00000000..bd573baa
--- /dev/null
+++ b/gallagher/dto/ref/pdf.py
@@ -0,0 +1,29 @@
+""" Personal Data Fields
+
+Custom fields defined per command centre installation, these need
+to be discovered ahead of time before we work with them.
+
+Note:
+- Keys are prefixed with @
+- Presence of space in the field name
+"""
+from ..utils import (
+    AppBaseModel,
+    HrefMixin,
+)
+
+
+class PDFRef(
+    AppBaseModel,
+    HrefMixin
+):
+    """ Personal Data Fields are custom fields for a card holder
+
+    These are defined per command centre installation and the fields
+    are prefixed with @ e.g @Personal URL, note the presence of the space
+    in the field name.
+
+    Since they are dynamic we will have to discover the personal data
+    fields much like the URL discovery before we are able to parse the data
+    """
+    name: str
diff --git a/gallagher/dto/ref/reception.py b/gallagher/dto/ref/reception.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/ref/role.py b/gallagher/dto/ref/role.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/salto.py b/gallagher/dto/ref/salto.py
similarity index 55%
rename from gallagher/dto/salto.py
rename to gallagher/dto/ref/salto.py
index bb7633c7..76649ede 100644
--- a/gallagher/dto/salto.py
+++ b/gallagher/dto/ref/salto.py
@@ -1,14 +1,8 @@
-"""
-
-
-"""
-
-from .utils import (
+from ..utils import (
     AppBaseModel,
     HrefMixin
 )
 
-from .schedule import ScheduleRef
 
 class SaltoItemTypeRef(
     AppBaseModel,
@@ -17,6 +11,7 @@ class SaltoItemTypeRef(
     """
     value: str
 
+
 class SaltoItemRef(
     AppBaseModel,
     HrefMixin
@@ -24,13 +19,3 @@ class SaltoItemRef(
     """ A Salto Item is a particular product e.g Salto CU5000
     """
     name: str
-
-class SaltoAccessItemSummary(
-    AppBaseModel,
-):
-    """ A Summary of Salto items
-
-    """
-    salto_item_type: SaltoItemTypeRef
-    salto_item: SaltoItemRef
-    schedule: ScheduleRef
\ No newline at end of file
diff --git a/gallagher/dto/ref/schedule.py b/gallagher/dto/ref/schedule.py
new file mode 100644
index 00000000..a81daa36
--- /dev/null
+++ b/gallagher/dto/ref/schedule.py
@@ -0,0 +1,17 @@
+""" Schedules
+
+"""
+
+from ..utils import (
+    AppBaseModel,
+    HrefMixin
+)
+
+
+class ScheduleRef(
+    AppBaseModel,
+    HrefMixin
+):
+    """ Schedule is a time
+    """
+    pass
diff --git a/gallagher/dto/ref/visit.py b/gallagher/dto/ref/visit.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/ref/visitor.py b/gallagher/dto/ref/visitor.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/zone.py b/gallagher/dto/ref/zone.py
similarity index 63%
rename from gallagher/dto/zone.py
rename to gallagher/dto/ref/zone.py
index fd3f6b54..64adac8e 100644
--- a/gallagher/dto/zone.py
+++ b/gallagher/dto/ref/zone.py
@@ -1,18 +1,15 @@
-"""
-
-
-"""
-
-from .utils import (
+from ..utils import (
     AppBaseModel,
-    HrefMixin
+    IdentityMixin,
+    HrefMixin,
 )
 
+
 class AccessZoneRef(
     AppBaseModel,
+    IdentityMixin,
     HrefMixin
 ):
     """ AccessZone represents
     """
     name: str
-
diff --git a/gallagher/dto/response/__init__.py b/gallagher/dto/response/__init__.py
new file mode 100644
index 00000000..e9fed1fc
--- /dev/null
+++ b/gallagher/dto/response/__init__.py
@@ -0,0 +1,50 @@
+"""
+
+
+"""
+
+from .alarm import (
+    AlarmResponse,
+)
+
+from .card_type import (
+    CardTypeResponse,
+)
+
+from .cardholder import (
+    CardholderSummaryResponse,
+)
+
+from .day_category import (
+    DayCategoryResponse,
+)
+
+from .discover import (
+    DiscoveryResponse,
+)
+
+from .division import (
+    DivisionDetailResponse,
+)
+
+from .door import (
+    DoorSummaryResponse,
+)
+
+from .event import (
+    EventSummaryResponse,
+    EventTypeResponse,
+)
+
+from .pdf import (
+    PdfResponse,
+)
+
+from .items import (
+    ItemsSummaryResponse,
+    ItemTypesResponse,
+)
+
+from .schedule import (
+    ScheduleSummaryResponse,
+)
diff --git a/gallagher/dto/response/access_group.py b/gallagher/dto/response/access_group.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/response/alarm.py b/gallagher/dto/response/alarm.py
new file mode 100644
index 00000000..2afc3424
--- /dev/null
+++ b/gallagher/dto/response/alarm.py
@@ -0,0 +1,17 @@
+from ..utils import (
+    AppBaseModel,
+    HrefMixin,
+)
+
+from ..summary import (
+    AlarmZoneSummary,
+)
+
+
+class AlarmResponse(
+    AppBaseModel,
+):
+    """ AlarmResponse represents a single alarm
+    """
+    alarms: list[AlarmZoneSummary]
+    updates: HrefMixin
diff --git a/gallagher/dto/response/card_type.py b/gallagher/dto/response/card_type.py
new file mode 100644
index 00000000..482978c7
--- /dev/null
+++ b/gallagher/dto/response/card_type.py
@@ -0,0 +1,19 @@
+from typing import Optional
+
+from ..utils import (
+    AppBaseModel,
+    HrefMixin
+)
+
+from ..detail import (
+    CardTypeDetail
+)
+
+
+class CardTypeResponse(
+    AppBaseModel,
+):
+    """ Card Types are cards mobile or physical that are supported at a site
+    """
+    results: list[CardTypeDetail]
+    next: Optional[HrefMixin] = None
diff --git a/gallagher/dto/response/cardholder.py b/gallagher/dto/response/cardholder.py
new file mode 100644
index 00000000..41c2f2f5
--- /dev/null
+++ b/gallagher/dto/response/cardholder.py
@@ -0,0 +1,37 @@
+"""
+
+"""
+from ..utils import (
+    AppBaseModel,
+)
+
+from ..summary import (
+    CardholderSummary
+)
+
+
+class CardholderSummaryResponse(
+    AppBaseModel
+):
+    """ Summary response for cardholder list and search
+
+    /api/cardholders is generally the endpoint that responds
+    to the query, it is dynamically configured from the discovery
+
+    """
+    results: list[CardholderSummary]
+
+    @property
+    def cli_header(self):
+        return [
+            "Id",
+            "First name",
+            "Last name",
+            "Authorised"
+        ]
+
+    def __rich_repr__(self):
+        return [r.__rich_repr__() for r in self.results]
+
+    def __str__(self):
+        return f"{len(self.results)} cardholders"
diff --git a/gallagher/dto/response/day_category.py b/gallagher/dto/response/day_category.py
new file mode 100644
index 00000000..0fa10138
--- /dev/null
+++ b/gallagher/dto/response/day_category.py
@@ -0,0 +1,20 @@
+from typing import Optional
+
+from ..utils import (
+    AppBaseModel,
+    HrefMixin
+)
+
+from ..ref import (
+    DayCategoryRef,
+)
+
+
+class DayCategoryResponse(
+    AppBaseModel,
+):
+    """ The response has a list of results and a link to the next page
+    """
+
+    results: list[DayCategoryRef]
+    next: Optional[HrefMixin] = None
diff --git a/gallagher/dto/response/discover.py b/gallagher/dto/response/discover.py
new file mode 100644
index 00000000..a42be437
--- /dev/null
+++ b/gallagher/dto/response/discover.py
@@ -0,0 +1,40 @@
+from typing import (
+    Annotated,
+)
+
+from ..utils import (
+    AppBaseModel,
+)
+
+from ..detail import (
+    FeaturesDetail,
+)
+
+
+class DiscoveryResponse(
+    AppBaseModel,
+):
+    """ A response that outlines the capability of the server
+
+    Gallagher requires customers to license individual features, if they are
+    the server will return a 403 HTTP code. The purpose of this model is to
+    discover what features are available on the server.
+
+    The response should be memoized as it is unlikely to change during individual
+    sessions, they can however change over a period of time.
+
+    This API client is updated to work with various versions of the server, the
+    server responds with a version string that can be used to determine if
+    the API client can work with the server.  
+    """
+
+    version: Annotated[str, "The version of the server"] = "0.0.0"
+    features: Annotated[FeaturesDetail,
+                        "A list of features available on the server"
+                        ] = FeaturesDetail()
+
+    @property
+    async def get_sem_ver(self):
+        """ Get a SemVer tuple from the version string
+        """
+        return self.version.split(".")
diff --git a/gallagher/dto/response/division.py b/gallagher/dto/response/division.py
new file mode 100644
index 00000000..37eec892
--- /dev/null
+++ b/gallagher/dto/response/division.py
@@ -0,0 +1,21 @@
+from typing import Optional
+
+from ..utils import (
+    AppBaseModel,
+    HrefMixin,
+)
+
+from ..detail import (
+    DivisionDetail
+)
+
+
+class DivisionDetailResponse(
+    AppBaseModel
+):
+    """ Division
+
+    """
+
+    results: list[DivisionDetail]
+    next: Optional[HrefMixin] = None
diff --git a/gallagher/dto/response/door.py b/gallagher/dto/response/door.py
new file mode 100644
index 00000000..da5b906f
--- /dev/null
+++ b/gallagher/dto/response/door.py
@@ -0,0 +1,16 @@
+from ..utils import (
+    AppBaseModel,
+)
+
+from ..summary import (
+    DoorSummary,
+)
+
+
+class DoorSummaryResponse(
+    AppBaseModel
+):
+    """
+
+    """
+    results: list[DoorSummary]
diff --git a/gallagher/dto/response/event.py b/gallagher/dto/response/event.py
new file mode 100644
index 00000000..9ac45a93
--- /dev/null
+++ b/gallagher/dto/response/event.py
@@ -0,0 +1,35 @@
+from typing import Optional
+
+from ..utils import (
+    AppBaseModel,
+    HrefMixin,
+    IdentityMixin,
+)
+
+from ..summary import (
+    EventGroupSummary,
+    EventSummary,
+)
+
+
+class EventSummaryResponse(
+    AppBaseModel,
+):
+
+    events: list[EventSummary]
+
+    next: HrefMixin
+    previous: HrefMixin
+    updates: HrefMixin
+
+
+class EventTypeResponse(
+    AppBaseModel,
+):
+    """ Event Type Response
+
+    Event Type Responses return a set of eventGroups which in turn
+    has identifiers, names and event types.
+
+    """
+    event_groups: list[EventGroupSummary]
diff --git a/gallagher/dto/response/features.py b/gallagher/dto/response/features.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/response/items.py b/gallagher/dto/response/items.py
new file mode 100644
index 00000000..159cd7d8
--- /dev/null
+++ b/gallagher/dto/response/items.py
@@ -0,0 +1,32 @@
+from typing import Optional
+
+from ..utils import (
+    AppBaseModel,
+    HrefMixin
+)
+
+from ..summary import (
+    ItemSummary,
+    ItemTypeSummary
+)
+
+
+class ItemsSummaryResponse(
+    AppBaseModel,
+):
+    """  ItemsResponse is the list of items from the API
+    it provides the summary of all Items Summary
+    """
+    results: list[ItemSummary]
+    next: Optional[HrefMixin] = None
+
+
+class ItemTypesResponse(
+    AppBaseModel,
+):
+    """ Every security centre can provide a list of item types
+
+    While the response is rather abridged, this is the detail form
+    of the Item Types.
+    """
+    item_types: list[ItemTypeSummary]
diff --git a/gallagher/dto/response/locker.py b/gallagher/dto/response/locker.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/response/operator.py b/gallagher/dto/response/operator.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/response/pdf.py b/gallagher/dto/response/pdf.py
new file mode 100644
index 00000000..ebf64486
--- /dev/null
+++ b/gallagher/dto/response/pdf.py
@@ -0,0 +1,29 @@
+""" Personal Data Fields
+
+Dynamically occurring fields are defined per site and are
+prefixed by an @ symbol as the key of the field, as they
+appear in the API responses.
+
+These fields appear either under the personalDataFields as
+href references or as individual fields in the user's profile.
+
+"""
+from ..utils import (
+    AppBaseModel,
+)
+
+from ..ref import (
+    PDFRef,
+)
+
+
+class PdfResponse(
+    AppBaseModel,
+):
+    """ Personal Definition fields
+
+    Returned as a set of results, each result is a PDFRef
+    these are to be cached just as we do the URL endpoints,
+    note that this must be done after the discovery completes
+    """
+    results: list[PDFRef]
diff --git a/gallagher/dto/response/reception.py b/gallagher/dto/response/reception.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/response/role.py b/gallagher/dto/response/role.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/response/salto.py b/gallagher/dto/response/salto.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/response/schedule.py b/gallagher/dto/response/schedule.py
new file mode 100644
index 00000000..3c0479fe
--- /dev/null
+++ b/gallagher/dto/response/schedule.py
@@ -0,0 +1,15 @@
+from ..utils import (
+    AppBaseModel,
+)
+
+from ..summary import (
+    ScheduleSummary
+)
+
+
+class ScheduleSummaryResponse(
+    AppBaseModel
+):
+    """ Schedule is a time
+    """
+    results: list[ScheduleSummary]
diff --git a/gallagher/dto/response/visit.py b/gallagher/dto/response/visit.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/response/visitor.py b/gallagher/dto/response/visitor.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/response/zone.py b/gallagher/dto/response/zone.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/schedule.py b/gallagher/dto/schedule.py
deleted file mode 100644
index 5f8263c4..00000000
--- a/gallagher/dto/schedule.py
+++ /dev/null
@@ -1,32 +0,0 @@
-""" Schedules
-
-"""
-
-from .utils import (
-    AppBaseModel,
-    HrefMixin
-)
-
-class ScheduleRef(
-    AppBaseModel,
-    HrefMixin
-):
-    """ Schedule is a time
-    """
-    pass
-
-
-class ScheduleSummary(
-    ScheduleRef
-):
-    """ Schedule is a time
-    """
-    name: str
-
-
-class ScheduleSummaryResponse(
-    AppBaseModel
-):
-    """ Schedule is a time
-    """
-    results: list[ScheduleSummary]
diff --git a/gallagher/dto/summary/__init__.py b/gallagher/dto/summary/__init__.py
new file mode 100644
index 00000000..12b0795d
--- /dev/null
+++ b/gallagher/dto/summary/__init__.py
@@ -0,0 +1,55 @@
+"""
+
+
+"""
+
+from .access_group import (
+    AccessSummary,
+    AccessGroupSummary
+)
+
+from .alarm import (
+    AlarmSourceSummary,
+    AlarmZoneSummary,
+)
+
+from .card_type import (
+    CardExpiryTypeSummary,
+    CardSummary,
+)
+
+from .cardholder import (
+    CardholderSummary,
+)
+
+from .day_category import (
+    DayCategorySummary,
+)
+
+from .door import (
+    DoorSummary,
+)
+
+from .event import (
+    EventSummary,
+    EventGroupSummary,
+    EventTypeSummary,
+)
+
+from .items import (
+    ItemTypeSummary,
+    ItemSummary,
+)
+
+from .salto import (
+    SaltoAccessItemSummary,
+)
+
+from .schedule import (
+    ScheduleSummary,
+)
+
+from .visitor import (
+    VisitorManagementSummary,
+    VisitorTypeSummary,
+)
diff --git a/gallagher/dto/summary/access_group.py b/gallagher/dto/summary/access_group.py
new file mode 100644
index 00000000..004c6e32
--- /dev/null
+++ b/gallagher/dto/summary/access_group.py
@@ -0,0 +1,40 @@
+from typing import Optional
+
+from ..utils import (
+    AppBaseModel,
+    IdentityMixin,
+    HrefMixin
+)
+
+from ..ref import (
+    AccessGroupRef,
+    AccessZoneRef,
+    ScheduleRef,
+)
+
+
+class AccessSummary(
+    AppBaseModel,
+    HrefMixin,
+):
+    """ Access is zone paired with a schedule
+    """
+    access_zone: AccessZoneRef
+    schedule: ScheduleRef
+
+
+class AccessGroupSummary(
+    AppBaseModel,
+):
+    """ AccessGroup Summary is what the API returns on searches
+
+    This builds on the Ref class to add the summary fields and is
+    extended by the Detail class to add the fully remainder of
+    the fields
+    """
+    name: str
+    description: Optional[str]
+    parent: Optional[AccessGroupRef]
+    division: IdentityMixin
+    cardholders: Optional[HrefMixin]
+    server_display_name: Optional[str]
diff --git a/gallagher/dto/summary/alarm.py b/gallagher/dto/summary/alarm.py
new file mode 100644
index 00000000..90226578
--- /dev/null
+++ b/gallagher/dto/summary/alarm.py
@@ -0,0 +1,53 @@
+from typing import Optional
+from datetime import datetime
+
+from ..utils import (
+    AppBaseModel,
+    HrefMixin,
+    IdentityMixin,
+)
+
+from .event import (
+    EventTypeSummary
+)
+
+
+class AlarmSourceSummary(
+    AppBaseModel,
+    HrefMixin,
+    IdentityMixin,
+):
+    """ AlarmSource represents a device that has triggered an alarm
+    """
+    name: str
+
+
+class AlarmZoneSummary(
+    AppBaseModel,
+    HrefMixin,
+    IdentityMixin,
+):
+    """ #TODO: Revise this if it shows up in other places
+
+    I have literally named this class to model the alarm_zones
+    property in the access_group schema. I don't know if this
+    is appropriate
+    """
+    time: datetime
+    message: str
+    source: AlarmSourceSummary
+    type: str
+    event_type: Optional[EventTypeSummary] = None
+    priority: int
+    state: str
+    active: bool
+    division: HrefMixin
+    event: Optional[HrefMixin] = None
+    note_presets: list[str] = []
+    view: HrefMixin
+    comment: HrefMixin
+    acknowledge: Optional[HrefMixin] = None
+    acknowledge_with_comment: Optional[HrefMixin] = None
+    process: Optional[HrefMixin] = None
+    process_with_comment: Optional[HrefMixin] = None
+    force_process: Optional[HrefMixin] = None
diff --git a/gallagher/dto/summary/card_type.py b/gallagher/dto/summary/card_type.py
new file mode 100644
index 00000000..67ef118c
--- /dev/null
+++ b/gallagher/dto/summary/card_type.py
@@ -0,0 +1,26 @@
+from typing import Optional
+
+from ..utils import (
+    AppBaseModel,
+    IdentityMixin,
+)
+
+
+class CardExpiryTypeSummary(
+    AppBaseModel
+):
+    expiry_type: Optional[str] = None
+
+
+class CardSummary(
+    AppBaseModel,
+    IdentityMixin,
+):
+    """ Card summary as sent by the Event objects
+
+    Note: that we should revise this if required
+
+    """
+    facility_code: str
+    number: str
+    issue_level: int
diff --git a/gallagher/dto/summary/cardholder.py b/gallagher/dto/summary/cardholder.py
new file mode 100644
index 00000000..ed041010
--- /dev/null
+++ b/gallagher/dto/summary/cardholder.py
@@ -0,0 +1,38 @@
+from typing import Optional
+
+from ..utils import (
+    AppBaseModel,
+    IdentityMixin,
+    HrefMixin,
+)
+
+
+class CardholderSummary(
+    AppBaseModel,
+    IdentityMixin,
+    HrefMixin,
+):
+    """
+    The cardholder search at /api/cardholders returns an array of these. 
+    It is a subset of what you get from a cardholder's detail page at 
+    /api/cardholders/{id} 
+
+    (linked as the href in this object), to be more suitable for large result sets.
+    """
+
+    first_name: str
+    last_name: str
+    short_name: Optional[str] = None
+    description: Optional[str] = None
+    authorised: bool = False
+
+    def __rich_repr__(self):
+        return [
+            self.id,
+            self.first_name,
+            self.last_name,
+            "yes" if self.authorised else "no"
+        ]
+
+    def __str__(self):
+        return f"{self.id} {self.first_name} {self.last_name}"
diff --git a/gallagher/dto/summary/day_category.py b/gallagher/dto/summary/day_category.py
new file mode 100644
index 00000000..7747d2c7
--- /dev/null
+++ b/gallagher/dto/summary/day_category.py
@@ -0,0 +1,23 @@
+""" A day category links a calendar to a schedule. 
+
+The calendar determines the days of the year that fall into a day category, 
+and the schedule determines what happens at certain times on those days.
+"""
+from typing import Optional
+
+from ..utils import (
+    AppBaseModel,
+    HrefMixin
+)
+
+
+class DayCategorySummary(
+    AppBaseModel,
+    HrefMixin,
+):
+    """ Represents a single entry from the response
+    """
+
+    name: str
+    description: Optional[str]
+    notes: Optional[str]
diff --git a/gallagher/dto/summary/division.py b/gallagher/dto/summary/division.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/summary/door.py b/gallagher/dto/summary/door.py
new file mode 100644
index 00000000..4037180e
--- /dev/null
+++ b/gallagher/dto/summary/door.py
@@ -0,0 +1,16 @@
+from ..utils import (
+    AppBaseModel,
+    IdentityMixin,
+    HrefMixin
+)
+
+
+class DoorSummary(
+    AppBaseModel,
+    IdentityMixin,
+    HrefMixin,
+):
+    """
+
+    """
+    name: str
diff --git a/gallagher/dto/summary/event.py b/gallagher/dto/summary/event.py
new file mode 100644
index 00000000..775fec9d
--- /dev/null
+++ b/gallagher/dto/summary/event.py
@@ -0,0 +1,79 @@
+from typing import Optional
+from datetime import datetime
+
+from ..utils import (
+    AppBaseModel,
+    HrefMixin,
+    IdentityMixin,
+    OptionalHref,
+)
+
+from ..ref import (
+    AlarmRef,
+    CardholderRef,
+    CardholderEventRef,
+    DoorRef,
+    AccessZoneRef,
+    DivisionRef,
+    ItemRef,
+    EventGroupRef,
+)
+
+from .card_type import (
+    CardSummary,
+)
+
+from .items import (
+    ItemSummary,
+)
+
+
+class EventTypeSummary(
+    AppBaseModel,
+    IdentityMixin,
+):
+    """ An event type has identifiers and names
+    """
+    name: str
+
+
+class EventGroupSummary(
+    AppBaseModel,
+    IdentityMixin,
+):
+    """ Event Groups are a collection of event types
+
+    Each group has names and event types. This is usually used
+    in an Event Type Response.
+    """
+    name: str
+    event_types: list[EventTypeSummary]
+
+
+class EventSummary(
+    AppBaseModel,
+    HrefMixin,
+    IdentityMixin,
+):
+    """ Summary of events that have occurred on the server
+    """
+    server_display_name: Optional[str] = None
+    time: datetime
+    message: Optional[str] = None
+    occurrences: Optional[int] = 0
+    priority: int
+    alarm: Optional[AlarmRef] = None
+
+    operator: Optional[CardholderRef] = None
+    source: ItemRef
+    group: Optional[EventGroupRef] = None
+    type: Optional[EventTypeSummary] = None
+    event_type: Optional[EventTypeSummary] = None
+    division: Optional[DivisionRef] = None
+    cardholder: Optional[CardholderEventRef] = None
+    entry_access_zone: Optional[AccessZoneRef] = None
+    exit_access_zone: Optional[AccessZoneRef] = None
+    door: Optional[DoorRef] = None
+    access_group: OptionalHref = None
+    card: Optional[CardSummary] = None
+    modified_item: Optional[ItemSummary] = None
diff --git a/gallagher/dto/summary/features.py b/gallagher/dto/summary/features.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/summary/items.py b/gallagher/dto/summary/items.py
new file mode 100644
index 00000000..593390e1
--- /dev/null
+++ b/gallagher/dto/summary/items.py
@@ -0,0 +1,32 @@
+from typing import Optional
+
+from ..utils import (
+    AppBaseModel,
+    IdentityMixin,
+    HrefMixin
+)
+
+
+class ItemTypeSummary(
+    AppBaseModel,
+):
+    """ Items Types only provide the name and id
+
+    This is used by the discovery endpoint and is used by
+    the ItemTypesResponse
+    """
+    id: str
+    name: str
+    canonical_type_name: Optional[str] = None
+
+
+class ItemSummary(
+    AppBaseModel,
+    HrefMixin,
+    IdentityMixin,
+):
+    """ Summary of an Item which adds the notes and 
+    server_display_name, this is used by the item summary response
+    """
+    name: Optional[str] = None
+    type: ItemTypeSummary
diff --git a/gallagher/dto/summary/locker.py b/gallagher/dto/summary/locker.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/summary/operator.py b/gallagher/dto/summary/operator.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/summary/pdf.py b/gallagher/dto/summary/pdf.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/summary/reception.py b/gallagher/dto/summary/reception.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/summary/role.py b/gallagher/dto/summary/role.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/summary/salto.py b/gallagher/dto/summary/salto.py
new file mode 100644
index 00000000..4e204e62
--- /dev/null
+++ b/gallagher/dto/summary/salto.py
@@ -0,0 +1,20 @@
+from ..utils import (
+    AppBaseModel,
+)
+
+from ..ref import (
+    ScheduleRef,
+    SaltoItemRef,
+    SaltoItemTypeRef,
+)
+
+
+class SaltoAccessItemSummary(
+    AppBaseModel,
+):
+    """ A Summary of Salto items
+
+    """
+    salto_item_type: SaltoItemTypeRef
+    salto_item: SaltoItemRef
+    schedule: ScheduleRef
diff --git a/gallagher/dto/summary/schedule.py b/gallagher/dto/summary/schedule.py
new file mode 100644
index 00000000..eef9257e
--- /dev/null
+++ b/gallagher/dto/summary/schedule.py
@@ -0,0 +1,13 @@
+from ..utils import (
+    AppBaseModel,
+    HrefMixin
+)
+
+
+class ScheduleSummary(
+    AppBaseModel,
+    HrefMixin
+):
+    """ Schedule is a time
+    """
+    name: str
diff --git a/gallagher/dto/summary/visit.py b/gallagher/dto/summary/visit.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/visitor.py b/gallagher/dto/summary/visitor.py
similarity index 83%
rename from gallagher/dto/visitor.py
rename to gallagher/dto/summary/visitor.py
index 040507ba..215e8229 100644
--- a/gallagher/dto/visitor.py
+++ b/gallagher/dto/summary/visitor.py
@@ -1,14 +1,17 @@
-
-
-from .utils import (
+from ..utils import (
     AppBaseModel,
     IdentityMixin
 )
 
+from ..ref import (
+    AccessGroupRef
+)
+
 from .access_group import (
-    AccessGroupRef,
-    AccessGroupSummary
+    AccessGroupSummary,
 )
+
+
 class VisitorTypeSummary(
     AppBaseModel,
     IdentityMixin
@@ -20,10 +23,11 @@ class VisitorTypeSummary(
 
     these are represented in Divisions
     """
-    access_group : AccessGroupRef
+    access_group: AccessGroupRef
     host_access_groups: list[AccessGroupSummary]
     visitor_access_groups: list[AccessGroupSummary]
 
+
 class VisitorManagementSummary(
     AppBaseModel
 ):
diff --git a/gallagher/dto/summary/zone.py b/gallagher/dto/summary/zone.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gallagher/dto/utils.py b/gallagher/dto/utils.py
index 811e47d0..7eac5472 100644
--- a/gallagher/dto/utils.py
+++ b/gallagher/dto/utils.py
@@ -3,6 +3,10 @@
 
 """
 
+from typing import (
+    Optional,
+)
+from datetime import datetime
 
 from pydantic import (
     BaseModel,
@@ -37,6 +41,22 @@ class AppBaseModel(BaseModel):
         alias_generator=to_lower_camel,
     )
 
+    # Set to the last time each response was retrieved
+    # If it's set to None then the response was either created
+    # by the API client or it wasn't retrieved from the server
+    #
+    # This is generally used for caching
+    good_known_since: Optional[datetime] = None
+
+    def model_post_init(self, __context) -> None:
+        """
+        The model_post_init method is called after the model is
+        initialized, this is used to set the good_known_since
+
+        https://docs.pydantic.dev/2.0/api/main/#pydantic.main.BaseModel.model_post_init
+        """
+        self.good_known_since = datetime.now()
+
 
 class IdentityMixin(BaseModel):
     """ Identifier 
@@ -54,3 +74,25 @@ class HrefMixin(BaseModel):
     responses from the Gallagher API.
     """
     href: str
+
+
+class OptionalHref(BaseModel):
+    """ Optionally available Href
+
+    This mixin is used to define the href field for all
+    responses from the Gallagher API.
+
+    Primarily used by the discovery endpoint, where the href
+    may be absent if the feature is not available.
+
+    Reason for this so the API Endpoint configuration can 
+    reference the href property (pre discovery), otherwise
+    the Feature* classes have a None object for the object
+
+    # Use with caution
+
+    Only use these with responses that don't optionally
+    require a href. See Gallagher's documentation for
+    confirmation.
+    """
+    href: Optional[str] = None
diff --git a/gallagher/dto/visit.py b/gallagher/dto/visit.py
deleted file mode 100644
index 52f0db0a..00000000
--- a/gallagher/dto/visit.py
+++ /dev/null
@@ -1,10 +0,0 @@
-"""
-
-
-"""
-
-
-from .utils import (
-    AppBaseModel,
-    HrefMixin
-)
diff --git a/gallagher/enum.py b/gallagher/enum.py
new file mode 100644
index 00000000..0ebf54c3
--- /dev/null
+++ b/gallagher/enum.py
@@ -0,0 +1,20 @@
+""" Contains Enumerations for the Gallagher objects
+
+Our naming convention is ObjectIntent
+
+CustomerSearch
+
+"""
+from enum import Enum
+
+
+class CustomerSort(Enum):
+    """ Sort descriptors for the Customer object
+
+    """
+
+    ID: str = "id"
+    ID_DESC: str = "-id"
+
+    NAME: str = "name"
+    NAME_DESC: str = "-name"
diff --git a/gallagher/exception.py b/gallagher/exception.py
index cbcbc3a7..84efab4d 100644
--- a/gallagher/exception.py
+++ b/gallagher/exception.py
@@ -6,3 +6,13 @@
 
 Everything declared here indicates an implementation error.
 """
+
+
+class UnlicensedFeatureException(Exception):
+    """ Raised when a feature is not licensed
+
+    This exception is raised when the client attempts to access
+    an endpoint that is not licensed by the server.
+
+    """
+    pass
diff --git a/gallagher/tui/__init__.py b/gallagher/tui/__init__.py
new file mode 100644
index 00000000..e650b746
--- /dev/null
+++ b/gallagher/tui/__init__.py
@@ -0,0 +1,60 @@
+""" Console
+"""
+
+from textual.app import (
+    App,
+    ComposeResult
+)
+
+from textual.containers import (
+    Container,
+    Horizontal,
+    Grid,
+)
+
+from textual.widgets import (
+    Header,
+    Footer,
+    Placeholder,
+)
+
+from .dashboard import Dashboard
+
+
+class GallagherConsole(App):
+    """ A console interface for the Gallagher Command Centre.
+    """
+
+    CSS_PATH = "gallagher.tcss"
+
+    BINDINGS = [
+        ("d", "toggle_dark", "Toggle dark mode"),
+        ("ctrl+q", "quit", "Quit")
+    ]
+
+    # Decorative constants
+    TITLE = "Gallagher"
+    SUB_TITLE = "power tools for the console"
+
+    # def on_mount(self) -> None:
+
+    def compose(self) -> ComposeResult:
+        """ Create child widgets for the app.
+        """
+        yield Header()
+        yield Dashboard()
+        yield Footer()
+
+    def action_toggle_dark(self) -> None:
+        """ An action to toggle dark mode.
+        """
+        self.dark = not self.dark
+
+
+def main():
+    app = GallagherConsole()
+    app.run()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/gallagher/tui/dashboard.py b/gallagher/tui/dashboard.py
new file mode 100644
index 00000000..bde4eab4
--- /dev/null
+++ b/gallagher/tui/dashboard.py
@@ -0,0 +1,29 @@
+""" Dashboard
+
+Where you land up once the tui is started via the command line
+this shows you some statistics, and health information.
+"""
+
+from textual.app import (
+    ComposeResult
+)
+
+from textual.containers import (
+    Container,
+    Grid,
+)
+
+from textual.widgets import (
+    Digits,
+    Placeholder,
+)
+
+
+class Dashboard(Container):
+
+    def compose(self) -> ComposeResult:
+        """ Create child widgets for the app.
+        """
+        with Grid():
+            yield Placeholder("Dashboard")
+            yield Digits("3.141,592,653,5897", id="pi")
diff --git a/gallagher/tui/gallagher.tcss b/gallagher/tui/gallagher.tcss
new file mode 100644
index 00000000..7c5c2f0c
--- /dev/null
+++ b/gallagher/tui/gallagher.tcss
@@ -0,0 +1,6 @@
+Grid {
+    grid-size: 3 3;
+    layout: grid;
+}
+#a {
+}
\ No newline at end of file
diff --git a/gallagher/tui/overview.py b/gallagher/tui/overview.py
new file mode 100644
index 00000000..747eea34
--- /dev/null
+++ b/gallagher/tui/overview.py
@@ -0,0 +1,11 @@
+"""
+
+
+"""
+from textual.containers import (
+    Container,
+)
+
+
+class Dashboard(Container):
+    pass
diff --git a/poetry.lock b/poetry.lock
index 212c560e..159a8278 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,132 @@
-# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.1 and should not be changed by hand.
+
+[[package]]
+name = "aiohttp"
+version = "3.9.3"
+description = "Async http client/server framework (asyncio)"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:939677b61f9d72a4fa2a042a5eee2a99a24001a67c13da113b2e30396567db54"},
+    {file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1f5cd333fcf7590a18334c90f8c9147c837a6ec8a178e88d90a9b96ea03194cc"},
+    {file = "aiohttp-3.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:82e6aa28dd46374f72093eda8bcd142f7771ee1eb9d1e223ff0fa7177a96b4a5"},
+    {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f56455b0c2c7cc3b0c584815264461d07b177f903a04481dfc33e08a89f0c26b"},
+    {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bca77a198bb6e69795ef2f09a5f4c12758487f83f33d63acde5f0d4919815768"},
+    {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e083c285857b78ee21a96ba1eb1b5339733c3563f72980728ca2b08b53826ca5"},
+    {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab40e6251c3873d86ea9b30a1ac6d7478c09277b32e14745d0d3c6e76e3c7e29"},
+    {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df822ee7feaaeffb99c1a9e5e608800bd8eda6e5f18f5cfb0dc7eeb2eaa6bbec"},
+    {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:acef0899fea7492145d2bbaaaec7b345c87753168589cc7faf0afec9afe9b747"},
+    {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cd73265a9e5ea618014802ab01babf1940cecb90c9762d8b9e7d2cc1e1969ec6"},
+    {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a78ed8a53a1221393d9637c01870248a6f4ea5b214a59a92a36f18151739452c"},
+    {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:6b0e029353361f1746bac2e4cc19b32f972ec03f0f943b390c4ab3371840aabf"},
+    {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7cf5c9458e1e90e3c390c2639f1017a0379a99a94fdfad3a1fd966a2874bba52"},
+    {file = "aiohttp-3.9.3-cp310-cp310-win32.whl", hash = "sha256:3e59c23c52765951b69ec45ddbbc9403a8761ee6f57253250c6e1536cacc758b"},
+    {file = "aiohttp-3.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:055ce4f74b82551678291473f66dc9fb9048a50d8324278751926ff0ae7715e5"},
+    {file = "aiohttp-3.9.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6b88f9386ff1ad91ace19d2a1c0225896e28815ee09fc6a8932fded8cda97c3d"},
+    {file = "aiohttp-3.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c46956ed82961e31557b6857a5ca153c67e5476972e5f7190015018760938da2"},
+    {file = "aiohttp-3.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07b837ef0d2f252f96009e9b8435ec1fef68ef8b1461933253d318748ec1acdc"},
+    {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad46e6f620574b3b4801c68255492e0159d1712271cc99d8bdf35f2043ec266"},
+    {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ed3e046ea7b14938112ccd53d91c1539af3e6679b222f9469981e3dac7ba1ce"},
+    {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:039df344b45ae0b34ac885ab5b53940b174530d4dd8a14ed8b0e2155b9dddccb"},
+    {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7943c414d3a8d9235f5f15c22ace69787c140c80b718dcd57caaade95f7cd93b"},
+    {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84871a243359bb42c12728f04d181a389718710129b36b6aad0fc4655a7647d4"},
+    {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5eafe2c065df5401ba06821b9a054d9cb2848867f3c59801b5d07a0be3a380ae"},
+    {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9d3c9b50f19704552f23b4eaea1fc082fdd82c63429a6506446cbd8737823da3"},
+    {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:f033d80bc6283092613882dfe40419c6a6a1527e04fc69350e87a9df02bbc283"},
+    {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2c895a656dd7e061b2fd6bb77d971cc38f2afc277229ce7dd3552de8313a483e"},
+    {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1f5a71d25cd8106eab05f8704cd9167b6e5187bcdf8f090a66c6d88b634802b4"},
+    {file = "aiohttp-3.9.3-cp311-cp311-win32.whl", hash = "sha256:50fca156d718f8ced687a373f9e140c1bb765ca16e3d6f4fe116e3df7c05b2c5"},
+    {file = "aiohttp-3.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:5fe9ce6c09668063b8447f85d43b8d1c4e5d3d7e92c63173e6180b2ac5d46dd8"},
+    {file = "aiohttp-3.9.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:38a19bc3b686ad55804ae931012f78f7a534cce165d089a2059f658f6c91fa60"},
+    {file = "aiohttp-3.9.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:770d015888c2a598b377bd2f663adfd947d78c0124cfe7b959e1ef39f5b13869"},
+    {file = "aiohttp-3.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee43080e75fc92bf36219926c8e6de497f9b247301bbf88c5c7593d931426679"},
+    {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52df73f14ed99cee84865b95a3d9e044f226320a87af208f068ecc33e0c35b96"},
+    {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc9b311743a78043b26ffaeeb9715dc360335e5517832f5a8e339f8a43581e4d"},
+    {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b955ed993491f1a5da7f92e98d5dad3c1e14dc175f74517c4e610b1f2456fb11"},
+    {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:504b6981675ace64c28bf4a05a508af5cde526e36492c98916127f5a02354d53"},
+    {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6fe5571784af92b6bc2fda8d1925cccdf24642d49546d3144948a6a1ed58ca5"},
+    {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ba39e9c8627edc56544c8628cc180d88605df3892beeb2b94c9bc857774848ca"},
+    {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e5e46b578c0e9db71d04c4b506a2121c0cb371dd89af17a0586ff6769d4c58c1"},
+    {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:938a9653e1e0c592053f815f7028e41a3062e902095e5a7dc84617c87267ebd5"},
+    {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:c3452ea726c76e92f3b9fae4b34a151981a9ec0a4847a627c43d71a15ac32aa6"},
+    {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff30218887e62209942f91ac1be902cc80cddb86bf00fbc6783b7a43b2bea26f"},
+    {file = "aiohttp-3.9.3-cp312-cp312-win32.whl", hash = "sha256:38f307b41e0bea3294a9a2a87833191e4bcf89bb0365e83a8be3a58b31fb7f38"},
+    {file = "aiohttp-3.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:b791a3143681a520c0a17e26ae7465f1b6f99461a28019d1a2f425236e6eedb5"},
+    {file = "aiohttp-3.9.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ed621426d961df79aa3b963ac7af0d40392956ffa9be022024cd16297b30c8c"},
+    {file = "aiohttp-3.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7f46acd6a194287b7e41e87957bfe2ad1ad88318d447caf5b090012f2c5bb528"},
+    {file = "aiohttp-3.9.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feeb18a801aacb098220e2c3eea59a512362eb408d4afd0c242044c33ad6d542"},
+    {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f734e38fd8666f53da904c52a23ce517f1b07722118d750405af7e4123933511"},
+    {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b40670ec7e2156d8e57f70aec34a7216407848dfe6c693ef131ddf6e76feb672"},
+    {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fdd215b7b7fd4a53994f238d0f46b7ba4ac4c0adb12452beee724ddd0743ae5d"},
+    {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:017a21b0df49039c8f46ca0971b3a7fdc1f56741ab1240cb90ca408049766168"},
+    {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e99abf0bba688259a496f966211c49a514e65afa9b3073a1fcee08856e04425b"},
+    {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:648056db9a9fa565d3fa851880f99f45e3f9a771dd3ff3bb0c048ea83fb28194"},
+    {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8aacb477dc26797ee089721536a292a664846489c49d3ef9725f992449eda5a8"},
+    {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:522a11c934ea660ff8953eda090dcd2154d367dec1ae3c540aff9f8a5c109ab4"},
+    {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5bce0dc147ca85caa5d33debc4f4d65e8e8b5c97c7f9f660f215fa74fc49a321"},
+    {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b4af9f25b49a7be47c0972139e59ec0e8285c371049df1a63b6ca81fdd216a2"},
+    {file = "aiohttp-3.9.3-cp38-cp38-win32.whl", hash = "sha256:298abd678033b8571995650ccee753d9458dfa0377be4dba91e4491da3f2be63"},
+    {file = "aiohttp-3.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:69361bfdca5468c0488d7017b9b1e5ce769d40b46a9f4a2eed26b78619e9396c"},
+    {file = "aiohttp-3.9.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0fa43c32d1643f518491d9d3a730f85f5bbaedcbd7fbcae27435bb8b7a061b29"},
+    {file = "aiohttp-3.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:835a55b7ca49468aaaac0b217092dfdff370e6c215c9224c52f30daaa735c1c1"},
+    {file = "aiohttp-3.9.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06a9b2c8837d9a94fae16c6223acc14b4dfdff216ab9b7202e07a9a09541168f"},
+    {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abf151955990d23f84205286938796c55ff11bbfb4ccfada8c9c83ae6b3c89a3"},
+    {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59c26c95975f26e662ca78fdf543d4eeaef70e533a672b4113dd888bd2423caa"},
+    {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f95511dd5d0e05fd9728bac4096319f80615aaef4acbecb35a990afebe953b0e"},
+    {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:595f105710293e76b9dc09f52e0dd896bd064a79346234b521f6b968ffdd8e58"},
+    {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7c8b816c2b5af5c8a436df44ca08258fc1a13b449393a91484225fcb7545533"},
+    {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f1088fa100bf46e7b398ffd9904f4808a0612e1d966b4aa43baa535d1b6341eb"},
+    {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f59dfe57bb1ec82ac0698ebfcdb7bcd0e99c255bd637ff613760d5f33e7c81b3"},
+    {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:361a1026c9dd4aba0109e4040e2aecf9884f5cfe1b1b1bd3d09419c205e2e53d"},
+    {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:363afe77cfcbe3a36353d8ea133e904b108feea505aa4792dad6585a8192c55a"},
+    {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e2c45c208c62e955e8256949eb225bd8b66a4c9b6865729a786f2aa79b72e9d"},
+    {file = "aiohttp-3.9.3-cp39-cp39-win32.whl", hash = "sha256:f7217af2e14da0856e082e96ff637f14ae45c10a5714b63c77f26d8884cf1051"},
+    {file = "aiohttp-3.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:27468897f628c627230dba07ec65dc8d0db566923c48f29e084ce382119802bc"},
+    {file = "aiohttp-3.9.3.tar.gz", hash = "sha256:90842933e5d1ff760fae6caca4b2b3edba53ba8f4b71e95dacf2818a2aca06f7"},
+]
+
+[package.dependencies]
+aiosignal = ">=1.1.2"
+attrs = ">=17.3.0"
+frozenlist = ">=1.1.1"
+multidict = ">=4.5,<7.0"
+yarl = ">=1.0,<2.0"
+
+[package.extras]
+speedups = ["Brotli", "aiodns", "brotlicffi"]
+
+[[package]]
+name = "aiosignal"
+version = "1.3.1"
+description = "aiosignal: a list of registered asynchronous callbacks"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"},
+    {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"},
+]
+
+[package.dependencies]
+frozenlist = ">=1.1.0"
+
+[[package]]
+name = "alembic"
+version = "1.13.1"
+description = "A database migration tool for SQLAlchemy."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "alembic-1.13.1-py3-none-any.whl", hash = "sha256:2edcc97bed0bd3272611ce3a98d98279e9c209e7186e43e75bbb1b2bdfdbcc43"},
+    {file = "alembic-1.13.1.tar.gz", hash = "sha256:4932c8558bf68f2ee92b9bbcb8218671c627064d5b08939437af6d77dc05e595"},
+]
+
+[package.dependencies]
+Mako = "*"
+SQLAlchemy = ">=1.3.0"
+typing-extensions = ">=4"
+
+[package.extras]
+tz = ["backports.zoneinfo"]
 
 [[package]]
 name = "annotated-types"
@@ -13,13 +141,13 @@ files = [
 
 [[package]]
 name = "anyio"
-version = "4.0.0"
+version = "4.3.0"
 description = "High level compatibility layer for multiple asynchronous event loop implementations"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "anyio-4.0.0-py3-none-any.whl", hash = "sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f"},
-    {file = "anyio-4.0.0.tar.gz", hash = "sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a"},
+    {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"},
+    {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"},
 ]
 
 [package.dependencies]
@@ -27,21 +155,68 @@ idna = ">=2.8"
 sniffio = ">=1.1"
 
 [package.extras]
-doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)"]
-test = ["anyio[trio]", "coverage[toml] (>=7)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
-trio = ["trio (>=0.22)"]
+doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
+test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
+trio = ["trio (>=0.23)"]
+
+[[package]]
+name = "asyncer"
+version = "0.0.3"
+description = "Asyncer, async and await, focused on developer experience."
+optional = false
+python-versions = ">=3.8,<4.0"
+files = [
+    {file = "asyncer-0.0.3-py3-none-any.whl", hash = "sha256:803398c31c23337285b97aa50e7594ac92fa99672a9e26e596e8dd92ed03fe1e"},
+    {file = "asyncer-0.0.3.tar.gz", hash = "sha256:4734ede2bbe9c45caf8d81573af7a98d1bf866e3b92aa174b3f17b191cc51726"},
+]
+
+[package.dependencies]
+anyio = ">=3.4.0,<5.0"
+
+[[package]]
+name = "attrs"
+version = "23.2.0"
+description = "Classes Without Boilerplate"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"},
+    {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"},
+]
+
+[package.extras]
+cov = ["attrs[tests]", "coverage[toml] (>=5.3)"]
+dev = ["attrs[tests]", "pre-commit"]
+docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"]
+tests = ["attrs[tests-no-zope]", "zope-interface"]
+tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"]
+tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"]
 
 [[package]]
 name = "certifi"
-version = "2023.7.22"
+version = "2024.2.2"
 description = "Python package for providing Mozilla's CA Bundle."
 optional = false
 python-versions = ">=3.6"
 files = [
-    {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"},
-    {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"},
+    {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"},
+    {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"},
 ]
 
+[[package]]
+name = "click"
+version = "8.1.7"
+description = "Composable command line interface toolkit"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
+    {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
 [[package]]
 name = "colorama"
 version = "0.4.6"
@@ -55,68 +230,225 @@ files = [
 
 [[package]]
 name = "coverage"
-version = "7.3.2"
+version = "7.4.3"
 description = "Code coverage measurement for Python"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "coverage-7.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf"},
-    {file = "coverage-7.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda"},
-    {file = "coverage-7.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a"},
-    {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c"},
-    {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f"},
-    {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6"},
-    {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148"},
-    {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9"},
-    {file = "coverage-7.3.2-cp310-cp310-win32.whl", hash = "sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f"},
-    {file = "coverage-7.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611"},
-    {file = "coverage-7.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c"},
-    {file = "coverage-7.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074"},
-    {file = "coverage-7.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a"},
-    {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3"},
-    {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a"},
-    {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1"},
-    {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c"},
-    {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312"},
-    {file = "coverage-7.3.2-cp311-cp311-win32.whl", hash = "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640"},
-    {file = "coverage-7.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2"},
-    {file = "coverage-7.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836"},
-    {file = "coverage-7.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63"},
-    {file = "coverage-7.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216"},
-    {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4"},
-    {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf"},
-    {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf"},
-    {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84"},
-    {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a"},
-    {file = "coverage-7.3.2-cp312-cp312-win32.whl", hash = "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb"},
-    {file = "coverage-7.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed"},
-    {file = "coverage-7.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738"},
-    {file = "coverage-7.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2"},
-    {file = "coverage-7.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2"},
-    {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c"},
-    {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9"},
-    {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82"},
-    {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901"},
-    {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76"},
-    {file = "coverage-7.3.2-cp38-cp38-win32.whl", hash = "sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92"},
-    {file = "coverage-7.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a"},
-    {file = "coverage-7.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce"},
-    {file = "coverage-7.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9"},
-    {file = "coverage-7.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f"},
-    {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25"},
-    {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9"},
-    {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6"},
-    {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc"},
-    {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083"},
-    {file = "coverage-7.3.2-cp39-cp39-win32.whl", hash = "sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce"},
-    {file = "coverage-7.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f"},
-    {file = "coverage-7.3.2-pp38.pp39.pp310-none-any.whl", hash = "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637"},
-    {file = "coverage-7.3.2.tar.gz", hash = "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef"},
+    {file = "coverage-7.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8580b827d4746d47294c0e0b92854c85a92c2227927433998f0d3320ae8a71b6"},
+    {file = "coverage-7.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:718187eeb9849fc6cc23e0d9b092bc2348821c5e1a901c9f8975df0bc785bfd4"},
+    {file = "coverage-7.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:767b35c3a246bcb55b8044fd3a43b8cd553dd1f9f2c1eeb87a302b1f8daa0524"},
+    {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae7f19afe0cce50039e2c782bff379c7e347cba335429678450b8fe81c4ef96d"},
+    {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba3a8aaed13770e970b3df46980cb068d1c24af1a1968b7818b69af8c4347efb"},
+    {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ee866acc0861caebb4f2ab79f0b94dbfbdbfadc19f82e6e9c93930f74e11d7a0"},
+    {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:506edb1dd49e13a2d4cac6a5173317b82a23c9d6e8df63efb4f0380de0fbccbc"},
+    {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd6545d97c98a192c5ac995d21c894b581f1fd14cf389be90724d21808b657e2"},
+    {file = "coverage-7.4.3-cp310-cp310-win32.whl", hash = "sha256:f6a09b360d67e589236a44f0c39218a8efba2593b6abdccc300a8862cffc2f94"},
+    {file = "coverage-7.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:18d90523ce7553dd0b7e23cbb28865db23cddfd683a38fb224115f7826de78d0"},
+    {file = "coverage-7.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbbe5e739d45a52f3200a771c6d2c7acf89eb2524890a4a3aa1a7fa0695d2a47"},
+    {file = "coverage-7.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:489763b2d037b164846ebac0cbd368b8a4ca56385c4090807ff9fad817de4113"},
+    {file = "coverage-7.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:451f433ad901b3bb00184d83fd83d135fb682d780b38af7944c9faeecb1e0bfe"},
+    {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcc66e222cf4c719fe7722a403888b1f5e1682d1679bd780e2b26c18bb648cdc"},
+    {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ec74cfef2d985e145baae90d9b1b32f85e1741b04cd967aaf9cfa84c1334f3"},
+    {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:abbbd8093c5229c72d4c2926afaee0e6e3140de69d5dcd918b2921f2f0c8baba"},
+    {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:35eb581efdacf7b7422af677b92170da4ef34500467381e805944a3201df2079"},
+    {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8249b1c7334be8f8c3abcaaa996e1e4927b0e5a23b65f5bf6cfe3180d8ca7840"},
+    {file = "coverage-7.4.3-cp311-cp311-win32.whl", hash = "sha256:cf30900aa1ba595312ae41978b95e256e419d8a823af79ce670835409fc02ad3"},
+    {file = "coverage-7.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:18c7320695c949de11a351742ee001849912fd57e62a706d83dfc1581897fa2e"},
+    {file = "coverage-7.4.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b51bfc348925e92a9bd9b2e48dad13431b57011fd1038f08316e6bf1df107d10"},
+    {file = "coverage-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d6cdecaedea1ea9e033d8adf6a0ab11107b49571bbb9737175444cea6eb72328"},
+    {file = "coverage-7.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b2eccb883368f9e972e216c7b4c7c06cabda925b5f06dde0650281cb7666a30"},
+    {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c00cdc8fa4e50e1cc1f941a7f2e3e0f26cb2a1233c9696f26963ff58445bac7"},
+    {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4a8dd3dcf4cbd3165737358e4d7dfbd9d59902ad11e3b15eebb6393b0446e"},
+    {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:062b0a75d9261e2f9c6d071753f7eef0fc9caf3a2c82d36d76667ba7b6470003"},
+    {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ebe7c9e67a2d15fa97b77ea6571ce5e1e1f6b0db71d1d5e96f8d2bf134303c1d"},
+    {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c0a120238dd71c68484f02562f6d446d736adcc6ca0993712289b102705a9a3a"},
+    {file = "coverage-7.4.3-cp312-cp312-win32.whl", hash = "sha256:37389611ba54fd6d278fde86eb2c013c8e50232e38f5c68235d09d0a3f8aa352"},
+    {file = "coverage-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:d25b937a5d9ffa857d41be042b4238dd61db888533b53bc76dc082cb5a15e914"},
+    {file = "coverage-7.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:28ca2098939eabab044ad68850aac8f8db6bf0b29bc7f2887d05889b17346454"},
+    {file = "coverage-7.4.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:280459f0a03cecbe8800786cdc23067a8fc64c0bd51dc614008d9c36e1659d7e"},
+    {file = "coverage-7.4.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c0cdedd3500e0511eac1517bf560149764b7d8e65cb800d8bf1c63ebf39edd2"},
+    {file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a9babb9466fe1da12417a4aed923e90124a534736de6201794a3aea9d98484e"},
+    {file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dec9de46a33cf2dd87a5254af095a409ea3bf952d85ad339751e7de6d962cde6"},
+    {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:16bae383a9cc5abab9bb05c10a3e5a52e0a788325dc9ba8499e821885928968c"},
+    {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2c854ce44e1ee31bda4e318af1dbcfc929026d12c5ed030095ad98197eeeaed0"},
+    {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ce8c50520f57ec57aa21a63ea4f325c7b657386b3f02ccaedeccf9ebe27686e1"},
+    {file = "coverage-7.4.3-cp38-cp38-win32.whl", hash = "sha256:708a3369dcf055c00ddeeaa2b20f0dd1ce664eeabde6623e516c5228b753654f"},
+    {file = "coverage-7.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:1bf25fbca0c8d121a3e92a2a0555c7e5bc981aee5c3fdaf4bb7809f410f696b9"},
+    {file = "coverage-7.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b253094dbe1b431d3a4ac2f053b6d7ede2664ac559705a704f621742e034f1f"},
+    {file = "coverage-7.4.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:77fbfc5720cceac9c200054b9fab50cb2a7d79660609200ab83f5db96162d20c"},
+    {file = "coverage-7.4.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6679060424faa9c11808598504c3ab472de4531c571ab2befa32f4971835788e"},
+    {file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4af154d617c875b52651dd8dd17a31270c495082f3d55f6128e7629658d63765"},
+    {file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8640f1fde5e1b8e3439fe482cdc2b0bb6c329f4bb161927c28d2e8879c6029ee"},
+    {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:69b9f6f66c0af29642e73a520b6fed25ff9fd69a25975ebe6acb297234eda501"},
+    {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0842571634f39016a6c03e9d4aba502be652a6e4455fadb73cd3a3a49173e38f"},
+    {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a78ed23b08e8ab524551f52953a8a05d61c3a760781762aac49f8de6eede8c45"},
+    {file = "coverage-7.4.3-cp39-cp39-win32.whl", hash = "sha256:c0524de3ff096e15fcbfe8f056fdb4ea0bf497d584454f344d59fce069d3e6e9"},
+    {file = "coverage-7.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:0209a6369ccce576b43bb227dc8322d8ef9e323d089c6f3f26a597b09cb4d2aa"},
+    {file = "coverage-7.4.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:7cbde573904625509a3f37b6fecea974e363460b556a627c60dc2f47e2fffa51"},
+    {file = "coverage-7.4.3.tar.gz", hash = "sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52"},
 ]
 
 [package.extras]
 toml = ["tomli"]
 
+[[package]]
+name = "frozenlist"
+version = "1.4.1"
+description = "A list-like structure which implements collections.abc.MutableSequence"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"},
+    {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"},
+    {file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"},
+    {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"},
+    {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"},
+    {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"},
+    {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"},
+    {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"},
+    {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"},
+    {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"},
+    {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"},
+    {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"},
+    {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"},
+    {file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"},
+    {file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"},
+    {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"},
+    {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"},
+    {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"},
+    {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"},
+    {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"},
+    {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"},
+    {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"},
+    {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"},
+    {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"},
+    {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"},
+    {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"},
+    {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"},
+    {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"},
+    {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"},
+    {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"},
+    {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"},
+    {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"},
+    {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"},
+    {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"},
+    {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"},
+    {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"},
+    {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"},
+    {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"},
+    {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"},
+    {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"},
+    {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"},
+    {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"},
+    {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"},
+    {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"},
+    {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"},
+    {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"},
+    {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"},
+    {file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"},
+    {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"},
+    {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"},
+    {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"},
+    {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"},
+    {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"},
+    {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"},
+    {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"},
+    {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"},
+    {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"},
+    {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"},
+    {file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"},
+    {file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"},
+    {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"},
+    {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"},
+    {file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"},
+    {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"},
+    {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"},
+    {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"},
+    {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"},
+    {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"},
+    {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"},
+    {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"},
+    {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"},
+    {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"},
+    {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"},
+    {file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"},
+    {file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"},
+    {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"},
+    {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"},
+]
+
+[[package]]
+name = "greenlet"
+version = "3.0.3"
+description = "Lightweight in-process concurrent programming"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"},
+    {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"},
+    {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"},
+    {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"},
+    {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"},
+    {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"},
+    {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"},
+    {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"},
+    {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"},
+    {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"},
+    {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"},
+    {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"},
+    {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"},
+    {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"},
+    {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"},
+    {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"},
+    {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"},
+    {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"},
+    {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"},
+    {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"},
+    {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"},
+    {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"},
+    {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"},
+    {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"},
+    {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"},
+    {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"},
+    {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"},
+    {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"},
+    {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"},
+    {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"},
+    {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"},
+    {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"},
+    {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"},
+    {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"},
+    {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"},
+    {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"},
+    {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"},
+    {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"},
+    {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"},
+    {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"},
+    {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"},
+    {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"},
+    {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"},
+    {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"},
+    {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"},
+    {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"},
+    {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"},
+    {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"},
+    {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"},
+    {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"},
+    {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"},
+    {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"},
+    {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"},
+    {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"},
+    {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"},
+    {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"},
+    {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"},
+    {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"},
+]
+
+[package.extras]
+docs = ["Sphinx", "furo"]
+test = ["objgraph", "psutil"]
+
 [[package]]
 name = "h11"
 version = "0.14.0"
@@ -130,39 +462,40 @@ files = [
 
 [[package]]
 name = "httpcore"
-version = "0.16.3"
+version = "1.0.2"
 description = "A minimal low-level HTTP client."
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"},
-    {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"},
+    {file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"},
+    {file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"},
 ]
 
 [package.dependencies]
-anyio = ">=3.0,<5.0"
 certifi = "*"
 h11 = ">=0.13,<0.15"
-sniffio = "==1.*"
 
 [package.extras]
+asyncio = ["anyio (>=4.0,<5.0)"]
 http2 = ["h2 (>=3,<5)"]
 socks = ["socksio (==1.*)"]
+trio = ["trio (>=0.22.0,<0.23.0)"]
 
 [[package]]
 name = "httpx"
-version = "0.24.1"
+version = "0.27.0"
 description = "The next generation HTTP client."
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "httpx-0.24.1-py3-none-any.whl", hash = "sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd"},
-    {file = "httpx-0.24.1.tar.gz", hash = "sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd"},
+    {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"},
+    {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"},
 ]
 
 [package.dependencies]
+anyio = "*"
 certifi = "*"
-httpcore = ">=0.15.0,<0.18.0"
+httpcore = "==1.*"
 idna = "*"
 sniffio = "*"
 
@@ -174,13 +507,13 @@ socks = ["socksio (==1.*)"]
 
 [[package]]
 name = "idna"
-version = "3.4"
+version = "3.6"
 description = "Internationalized Domain Names in Applications (IDNA)"
 optional = false
 python-versions = ">=3.5"
 files = [
-    {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
-    {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
+    {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"},
+    {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"},
 ]
 
 [[package]]
@@ -194,46 +527,369 @@ files = [
     {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
 ]
 
+[[package]]
+name = "linkify-it-py"
+version = "2.0.2"
+description = "Links recognition library with FULL unicode support."
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "linkify-it-py-2.0.2.tar.gz", hash = "sha256:19f3060727842c254c808e99d465c80c49d2c7306788140987a1a7a29b0d6ad2"},
+    {file = "linkify_it_py-2.0.2-py3-none-any.whl", hash = "sha256:a3a24428f6c96f27370d7fe61d2ac0be09017be5190d68d8658233171f1b6541"},
+]
+
+[package.dependencies]
+uc-micro-py = "*"
+
+[package.extras]
+benchmark = ["pytest", "pytest-benchmark"]
+dev = ["black", "flake8", "isort", "pre-commit", "pyproject-flake8"]
+doc = ["myst-parser", "sphinx", "sphinx-book-theme"]
+test = ["coverage", "pytest", "pytest-cov"]
+
+[[package]]
+name = "mako"
+version = "1.3.1"
+description = "A super-fast templating language that borrows the best ideas from the existing templating languages."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "Mako-1.3.1-py3-none-any.whl", hash = "sha256:463f03e04559689adaee25e0967778d6ad41285ed607dc1e7df0dd4e4df81f9e"},
+    {file = "Mako-1.3.1.tar.gz", hash = "sha256:baee30b9c61718e093130298e678abed0dbfa1b411fcc4c1ab4df87cd631a0f2"},
+]
+
+[package.dependencies]
+MarkupSafe = ">=0.9.2"
+
+[package.extras]
+babel = ["Babel"]
+lingua = ["lingua"]
+testing = ["pytest"]
+
+[[package]]
+name = "markdown-it-py"
+version = "3.0.0"
+description = "Python port of markdown-it. Markdown parsing, done right!"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
+    {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
+]
+
+[package.dependencies]
+linkify-it-py = {version = ">=1,<3", optional = true, markers = "extra == \"linkify\""}
+mdit-py-plugins = {version = "*", optional = true, markers = "extra == \"plugins\""}
+mdurl = ">=0.1,<1.0"
+
+[package.extras]
+benchmarking = ["psutil", "pytest", "pytest-benchmark"]
+code-style = ["pre-commit (>=3.0,<4.0)"]
+compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
+linkify = ["linkify-it-py (>=1,<3)"]
+plugins = ["mdit-py-plugins"]
+profiling = ["gprof2dot"]
+rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
+testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
+
+[[package]]
+name = "markupsafe"
+version = "2.1.4"
+description = "Safely add untrusted strings to HTML/XML markup."
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "MarkupSafe-2.1.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de8153a7aae3835484ac168a9a9bdaa0c5eee4e0bc595503c95d53b942879c84"},
+    {file = "MarkupSafe-2.1.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e888ff76ceb39601c59e219f281466c6d7e66bd375b4ec1ce83bcdc68306796b"},
+    {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0b838c37ba596fcbfca71651a104a611543077156cb0a26fe0c475e1f152ee8"},
+    {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac1ebf6983148b45b5fa48593950f90ed6d1d26300604f321c74a9ca1609f8e"},
+    {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fbad3d346df8f9d72622ac71b69565e621ada2ce6572f37c2eae8dacd60385d"},
+    {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5291d98cd3ad9a562883468c690a2a238c4a6388ab3bd155b0c75dd55ece858"},
+    {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a7cc49ef48a3c7a0005a949f3c04f8baa5409d3f663a1b36f0eba9bfe2a0396e"},
+    {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b83041cda633871572f0d3c41dddd5582ad7d22f65a72eacd8d3d6d00291df26"},
+    {file = "MarkupSafe-2.1.4-cp310-cp310-win32.whl", hash = "sha256:0c26f67b3fe27302d3a412b85ef696792c4a2386293c53ba683a89562f9399b0"},
+    {file = "MarkupSafe-2.1.4-cp310-cp310-win_amd64.whl", hash = "sha256:a76055d5cb1c23485d7ddae533229039b850db711c554a12ea64a0fd8a0129e2"},
+    {file = "MarkupSafe-2.1.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9e9e3c4020aa2dc62d5dd6743a69e399ce3de58320522948af6140ac959ab863"},
+    {file = "MarkupSafe-2.1.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0042d6a9880b38e1dd9ff83146cc3c9c18a059b9360ceae207805567aacccc69"},
+    {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d03fea4c4e9fd0ad75dc2e7e2b6757b80c152c032ea1d1de487461d8140efc"},
+    {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ab3a886a237f6e9c9f4f7d272067e712cdb4efa774bef494dccad08f39d8ae6"},
+    {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abf5ebbec056817057bfafc0445916bb688a255a5146f900445d081db08cbabb"},
+    {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e1a0d1924a5013d4f294087e00024ad25668234569289650929ab871231668e7"},
+    {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e7902211afd0af05fbadcc9a312e4cf10f27b779cf1323e78d52377ae4b72bea"},
+    {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c669391319973e49a7c6230c218a1e3044710bc1ce4c8e6eb71f7e6d43a2c131"},
+    {file = "MarkupSafe-2.1.4-cp311-cp311-win32.whl", hash = "sha256:31f57d64c336b8ccb1966d156932f3daa4fee74176b0fdc48ef580be774aae74"},
+    {file = "MarkupSafe-2.1.4-cp311-cp311-win_amd64.whl", hash = "sha256:54a7e1380dfece8847c71bf7e33da5d084e9b889c75eca19100ef98027bd9f56"},
+    {file = "MarkupSafe-2.1.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a76cd37d229fc385738bd1ce4cba2a121cf26b53864c1772694ad0ad348e509e"},
+    {file = "MarkupSafe-2.1.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:987d13fe1d23e12a66ca2073b8d2e2a75cec2ecb8eab43ff5624ba0ad42764bc"},
+    {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5244324676254697fe5c181fc762284e2c5fceeb1c4e3e7f6aca2b6f107e60dc"},
+    {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78bc995e004681246e85e28e068111a4c3f35f34e6c62da1471e844ee1446250"},
+    {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4d176cfdfde84f732c4a53109b293d05883e952bbba68b857ae446fa3119b4f"},
+    {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f9917691f410a2e0897d1ef99619fd3f7dd503647c8ff2475bf90c3cf222ad74"},
+    {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f06e5a9e99b7df44640767842f414ed5d7bedaaa78cd817ce04bbd6fd86e2dd6"},
+    {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:396549cea79e8ca4ba65525470d534e8a41070e6b3500ce2414921099cb73e8d"},
+    {file = "MarkupSafe-2.1.4-cp312-cp312-win32.whl", hash = "sha256:f6be2d708a9d0e9b0054856f07ac7070fbe1754be40ca8525d5adccdbda8f475"},
+    {file = "MarkupSafe-2.1.4-cp312-cp312-win_amd64.whl", hash = "sha256:5045e892cfdaecc5b4c01822f353cf2c8feb88a6ec1c0adef2a2e705eef0f656"},
+    {file = "MarkupSafe-2.1.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7a07f40ef8f0fbc5ef1000d0c78771f4d5ca03b4953fc162749772916b298fc4"},
+    {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d18b66fe626ac412d96c2ab536306c736c66cf2a31c243a45025156cc190dc8a"},
+    {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:698e84142f3f884114ea8cf83e7a67ca8f4ace8454e78fe960646c6c91c63bfa"},
+    {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49a3b78a5af63ec10d8604180380c13dcd870aba7928c1fe04e881d5c792dc4e"},
+    {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:15866d7f2dc60cfdde12ebb4e75e41be862348b4728300c36cdf405e258415ec"},
+    {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6aa5e2e7fc9bc042ae82d8b79d795b9a62bd8f15ba1e7594e3db243f158b5565"},
+    {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:54635102ba3cf5da26eb6f96c4b8c53af8a9c0d97b64bdcb592596a6255d8518"},
+    {file = "MarkupSafe-2.1.4-cp37-cp37m-win32.whl", hash = "sha256:3583a3a3ab7958e354dc1d25be74aee6228938312ee875a22330c4dc2e41beb0"},
+    {file = "MarkupSafe-2.1.4-cp37-cp37m-win_amd64.whl", hash = "sha256:d6e427c7378c7f1b2bef6a344c925b8b63623d3321c09a237b7cc0e77dd98ceb"},
+    {file = "MarkupSafe-2.1.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:bf1196dcc239e608605b716e7b166eb5faf4bc192f8a44b81e85251e62584bd2"},
+    {file = "MarkupSafe-2.1.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4df98d4a9cd6a88d6a585852f56f2155c9cdb6aec78361a19f938810aa020954"},
+    {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b835aba863195269ea358cecc21b400276747cc977492319fd7682b8cd2c253d"},
+    {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23984d1bdae01bee794267424af55eef4dfc038dc5d1272860669b2aa025c9e3"},
+    {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c98c33ffe20e9a489145d97070a435ea0679fddaabcafe19982fe9c971987d5"},
+    {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9896fca4a8eb246defc8b2a7ac77ef7553b638e04fbf170bff78a40fa8a91474"},
+    {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b0fe73bac2fed83839dbdbe6da84ae2a31c11cfc1c777a40dbd8ac8a6ed1560f"},
+    {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c7556bafeaa0a50e2fe7dc86e0382dea349ebcad8f010d5a7dc6ba568eaaa789"},
+    {file = "MarkupSafe-2.1.4-cp38-cp38-win32.whl", hash = "sha256:fc1a75aa8f11b87910ffd98de62b29d6520b6d6e8a3de69a70ca34dea85d2a8a"},
+    {file = "MarkupSafe-2.1.4-cp38-cp38-win_amd64.whl", hash = "sha256:3a66c36a3864df95e4f62f9167c734b3b1192cb0851b43d7cc08040c074c6279"},
+    {file = "MarkupSafe-2.1.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:765f036a3d00395a326df2835d8f86b637dbaf9832f90f5d196c3b8a7a5080cb"},
+    {file = "MarkupSafe-2.1.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:21e7af8091007bf4bebf4521184f4880a6acab8df0df52ef9e513d8e5db23411"},
+    {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5c31fe855c77cad679b302aabc42d724ed87c043b1432d457f4976add1c2c3e"},
+    {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7653fa39578957bc42e5ebc15cf4361d9e0ee4b702d7d5ec96cdac860953c5b4"},
+    {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47bb5f0142b8b64ed1399b6b60f700a580335c8e1c57f2f15587bd072012decc"},
+    {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fe8512ed897d5daf089e5bd010c3dc03bb1bdae00b35588c49b98268d4a01e00"},
+    {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:36d7626a8cca4d34216875aee5a1d3d654bb3dac201c1c003d182283e3205949"},
+    {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b6f14a9cd50c3cb100eb94b3273131c80d102e19bb20253ac7bd7336118a673a"},
+    {file = "MarkupSafe-2.1.4-cp39-cp39-win32.whl", hash = "sha256:c8f253a84dbd2c63c19590fa86a032ef3d8cc18923b8049d91bcdeeb2581fbf6"},
+    {file = "MarkupSafe-2.1.4-cp39-cp39-win_amd64.whl", hash = "sha256:8b570a1537367b52396e53325769608f2a687ec9a4363647af1cded8928af959"},
+    {file = "MarkupSafe-2.1.4.tar.gz", hash = "sha256:3aae9af4cac263007fd6309c64c6ab4506dd2b79382d9d19a1994f9240b8db4f"},
+]
+
+[[package]]
+name = "mdit-py-plugins"
+version = "0.4.0"
+description = "Collection of plugins for markdown-it-py"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "mdit_py_plugins-0.4.0-py3-none-any.whl", hash = "sha256:b51b3bb70691f57f974e257e367107857a93b36f322a9e6d44ca5bf28ec2def9"},
+    {file = "mdit_py_plugins-0.4.0.tar.gz", hash = "sha256:d8ab27e9aed6c38aa716819fedfde15ca275715955f8a185a8e1cf90fb1d2c1b"},
+]
+
+[package.dependencies]
+markdown-it-py = ">=1.0.0,<4.0.0"
+
+[package.extras]
+code-style = ["pre-commit"]
+rtd = ["myst-parser", "sphinx-book-theme"]
+testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+description = "Markdown URL utilities"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
+    {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
+]
+
+[[package]]
+name = "msgpack"
+version = "1.0.7"
+description = "MessagePack serializer"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "msgpack-1.0.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04ad6069c86e531682f9e1e71b71c1c3937d6014a7c3e9edd2aa81ad58842862"},
+    {file = "msgpack-1.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cca1b62fe70d761a282496b96a5e51c44c213e410a964bdffe0928e611368329"},
+    {file = "msgpack-1.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e50ebce52f41370707f1e21a59514e3375e3edd6e1832f5e5235237db933c98b"},
+    {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b4f35de6a304b5533c238bee86b670b75b03d31b7797929caa7a624b5dda6"},
+    {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28efb066cde83c479dfe5a48141a53bc7e5f13f785b92ddde336c716663039ee"},
+    {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cb14ce54d9b857be9591ac364cb08dc2d6a5c4318c1182cb1d02274029d590d"},
+    {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b573a43ef7c368ba4ea06050a957c2a7550f729c31f11dd616d2ac4aba99888d"},
+    {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ccf9a39706b604d884d2cb1e27fe973bc55f2890c52f38df742bc1d79ab9f5e1"},
+    {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cb70766519500281815dfd7a87d3a178acf7ce95390544b8c90587d76b227681"},
+    {file = "msgpack-1.0.7-cp310-cp310-win32.whl", hash = "sha256:b610ff0f24e9f11c9ae653c67ff8cc03c075131401b3e5ef4b82570d1728f8a9"},
+    {file = "msgpack-1.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:a40821a89dc373d6427e2b44b572efc36a2778d3f543299e2f24eb1a5de65415"},
+    {file = "msgpack-1.0.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:576eb384292b139821c41995523654ad82d1916da6a60cff129c715a6223ea84"},
+    {file = "msgpack-1.0.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:730076207cb816138cf1af7f7237b208340a2c5e749707457d70705715c93b93"},
+    {file = "msgpack-1.0.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:85765fdf4b27eb5086f05ac0491090fc76f4f2b28e09d9350c31aac25a5aaff8"},
+    {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3476fae43db72bd11f29a5147ae2f3cb22e2f1a91d575ef130d2bf49afd21c46"},
+    {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d4c80667de2e36970ebf74f42d1088cc9ee7ef5f4e8c35eee1b40eafd33ca5b"},
+    {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b0bf0effb196ed76b7ad883848143427a73c355ae8e569fa538365064188b8e"},
+    {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f9a7c509542db4eceed3dcf21ee5267ab565a83555c9b88a8109dcecc4709002"},
+    {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:84b0daf226913133f899ea9b30618722d45feffa67e4fe867b0b5ae83a34060c"},
+    {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ec79ff6159dffcc30853b2ad612ed572af86c92b5168aa3fc01a67b0fa40665e"},
+    {file = "msgpack-1.0.7-cp311-cp311-win32.whl", hash = "sha256:3e7bf4442b310ff154b7bb9d81eb2c016b7d597e364f97d72b1acc3817a0fdc1"},
+    {file = "msgpack-1.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:3f0c8c6dfa6605ab8ff0611995ee30d4f9fcff89966cf562733b4008a3d60d82"},
+    {file = "msgpack-1.0.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f0936e08e0003f66bfd97e74ee530427707297b0d0361247e9b4f59ab78ddc8b"},
+    {file = "msgpack-1.0.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98bbd754a422a0b123c66a4c341de0474cad4a5c10c164ceed6ea090f3563db4"},
+    {file = "msgpack-1.0.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b291f0ee7961a597cbbcc77709374087fa2a9afe7bdb6a40dbbd9b127e79afee"},
+    {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebbbba226f0a108a7366bf4b59bf0f30a12fd5e75100c630267d94d7f0ad20e5"},
+    {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e2d69948e4132813b8d1131f29f9101bc2c915f26089a6d632001a5c1349672"},
+    {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdf38ba2d393c7911ae989c3bbba510ebbcdf4ecbdbfec36272abe350c454075"},
+    {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:993584fc821c58d5993521bfdcd31a4adf025c7d745bbd4d12ccfecf695af5ba"},
+    {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:52700dc63a4676669b341ba33520f4d6e43d3ca58d422e22ba66d1736b0a6e4c"},
+    {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e45ae4927759289c30ccba8d9fdce62bb414977ba158286b5ddaf8df2cddb5c5"},
+    {file = "msgpack-1.0.7-cp312-cp312-win32.whl", hash = "sha256:27dcd6f46a21c18fa5e5deed92a43d4554e3df8d8ca5a47bf0615d6a5f39dbc9"},
+    {file = "msgpack-1.0.7-cp312-cp312-win_amd64.whl", hash = "sha256:7687e22a31e976a0e7fc99c2f4d11ca45eff652a81eb8c8085e9609298916dcf"},
+    {file = "msgpack-1.0.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5b6ccc0c85916998d788b295765ea0e9cb9aac7e4a8ed71d12e7d8ac31c23c95"},
+    {file = "msgpack-1.0.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:235a31ec7db685f5c82233bddf9858748b89b8119bf4538d514536c485c15fe0"},
+    {file = "msgpack-1.0.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cab3db8bab4b7e635c1c97270d7a4b2a90c070b33cbc00c99ef3f9be03d3e1f7"},
+    {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bfdd914e55e0d2c9e1526de210f6fe8ffe9705f2b1dfcc4aecc92a4cb4b533d"},
+    {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36e17c4592231a7dbd2ed09027823ab295d2791b3b1efb2aee874b10548b7524"},
+    {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38949d30b11ae5f95c3c91917ee7a6b239f5ec276f271f28638dec9156f82cfc"},
+    {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ff1d0899f104f3921d94579a5638847f783c9b04f2d5f229392ca77fba5b82fc"},
+    {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:dc43f1ec66eb8440567186ae2f8c447d91e0372d793dfe8c222aec857b81a8cf"},
+    {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dd632777ff3beaaf629f1ab4396caf7ba0bdd075d948a69460d13d44357aca4c"},
+    {file = "msgpack-1.0.7-cp38-cp38-win32.whl", hash = "sha256:4e71bc4416de195d6e9b4ee93ad3f2f6b2ce11d042b4d7a7ee00bbe0358bd0c2"},
+    {file = "msgpack-1.0.7-cp38-cp38-win_amd64.whl", hash = "sha256:8f5b234f567cf76ee489502ceb7165c2a5cecec081db2b37e35332b537f8157c"},
+    {file = "msgpack-1.0.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfef2bb6ef068827bbd021017a107194956918ab43ce4d6dc945ffa13efbc25f"},
+    {file = "msgpack-1.0.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:484ae3240666ad34cfa31eea7b8c6cd2f1fdaae21d73ce2974211df099a95d81"},
+    {file = "msgpack-1.0.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3967e4ad1aa9da62fd53e346ed17d7b2e922cba5ab93bdd46febcac39be636fc"},
+    {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dd178c4c80706546702c59529ffc005681bd6dc2ea234c450661b205445a34d"},
+    {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6ffbc252eb0d229aeb2f9ad051200668fc3a9aaa8994e49f0cb2ffe2b7867e7"},
+    {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:822ea70dc4018c7e6223f13affd1c5c30c0f5c12ac1f96cd8e9949acddb48a61"},
+    {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:384d779f0d6f1b110eae74cb0659d9aa6ff35aaf547b3955abf2ab4c901c4819"},
+    {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f64e376cd20d3f030190e8c32e1c64582eba56ac6dc7d5b0b49a9d44021b52fd"},
+    {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ed82f5a7af3697b1c4786053736f24a0efd0a1b8a130d4c7bfee4b9ded0f08f"},
+    {file = "msgpack-1.0.7-cp39-cp39-win32.whl", hash = "sha256:f26a07a6e877c76a88e3cecac8531908d980d3d5067ff69213653649ec0f60ad"},
+    {file = "msgpack-1.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:1dc93e8e4653bdb5910aed79f11e165c85732067614f180f70534f056da97db3"},
+    {file = "msgpack-1.0.7.tar.gz", hash = "sha256:572efc93db7a4d27e404501975ca6d2d9775705c2d922390d878fcf768d92c87"},
+]
+
+[[package]]
+name = "multidict"
+version = "6.0.4"
+description = "multidict implementation"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"},
+    {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"},
+    {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"},
+    {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"},
+    {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"},
+    {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"},
+    {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"},
+    {file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"},
+    {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"},
+    {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"},
+    {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"},
+    {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"},
+    {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"},
+    {file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"},
+    {file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"},
+    {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"},
+    {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"},
+    {file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"},
+    {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"},
+    {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"},
+    {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"},
+    {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"},
+    {file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"},
+    {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"},
+    {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"},
+    {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"},
+    {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"},
+    {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"},
+    {file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"},
+    {file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"},
+    {file = "multidict-6.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78"},
+    {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f"},
+    {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603"},
+    {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac"},
+    {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9"},
+    {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2"},
+    {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde"},
+    {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe"},
+    {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067"},
+    {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87"},
+    {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d"},
+    {file = "multidict-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775"},
+    {file = "multidict-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e"},
+    {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"},
+    {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"},
+    {file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"},
+    {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"},
+    {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"},
+    {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"},
+    {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"},
+    {file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"},
+    {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"},
+    {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"},
+    {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"},
+    {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"},
+    {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"},
+    {file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"},
+    {file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"},
+    {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"},
+    {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"},
+    {file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"},
+    {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"},
+    {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"},
+    {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"},
+    {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"},
+    {file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"},
+    {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"},
+    {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"},
+    {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"},
+    {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"},
+    {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"},
+    {file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"},
+    {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"},
+    {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"},
+]
+
 [[package]]
 name = "packaging"
-version = "23.0"
+version = "23.2"
 description = "Core utilities for Python packages"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"},
-    {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"},
+    {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
+    {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
 ]
 
 [[package]]
 name = "pluggy"
-version = "1.0.0"
+version = "1.4.0"
 description = "plugin and hook calling mechanisms for python"
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.8"
 files = [
-    {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
-    {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
+    {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"},
+    {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"},
 ]
 
 [package.extras]
 dev = ["pre-commit", "tox"]
 testing = ["pytest", "pytest-benchmark"]
 
+[[package]]
+name = "pprintpp"
+version = "0.4.0"
+description = "A drop-in replacement for pprint that's actually pretty"
+optional = false
+python-versions = "*"
+files = [
+    {file = "pprintpp-0.4.0-py2.py3-none-any.whl", hash = "sha256:b6b4dcdd0c0c0d75e4d7b2f21a9e933e5b2ce62b26e1a54537f9651ae5a5c01d"},
+    {file = "pprintpp-0.4.0.tar.gz", hash = "sha256:ea826108e2c7f49dc6d66c752973c3fc9749142a798d6b254e1e301cfdbc6403"},
+]
+
 [[package]]
 name = "pydantic"
-version = "2.4.2"
+version = "2.6.4"
 description = "Data validation using Python type hints"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "pydantic-2.4.2-py3-none-any.whl", hash = "sha256:bc3ddf669d234f4220e6e1c4d96b061abe0998185a8d7855c0126782b7abc8c1"},
-    {file = "pydantic-2.4.2.tar.gz", hash = "sha256:94f336138093a5d7f426aac732dcfe7ab4eb4da243c88f891d65deb4a2556ee7"},
+    {file = "pydantic-2.6.4-py3-none-any.whl", hash = "sha256:cc46fce86607580867bdc3361ad462bab9c222ef042d3da86f2fb333e1d916c5"},
+    {file = "pydantic-2.6.4.tar.gz", hash = "sha256:b1704e0847db01817624a6b86766967f552dd9dbf3afba4004409f908dcc84e6"},
 ]
 
 [package.dependencies]
 annotated-types = ">=0.4.0"
-pydantic-core = "2.10.1"
+pydantic-core = "2.16.3"
 typing-extensions = ">=4.6.1"
 
 [package.extras]
@@ -241,141 +897,162 @@ email = ["email-validator (>=2.0.0)"]
 
 [[package]]
 name = "pydantic-core"
-version = "2.10.1"
+version = "2.16.3"
 description = ""
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "pydantic_core-2.10.1-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:d64728ee14e667ba27c66314b7d880b8eeb050e58ffc5fec3b7a109f8cddbd63"},
-    {file = "pydantic_core-2.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:48525933fea744a3e7464c19bfede85df4aba79ce90c60b94d8b6e1eddd67096"},
-    {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef337945bbd76cce390d1b2496ccf9f90b1c1242a3a7bc242ca4a9fc5993427a"},
-    {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1392e0638af203cee360495fd2cfdd6054711f2db5175b6e9c3c461b76f5175"},
-    {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0675ba5d22de54d07bccde38997e780044dcfa9a71aac9fd7d4d7a1d2e3e65f7"},
-    {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:128552af70a64660f21cb0eb4876cbdadf1a1f9d5de820fed6421fa8de07c893"},
-    {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f6e6aed5818c264412ac0598b581a002a9f050cb2637a84979859e70197aa9e"},
-    {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ecaac27da855b8d73f92123e5f03612b04c5632fd0a476e469dfc47cd37d6b2e"},
-    {file = "pydantic_core-2.10.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b3c01c2fb081fced3bbb3da78510693dc7121bb893a1f0f5f4b48013201f362e"},
-    {file = "pydantic_core-2.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:92f675fefa977625105708492850bcbc1182bfc3e997f8eecb866d1927c98ae6"},
-    {file = "pydantic_core-2.10.1-cp310-none-win32.whl", hash = "sha256:420a692b547736a8d8703c39ea935ab5d8f0d2573f8f123b0a294e49a73f214b"},
-    {file = "pydantic_core-2.10.1-cp310-none-win_amd64.whl", hash = "sha256:0880e239827b4b5b3e2ce05e6b766a7414e5f5aedc4523be6b68cfbc7f61c5d0"},
-    {file = "pydantic_core-2.10.1-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:073d4a470b195d2b2245d0343569aac7e979d3a0dcce6c7d2af6d8a920ad0bea"},
-    {file = "pydantic_core-2.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:600d04a7b342363058b9190d4e929a8e2e715c5682a70cc37d5ded1e0dd370b4"},
-    {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39215d809470f4c8d1881758575b2abfb80174a9e8daf8f33b1d4379357e417c"},
-    {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eeb3d3d6b399ffe55f9a04e09e635554012f1980696d6b0aca3e6cf42a17a03b"},
-    {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7a7902bf75779bc12ccfc508bfb7a4c47063f748ea3de87135d433a4cca7a2f"},
-    {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3625578b6010c65964d177626fde80cf60d7f2e297d56b925cb5cdeda6e9925a"},
-    {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:caa48fc31fc7243e50188197b5f0c4228956f97b954f76da157aae7f67269ae8"},
-    {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:07ec6d7d929ae9c68f716195ce15e745b3e8fa122fc67698ac6498d802ed0fa4"},
-    {file = "pydantic_core-2.10.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e6f31a17acede6a8cd1ae2d123ce04d8cca74056c9d456075f4f6f85de055607"},
-    {file = "pydantic_core-2.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d8f1ebca515a03e5654f88411420fea6380fc841d1bea08effb28184e3d4899f"},
-    {file = "pydantic_core-2.10.1-cp311-none-win32.whl", hash = "sha256:6db2eb9654a85ada248afa5a6db5ff1cf0f7b16043a6b070adc4a5be68c716d6"},
-    {file = "pydantic_core-2.10.1-cp311-none-win_amd64.whl", hash = "sha256:4a5be350f922430997f240d25f8219f93b0c81e15f7b30b868b2fddfc2d05f27"},
-    {file = "pydantic_core-2.10.1-cp311-none-win_arm64.whl", hash = "sha256:5fdb39f67c779b183b0c853cd6b45f7db84b84e0571b3ef1c89cdb1dfc367325"},
-    {file = "pydantic_core-2.10.1-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:b1f22a9ab44de5f082216270552aa54259db20189e68fc12484873d926426921"},
-    {file = "pydantic_core-2.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8572cadbf4cfa95fb4187775b5ade2eaa93511f07947b38f4cd67cf10783b118"},
-    {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db9a28c063c7c00844ae42a80203eb6d2d6bbb97070cfa00194dff40e6f545ab"},
-    {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2a35baa428181cb2270a15864ec6286822d3576f2ed0f4cd7f0c1708472aff"},
-    {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05560ab976012bf40f25d5225a58bfa649bb897b87192a36c6fef1ab132540d7"},
-    {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6495008733c7521a89422d7a68efa0a0122c99a5861f06020ef5b1f51f9ba7c"},
-    {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ac492c686defc8e6133e3a2d9eaf5261b3df26b8ae97450c1647286750b901"},
-    {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8282bab177a9a3081fd3d0a0175a07a1e2bfb7fcbbd949519ea0980f8a07144d"},
-    {file = "pydantic_core-2.10.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:aafdb89fdeb5fe165043896817eccd6434aee124d5ee9b354f92cd574ba5e78f"},
-    {file = "pydantic_core-2.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f6defd966ca3b187ec6c366604e9296f585021d922e666b99c47e78738b5666c"},
-    {file = "pydantic_core-2.10.1-cp312-none-win32.whl", hash = "sha256:7c4d1894fe112b0864c1fa75dffa045720a194b227bed12f4be7f6045b25209f"},
-    {file = "pydantic_core-2.10.1-cp312-none-win_amd64.whl", hash = "sha256:5994985da903d0b8a08e4935c46ed8daf5be1cf217489e673910951dc533d430"},
-    {file = "pydantic_core-2.10.1-cp312-none-win_arm64.whl", hash = "sha256:0d8a8adef23d86d8eceed3e32e9cca8879c7481c183f84ed1a8edc7df073af94"},
-    {file = "pydantic_core-2.10.1-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:9badf8d45171d92387410b04639d73811b785b5161ecadabf056ea14d62d4ede"},
-    {file = "pydantic_core-2.10.1-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:ebedb45b9feb7258fac0a268a3f6bec0a2ea4d9558f3d6f813f02ff3a6dc6698"},
-    {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfe1090245c078720d250d19cb05d67e21a9cd7c257698ef139bc41cf6c27b4f"},
-    {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e357571bb0efd65fd55f18db0a2fb0ed89d0bb1d41d906b138f088933ae618bb"},
-    {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3dcd587b69bbf54fc04ca157c2323b8911033e827fffaecf0cafa5a892a0904"},
-    {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c120c9ce3b163b985a3b966bb701114beb1da4b0468b9b236fc754783d85aa3"},
-    {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15d6bca84ffc966cc9976b09a18cf9543ed4d4ecbd97e7086f9ce9327ea48891"},
-    {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cabb9710f09d5d2e9e2748c3e3e20d991a4c5f96ed8f1132518f54ab2967221"},
-    {file = "pydantic_core-2.10.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:82f55187a5bebae7d81d35b1e9aaea5e169d44819789837cdd4720d768c55d15"},
-    {file = "pydantic_core-2.10.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:1d40f55222b233e98e3921df7811c27567f0e1a4411b93d4c5c0f4ce131bc42f"},
-    {file = "pydantic_core-2.10.1-cp37-none-win32.whl", hash = "sha256:14e09ff0b8fe6e46b93d36a878f6e4a3a98ba5303c76bb8e716f4878a3bee92c"},
-    {file = "pydantic_core-2.10.1-cp37-none-win_amd64.whl", hash = "sha256:1396e81b83516b9d5c9e26a924fa69164156c148c717131f54f586485ac3c15e"},
-    {file = "pydantic_core-2.10.1-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:6835451b57c1b467b95ffb03a38bb75b52fb4dc2762bb1d9dbed8de31ea7d0fc"},
-    {file = "pydantic_core-2.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b00bc4619f60c853556b35f83731bd817f989cba3e97dc792bb8c97941b8053a"},
-    {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fa467fd300a6f046bdb248d40cd015b21b7576c168a6bb20aa22e595c8ffcdd"},
-    {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d99277877daf2efe074eae6338453a4ed54a2d93fb4678ddfe1209a0c93a2468"},
-    {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa7db7558607afeccb33c0e4bf1c9a9a835e26599e76af6fe2fcea45904083a6"},
-    {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aad7bd686363d1ce4ee930ad39f14e1673248373f4a9d74d2b9554f06199fb58"},
-    {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:443fed67d33aa85357464f297e3d26e570267d1af6fef1c21ca50921d2976302"},
-    {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:042462d8d6ba707fd3ce9649e7bf268633a41018d6a998fb5fbacb7e928a183e"},
-    {file = "pydantic_core-2.10.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ecdbde46235f3d560b18be0cb706c8e8ad1b965e5c13bbba7450c86064e96561"},
-    {file = "pydantic_core-2.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ed550ed05540c03f0e69e6d74ad58d026de61b9eaebebbaaf8873e585cbb18de"},
-    {file = "pydantic_core-2.10.1-cp38-none-win32.whl", hash = "sha256:8cdbbd92154db2fec4ec973d45c565e767ddc20aa6dbaf50142676484cbff8ee"},
-    {file = "pydantic_core-2.10.1-cp38-none-win_amd64.whl", hash = "sha256:9f6f3e2598604956480f6c8aa24a3384dbf6509fe995d97f6ca6103bb8c2534e"},
-    {file = "pydantic_core-2.10.1-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:655f8f4c8d6a5963c9a0687793da37b9b681d9ad06f29438a3b2326d4e6b7970"},
-    {file = "pydantic_core-2.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e570ffeb2170e116a5b17e83f19911020ac79d19c96f320cbfa1fa96b470185b"},
-    {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64322bfa13e44c6c30c518729ef08fda6026b96d5c0be724b3c4ae4da939f875"},
-    {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:485a91abe3a07c3a8d1e082ba29254eea3e2bb13cbbd4351ea4e5a21912cc9b0"},
-    {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7c2b8eb9fc872e68b46eeaf835e86bccc3a58ba57d0eedc109cbb14177be531"},
-    {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5cb87bdc2e5f620693148b5f8f842d293cae46c5f15a1b1bf7ceeed324a740c"},
-    {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25bd966103890ccfa028841a8f30cebcf5875eeac8c4bde4fe221364c92f0c9a"},
-    {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f323306d0556351735b54acbf82904fe30a27b6a7147153cbe6e19aaaa2aa429"},
-    {file = "pydantic_core-2.10.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0c27f38dc4fbf07b358b2bc90edf35e82d1703e22ff2efa4af4ad5de1b3833e7"},
-    {file = "pydantic_core-2.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f1365e032a477c1430cfe0cf2856679529a2331426f8081172c4a74186f1d595"},
-    {file = "pydantic_core-2.10.1-cp39-none-win32.whl", hash = "sha256:a1c311fd06ab3b10805abb72109f01a134019739bd3286b8ae1bc2fc4e50c07a"},
-    {file = "pydantic_core-2.10.1-cp39-none-win_amd64.whl", hash = "sha256:ae8a8843b11dc0b03b57b52793e391f0122e740de3df1474814c700d2622950a"},
-    {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d43002441932f9a9ea5d6f9efaa2e21458221a3a4b417a14027a1d530201ef1b"},
-    {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fcb83175cc4936a5425dde3356f079ae03c0802bbdf8ff82c035f8a54b333521"},
-    {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:962ed72424bf1f72334e2f1e61b68f16c0e596f024ca7ac5daf229f7c26e4208"},
-    {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2cf5bb4dd67f20f3bbc1209ef572a259027c49e5ff694fa56bed62959b41e1f9"},
-    {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e544246b859f17373bed915182ab841b80849ed9cf23f1f07b73b7c58baee5fb"},
-    {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c0877239307b7e69d025b73774e88e86ce82f6ba6adf98f41069d5b0b78bd1bf"},
-    {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:53df009d1e1ba40f696f8995683e067e3967101d4bb4ea6f667931b7d4a01357"},
-    {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a1254357f7e4c82e77c348dabf2d55f1d14d19d91ff025004775e70a6ef40ada"},
-    {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:524ff0ca3baea164d6d93a32c58ac79eca9f6cf713586fdc0adb66a8cdeab96a"},
-    {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f0ac9fb8608dbc6eaf17956bf623c9119b4db7dbb511650910a82e261e6600f"},
-    {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:320f14bd4542a04ab23747ff2c8a778bde727158b606e2661349557f0770711e"},
-    {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:63974d168b6233b4ed6a0046296803cb13c56637a7b8106564ab575926572a55"},
-    {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:417243bf599ba1f1fef2bb8c543ceb918676954734e2dcb82bf162ae9d7bd514"},
-    {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dda81e5ec82485155a19d9624cfcca9be88a405e2857354e5b089c2a982144b2"},
-    {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:14cfbb00959259e15d684505263d5a21732b31248a5dd4941f73a3be233865b9"},
-    {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:631cb7415225954fdcc2a024119101946793e5923f6c4d73a5914d27eb3d3a05"},
-    {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:bec7dd208a4182e99c5b6c501ce0b1f49de2802448d4056091f8e630b28e9a52"},
-    {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:149b8a07712f45b332faee1a2258d8ef1fb4a36f88c0c17cb687f205c5dc6e7d"},
-    {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d966c47f9dd73c2d32a809d2be529112d509321c5310ebf54076812e6ecd884"},
-    {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7eb037106f5c6b3b0b864ad226b0b7ab58157124161d48e4b30c4a43fef8bc4b"},
-    {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:154ea7c52e32dce13065dbb20a4a6f0cc012b4f667ac90d648d36b12007fa9f7"},
-    {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e562617a45b5a9da5be4abe72b971d4f00bf8555eb29bb91ec2ef2be348cd132"},
-    {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:f23b55eb5464468f9e0e9a9935ce3ed2a870608d5f534025cd5536bca25b1402"},
-    {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:e9121b4009339b0f751955baf4543a0bfd6bc3f8188f8056b1a25a2d45099934"},
-    {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:0523aeb76e03f753b58be33b26540880bac5aa54422e4462404c432230543f33"},
-    {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e0e2959ef5d5b8dc9ef21e1a305a21a36e254e6a34432d00c72a92fdc5ecda5"},
-    {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da01bec0a26befab4898ed83b362993c844b9a607a86add78604186297eb047e"},
-    {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f2e9072d71c1f6cfc79a36d4484c82823c560e6f5599c43c1ca6b5cdbd54f881"},
-    {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f36a3489d9e28fe4b67be9992a23029c3cec0babc3bd9afb39f49844a8c721c5"},
-    {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f64f82cc3443149292b32387086d02a6c7fb39b8781563e0ca7b8d7d9cf72bd7"},
-    {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b4a6db486ac8e99ae696e09efc8b2b9fea67b63c8f88ba7a1a16c24a057a0776"},
-    {file = "pydantic_core-2.10.1.tar.gz", hash = "sha256:0f8682dbdd2f67f8e1edddcbffcc29f60a6182b4901c367fc8c1c40d30bb0a82"},
+    {file = "pydantic_core-2.16.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4"},
+    {file = "pydantic_core-2.16.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1"},
+    {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45"},
+    {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187"},
+    {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8"},
+    {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec"},
+    {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f"},
+    {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99"},
+    {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979"},
+    {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db"},
+    {file = "pydantic_core-2.16.3-cp310-none-win32.whl", hash = "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132"},
+    {file = "pydantic_core-2.16.3-cp310-none-win_amd64.whl", hash = "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb"},
+    {file = "pydantic_core-2.16.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4"},
+    {file = "pydantic_core-2.16.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd"},
+    {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac"},
+    {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda"},
+    {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340"},
+    {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97"},
+    {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e"},
+    {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f"},
+    {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e"},
+    {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba"},
+    {file = "pydantic_core-2.16.3-cp311-none-win32.whl", hash = "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721"},
+    {file = "pydantic_core-2.16.3-cp311-none-win_amd64.whl", hash = "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df"},
+    {file = "pydantic_core-2.16.3-cp311-none-win_arm64.whl", hash = "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9"},
+    {file = "pydantic_core-2.16.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff"},
+    {file = "pydantic_core-2.16.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975"},
+    {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2"},
+    {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120"},
+    {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053"},
+    {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b"},
+    {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade"},
+    {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e"},
+    {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca"},
+    {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf"},
+    {file = "pydantic_core-2.16.3-cp312-none-win32.whl", hash = "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe"},
+    {file = "pydantic_core-2.16.3-cp312-none-win_amd64.whl", hash = "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed"},
+    {file = "pydantic_core-2.16.3-cp312-none-win_arm64.whl", hash = "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6"},
+    {file = "pydantic_core-2.16.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01"},
+    {file = "pydantic_core-2.16.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7"},
+    {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48"},
+    {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a"},
+    {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8"},
+    {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d"},
+    {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9"},
+    {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c"},
+    {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8"},
+    {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5"},
+    {file = "pydantic_core-2.16.3-cp38-none-win32.whl", hash = "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a"},
+    {file = "pydantic_core-2.16.3-cp38-none-win_amd64.whl", hash = "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed"},
+    {file = "pydantic_core-2.16.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820"},
+    {file = "pydantic_core-2.16.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23"},
+    {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074"},
+    {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805"},
+    {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c"},
+    {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9"},
+    {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256"},
+    {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8"},
+    {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b"},
+    {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972"},
+    {file = "pydantic_core-2.16.3-cp39-none-win32.whl", hash = "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2"},
+    {file = "pydantic_core-2.16.3-cp39-none-win_amd64.whl", hash = "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf"},
+    {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c"},
+    {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a"},
+    {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241"},
+    {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183"},
+    {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad"},
+    {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"},
+    {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1"},
+    {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe"},
+    {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b"},
+    {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f"},
+    {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137"},
+    {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89"},
+    {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a"},
+    {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6"},
+    {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc"},
+    {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da"},
+    {file = "pydantic_core-2.16.3.tar.gz", hash = "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad"},
 ]
 
 [package.dependencies]
 typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
 
+[[package]]
+name = "pygments"
+version = "2.17.2"
+description = "Pygments is a syntax highlighting package written in Python."
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"},
+    {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"},
+]
+
+[package.extras]
+plugins = ["importlib-metadata"]
+windows-terminal = ["colorama (>=0.4.6)"]
+
 [[package]]
 name = "pytest"
-version = "7.4.3"
+version = "8.1.1"
 description = "pytest: simple powerful testing with Python"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"},
-    {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"},
+    {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"},
+    {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"},
 ]
 
 [package.dependencies]
 colorama = {version = "*", markers = "sys_platform == \"win32\""}
 iniconfig = "*"
 packaging = "*"
-pluggy = ">=0.12,<2.0"
+pluggy = ">=1.4,<2.0"
 
 [package.extras]
-testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
+testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
+
+[[package]]
+name = "pytest-asyncio"
+version = "0.23.6"
+description = "Pytest support for asyncio"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "pytest-asyncio-0.23.6.tar.gz", hash = "sha256:ffe523a89c1c222598c76856e76852b787504ddb72dd5d9b6617ffa8aa2cde5f"},
+    {file = "pytest_asyncio-0.23.6-py3-none-any.whl", hash = "sha256:68516fdd1018ac57b846c9846b954f0393b26f094764a28c955eabb0536a4e8a"},
+]
+
+[package.dependencies]
+pytest = ">=7.0.0,<9"
+
+[package.extras]
+docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
+testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
+
+[[package]]
+name = "pytest-clarity"
+version = "1.0.1"
+description = "A plugin providing an alternative, colourful diff output for failing assertions."
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+files = [
+    {file = "pytest-clarity-1.0.1.tar.gz", hash = "sha256:505fe345fad4fe11c6a4187fe683f2c7c52c077caa1e135f3e483fe112db7772"},
+]
+
+[package.dependencies]
+pprintpp = ">=0.4.0"
+pytest = ">=3.5.0"
+rich = ">=8.0.0"
 
 [[package]]
 name = "pytest-cov"
@@ -397,18 +1074,51 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale
 
 [[package]]
 name = "pytest-order"
-version = "1.1.0"
+version = "1.2.0"
 description = "pytest plugin to run your tests in a specific order"
 optional = false
 python-versions = ">=3.6"
 files = [
-    {file = "pytest-order-1.1.0.tar.gz", hash = "sha256:139d25b30826b78eebb42722f747eab14c44b88059d7a71d4f79d14a057269a5"},
-    {file = "pytest_order-1.1.0-py3-none-any.whl", hash = "sha256:3b3730969c97900fa5cd31ecff80847680ed56b2490954565c14949ba60d9371"},
+    {file = "pytest-order-1.2.0.tar.gz", hash = "sha256:944f86b6d441aa7b1da80f801c6ab65b84bbeba472d0a7a12eb43ba26650101a"},
+    {file = "pytest_order-1.2.0-py3-none-any.whl", hash = "sha256:9d65c3b6dc6d6ee984d6ae2c6c4aa4f1331e5b915116219075c888c8bcbb93b8"},
 ]
 
 [package.dependencies]
 pytest = {version = ">=6.2.4", markers = "python_version >= \"3.10\""}
 
+[[package]]
+name = "pytest-tap"
+version = "3.4"
+description = "Test Anything Protocol (TAP) reporting plugin for pytest"
+optional = false
+python-versions = "*"
+files = [
+    {file = "pytest-tap-3.4.tar.gz", hash = "sha256:a7c2a4a3e8b4bf18522e46d74208f8579a191dd972c59182104ad9a4967318fb"},
+    {file = "pytest_tap-3.4-py3-none-any.whl", hash = "sha256:d97a2115c94415086f6faec395d243b3c18ea846ce1c1653a4b2588082be35d8"},
+]
+
+[package.dependencies]
+pytest = ">=3.0"
+"tap.py" = ">=3.0,<4.0"
+
+[[package]]
+name = "rich"
+version = "13.7.1"
+description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
+optional = false
+python-versions = ">=3.7.0"
+files = [
+    {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"},
+    {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"},
+]
+
+[package.dependencies]
+markdown-it-py = ">=2.2.0"
+pygments = ">=2.13.0,<3.0.0"
+
+[package.extras]
+jupyter = ["ipywidgets (>=7.5.1,<9)"]
+
 [[package]]
 name = "sniffio"
 version = "1.3.0"
@@ -420,18 +1130,298 @@ files = [
     {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
 ]
 
+[[package]]
+name = "sqlalchemy"
+version = "2.0.28"
+description = "Database Abstraction Library"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "SQLAlchemy-2.0.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0b148ab0438f72ad21cb004ce3bdaafd28465c4276af66df3b9ecd2037bf252"},
+    {file = "SQLAlchemy-2.0.28-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bbda76961eb8f27e6ad3c84d1dc56d5bc61ba8f02bd20fcf3450bd421c2fcc9c"},
+    {file = "SQLAlchemy-2.0.28-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feea693c452d85ea0015ebe3bb9cd15b6f49acc1a31c28b3c50f4db0f8fb1e71"},
+    {file = "SQLAlchemy-2.0.28-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5da98815f82dce0cb31fd1e873a0cb30934971d15b74e0d78cf21f9e1b05953f"},
+    {file = "SQLAlchemy-2.0.28-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a5adf383c73f2d49ad15ff363a8748319ff84c371eed59ffd0127355d6ea1da"},
+    {file = "SQLAlchemy-2.0.28-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:56856b871146bfead25fbcaed098269d90b744eea5cb32a952df00d542cdd368"},
+    {file = "SQLAlchemy-2.0.28-cp310-cp310-win32.whl", hash = "sha256:943aa74a11f5806ab68278284a4ddd282d3fb348a0e96db9b42cb81bf731acdc"},
+    {file = "SQLAlchemy-2.0.28-cp310-cp310-win_amd64.whl", hash = "sha256:c6c4da4843e0dabde41b8f2e8147438330924114f541949e6318358a56d1875a"},
+    {file = "SQLAlchemy-2.0.28-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46a3d4e7a472bfff2d28db838669fc437964e8af8df8ee1e4548e92710929adc"},
+    {file = "SQLAlchemy-2.0.28-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0d3dd67b5d69794cfe82862c002512683b3db038b99002171f624712fa71aeaa"},
+    {file = "SQLAlchemy-2.0.28-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61e2e41656a673b777e2f0cbbe545323dbe0d32312f590b1bc09da1de6c2a02"},
+    {file = "SQLAlchemy-2.0.28-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0315d9125a38026227f559488fe7f7cee1bd2fbc19f9fd637739dc50bb6380b2"},
+    {file = "SQLAlchemy-2.0.28-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:af8ce2d31679006e7b747d30a89cd3ac1ec304c3d4c20973f0f4ad58e2d1c4c9"},
+    {file = "SQLAlchemy-2.0.28-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:81ba314a08c7ab701e621b7ad079c0c933c58cdef88593c59b90b996e8b58fa5"},
+    {file = "SQLAlchemy-2.0.28-cp311-cp311-win32.whl", hash = "sha256:1ee8bd6d68578e517943f5ebff3afbd93fc65f7ef8f23becab9fa8fb315afb1d"},
+    {file = "SQLAlchemy-2.0.28-cp311-cp311-win_amd64.whl", hash = "sha256:ad7acbe95bac70e4e687a4dc9ae3f7a2f467aa6597049eeb6d4a662ecd990bb6"},
+    {file = "SQLAlchemy-2.0.28-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d3499008ddec83127ab286c6f6ec82a34f39c9817f020f75eca96155f9765097"},
+    {file = "SQLAlchemy-2.0.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9b66fcd38659cab5d29e8de5409cdf91e9986817703e1078b2fdaad731ea66f5"},
+    {file = "SQLAlchemy-2.0.28-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bea30da1e76cb1acc5b72e204a920a3a7678d9d52f688f087dc08e54e2754c67"},
+    {file = "SQLAlchemy-2.0.28-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:124202b4e0edea7f08a4db8c81cc7859012f90a0d14ba2bf07c099aff6e96462"},
+    {file = "SQLAlchemy-2.0.28-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e23b88c69497a6322b5796c0781400692eca1ae5532821b39ce81a48c395aae9"},
+    {file = "SQLAlchemy-2.0.28-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b6303bfd78fb3221847723104d152e5972c22367ff66edf09120fcde5ddc2e2"},
+    {file = "SQLAlchemy-2.0.28-cp312-cp312-win32.whl", hash = "sha256:a921002be69ac3ab2cf0c3017c4e6a3377f800f1fca7f254c13b5f1a2f10022c"},
+    {file = "SQLAlchemy-2.0.28-cp312-cp312-win_amd64.whl", hash = "sha256:b4a2cf92995635b64876dc141af0ef089c6eea7e05898d8d8865e71a326c0385"},
+    {file = "SQLAlchemy-2.0.28-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e91b5e341f8c7f1e5020db8e5602f3ed045a29f8e27f7f565e0bdee3338f2c7"},
+    {file = "SQLAlchemy-2.0.28-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45c7b78dfc7278329f27be02c44abc0d69fe235495bb8e16ec7ef1b1a17952db"},
+    {file = "SQLAlchemy-2.0.28-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3eba73ef2c30695cb7eabcdb33bb3d0b878595737479e152468f3ba97a9c22a4"},
+    {file = "SQLAlchemy-2.0.28-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5df5d1dafb8eee89384fb7a1f79128118bc0ba50ce0db27a40750f6f91aa99d5"},
+    {file = "SQLAlchemy-2.0.28-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2858bbab1681ee5406650202950dc8f00e83b06a198741b7c656e63818633526"},
+    {file = "SQLAlchemy-2.0.28-cp37-cp37m-win32.whl", hash = "sha256:9461802f2e965de5cff80c5a13bc945abea7edaa1d29360b485c3d2b56cdb075"},
+    {file = "SQLAlchemy-2.0.28-cp37-cp37m-win_amd64.whl", hash = "sha256:a6bec1c010a6d65b3ed88c863d56b9ea5eeefdf62b5e39cafd08c65f5ce5198b"},
+    {file = "SQLAlchemy-2.0.28-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:843a882cadebecc655a68bd9a5b8aa39b3c52f4a9a5572a3036fb1bb2ccdc197"},
+    {file = "SQLAlchemy-2.0.28-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dbb990612c36163c6072723523d2be7c3eb1517bbdd63fe50449f56afafd1133"},
+    {file = "SQLAlchemy-2.0.28-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7e4baf9161d076b9a7e432fce06217b9bd90cfb8f1d543d6e8c4595627edb9"},
+    {file = "SQLAlchemy-2.0.28-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0a5354cb4de9b64bccb6ea33162cb83e03dbefa0d892db88a672f5aad638a75"},
+    {file = "SQLAlchemy-2.0.28-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:fffcc8edc508801ed2e6a4e7b0d150a62196fd28b4e16ab9f65192e8186102b6"},
+    {file = "SQLAlchemy-2.0.28-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aca7b6d99a4541b2ebab4494f6c8c2f947e0df4ac859ced575238e1d6ca5716b"},
+    {file = "SQLAlchemy-2.0.28-cp38-cp38-win32.whl", hash = "sha256:8c7f10720fc34d14abad5b647bc8202202f4948498927d9f1b4df0fb1cf391b7"},
+    {file = "SQLAlchemy-2.0.28-cp38-cp38-win_amd64.whl", hash = "sha256:243feb6882b06a2af68ecf4bec8813d99452a1b62ba2be917ce6283852cf701b"},
+    {file = "SQLAlchemy-2.0.28-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fc4974d3684f28b61b9a90fcb4c41fb340fd4b6a50c04365704a4da5a9603b05"},
+    {file = "SQLAlchemy-2.0.28-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:87724e7ed2a936fdda2c05dbd99d395c91ea3c96f029a033a4a20e008dd876bf"},
+    {file = "SQLAlchemy-2.0.28-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68722e6a550f5de2e3cfe9da6afb9a7dd15ef7032afa5651b0f0c6b3adb8815d"},
+    {file = "SQLAlchemy-2.0.28-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:328529f7c7f90adcd65aed06a161851f83f475c2f664a898af574893f55d9e53"},
+    {file = "SQLAlchemy-2.0.28-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:df40c16a7e8be7413b885c9bf900d402918cc848be08a59b022478804ea076b8"},
+    {file = "SQLAlchemy-2.0.28-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:426f2fa71331a64f5132369ede5171c52fd1df1bd9727ce621f38b5b24f48750"},
+    {file = "SQLAlchemy-2.0.28-cp39-cp39-win32.whl", hash = "sha256:33157920b233bc542ce497a81a2e1452e685a11834c5763933b440fedd1d8e2d"},
+    {file = "SQLAlchemy-2.0.28-cp39-cp39-win_amd64.whl", hash = "sha256:2f60843068e432311c886c5f03c4664acaef507cf716f6c60d5fde7265be9d7b"},
+    {file = "SQLAlchemy-2.0.28-py3-none-any.whl", hash = "sha256:78bb7e8da0183a8301352d569900d9d3594c48ac21dc1c2ec6b3121ed8b6c986"},
+    {file = "SQLAlchemy-2.0.28.tar.gz", hash = "sha256:dd53b6c4e6d960600fd6532b79ee28e2da489322fcf6648738134587faf767b6"},
+]
+
+[package.dependencies]
+greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""}
+typing-extensions = ">=4.6.0"
+
+[package.extras]
+aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"]
+aioodbc = ["aioodbc", "greenlet (!=0.4.17)"]
+aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"]
+asyncio = ["greenlet (!=0.4.17)"]
+asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"]
+mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"]
+mssql = ["pyodbc"]
+mssql-pymssql = ["pymssql"]
+mssql-pyodbc = ["pyodbc"]
+mypy = ["mypy (>=0.910)"]
+mysql = ["mysqlclient (>=1.4.0)"]
+mysql-connector = ["mysql-connector-python"]
+oracle = ["cx_oracle (>=8)"]
+oracle-oracledb = ["oracledb (>=1.0.1)"]
+postgresql = ["psycopg2 (>=2.7)"]
+postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"]
+postgresql-pg8000 = ["pg8000 (>=1.29.1)"]
+postgresql-psycopg = ["psycopg (>=3.0.7)"]
+postgresql-psycopg2binary = ["psycopg2-binary"]
+postgresql-psycopg2cffi = ["psycopg2cffi"]
+postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"]
+pymysql = ["pymysql"]
+sqlcipher = ["sqlcipher3_binary"]
+
+[[package]]
+name = "tap-py"
+version = "3.1"
+description = "Test Anything Protocol (TAP) tools"
+optional = false
+python-versions = "*"
+files = [
+    {file = "tap.py-3.1-py3-none-any.whl", hash = "sha256:928c852f3361707b796c93730cc5402c6378660b161114461066acf53d65bf5d"},
+    {file = "tap.py-3.1.tar.gz", hash = "sha256:3c0cd45212ad5a25b35445964e2517efa000a118a1bfc3437dae828892eaf1e1"},
+]
+
+[package.extras]
+yaml = ["PyYAML (>=5.1)", "more-itertools"]
+
+[[package]]
+name = "textual"
+version = "0.53.1"
+description = "Modern Text User Interface framework"
+optional = false
+python-versions = ">=3.8,<4.0"
+files = [
+    {file = "textual-0.53.1-py3-none-any.whl", hash = "sha256:32201aa9d334ed064d5e670f15fe3d7f19c736ca54cecb054a5b995691104434"},
+    {file = "textual-0.53.1.tar.gz", hash = "sha256:23ba673be7974819ded35ea88d28df7117987e53d58f15b2cc890ac2ecf56401"},
+]
+
+[package.dependencies]
+markdown-it-py = {version = ">=2.1.0", extras = ["linkify", "plugins"]}
+rich = ">=13.3.3"
+typing-extensions = ">=4.4.0,<5.0.0"
+
+[package.extras]
+syntax = ["tree-sitter (>=0.20.1,<0.21.0)", "tree_sitter_languages (>=1.7.0)"]
+
+[[package]]
+name = "textual-dev"
+version = "1.5.1"
+description = "Development tools for working with Textual"
+optional = false
+python-versions = ">=3.8,<4.0"
+files = [
+    {file = "textual_dev-1.5.1-py3-none-any.whl", hash = "sha256:bb37dd769ae6b67e1422aa97f6d6ef952e0a6d2aafe08327449e8bdd70474776"},
+    {file = "textual_dev-1.5.1.tar.gz", hash = "sha256:e0366ab6f42c128d7daa37a7c418e61fe7aa83731983da990808e4bf2de922a1"},
+]
+
+[package.dependencies]
+aiohttp = ">=3.8.1"
+click = ">=8.1.2"
+msgpack = ">=1.0.3"
+textual = ">=0.36.0"
+typing-extensions = ">=4.4.0,<5.0.0"
+
+[[package]]
+name = "typer"
+version = "0.9.0"
+description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
+optional = false
+python-versions = ">=3.6"
+files = [
+    {file = "typer-0.9.0-py3-none-any.whl", hash = "sha256:5d96d986a21493606a358cae4461bd8cdf83cbf33a5aa950ae629ca3b51467ee"},
+    {file = "typer-0.9.0.tar.gz", hash = "sha256:50922fd79aea2f4751a8e0408ff10d2662bd0c8bbfa84755a699f3bada2978b2"},
+]
+
+[package.dependencies]
+click = ">=7.1.1,<9.0.0"
+typing-extensions = ">=3.7.4.3"
+
+[package.extras]
+all = ["colorama (>=0.4.3,<0.5.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"]
+dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"]
+doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"]
+test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"]
+
 [[package]]
 name = "typing-extensions"
-version = "4.8.0"
+version = "4.10.0"
 description = "Backported and Experimental Type Hints for Python 3.8+"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"},
-    {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"},
+    {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"},
+    {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"},
 ]
 
+[[package]]
+name = "uc-micro-py"
+version = "1.0.2"
+description = "Micro subset of unicode data files for linkify-it-py projects."
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "uc-micro-py-1.0.2.tar.gz", hash = "sha256:30ae2ac9c49f39ac6dce743bd187fcd2b574b16ca095fa74cd9396795c954c54"},
+    {file = "uc_micro_py-1.0.2-py3-none-any.whl", hash = "sha256:8c9110c309db9d9e87302e2f4ad2c3152770930d88ab385cd544e7a7e75f3de0"},
+]
+
+[package.extras]
+test = ["coverage", "pytest", "pytest-cov"]
+
+[[package]]
+name = "yarl"
+version = "1.9.4"
+description = "Yet another URL library"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"},
+    {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"},
+    {file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"},
+    {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"},
+    {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"},
+    {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"},
+    {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"},
+    {file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"},
+    {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"},
+    {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"},
+    {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"},
+    {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"},
+    {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"},
+    {file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"},
+    {file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"},
+    {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"},
+    {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"},
+    {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"},
+    {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"},
+    {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"},
+    {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"},
+    {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"},
+    {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"},
+    {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"},
+    {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"},
+    {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"},
+    {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"},
+    {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"},
+    {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"},
+    {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"},
+    {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"},
+    {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"},
+    {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"},
+    {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"},
+    {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"},
+    {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"},
+    {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"},
+    {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"},
+    {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"},
+    {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"},
+    {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"},
+    {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"},
+    {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"},
+    {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"},
+    {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"},
+    {file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"},
+    {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"},
+    {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"},
+    {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"},
+    {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"},
+    {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"},
+    {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"},
+    {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"},
+    {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"},
+    {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"},
+    {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"},
+    {file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"},
+    {file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"},
+    {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"},
+    {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"},
+    {file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"},
+    {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"},
+    {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"},
+    {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"},
+    {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"},
+    {file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"},
+    {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"},
+    {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"},
+    {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"},
+    {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"},
+    {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"},
+    {file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"},
+    {file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"},
+    {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"},
+    {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"},
+    {file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"},
+    {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"},
+    {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"},
+    {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"},
+    {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"},
+    {file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"},
+    {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"},
+    {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"},
+    {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"},
+    {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"},
+    {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"},
+    {file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"},
+    {file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"},
+    {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"},
+    {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"},
+]
+
+[package.dependencies]
+idna = ">=2.0"
+multidict = ">=4.0"
+
+[extras]
+cli = []
+console = []
+
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.11"
-content-hash = "c39835f0564595eaa7cde3e93817d88e2632343de3bcd3876ad832fc5ff2d5f3"
+content-hash = "8db25ce7034277e23607e1066781044f94157396150fd24bfa30793ab74a3c89"
diff --git a/pyproject.toml b/pyproject.toml
index c04cabc3..81e67675 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,28 +1,76 @@
 [tool.poetry]
 name = "gallagher"
-version = "0.1.0"
-description = "Python idiomatic client for Gallagher Command Centre API"
+version = "0.1.0-alpha.2"
+description = "Python idiomatic client and tools for Gallagher Command Centre API"
 authors = ["Dev Mukherjee <devraj@gmail.com>"]
 readme = "README.md"
+license = "MIT"
+repository = "https://github.com/anomaly/gallagher"
+documentation = "https://anomaly.github.io/gallagher/"
+keywords = ["gallagher", "rest", "api"]
+classifiers = [
+    "License :: OSI Approved :: MIT License",
+    "Topic :: Software Development :: Build Tools",
+    "Topic :: Software Development :: Libraries :: Python Modules",
+    "Operating System :: OS Independent",
+    "Framework :: Pydantic",
+    "Framework :: Pydantic :: 2",
+]
+
+[tool.poetry.urls]
+"Bug Tracker" = "https://github.com/anomaly/gallagher/issues"
+
+[tool.poetry.extras]
+cli = ["asyncer", "typer", "rich"]
+console = ["textual"]
 
 [tool.poetry.dependencies]
 python = "^3.11"
-httpx = "^0.24.1"
-pydantic = "^2.4.2"
-typing-extensions = "^4.8.0"
-coverage = "^7.3.2"
-pytest = "^7.4.3"
-pytest-order = "^1.1.0"
-anyio = "^4.0.0"
+httpx = "^0.27.0"
+pydantic = "^2.6.4"
+typing-extensions = "^4.10.0"
 annotated-types = "^0.6.0"
-certifi = "^2023.7.22"
-
+certifi = "^2024.2.2"
+idna = "^3.6"
+packaging = "^23.2"
+pluggy = "^1.3.0"
+anyio = "^4.3.0"
+aiohttp = "^3.9.3"
 
 [tool.poetry.group.dev.dependencies]
-pytest = "^7.2.2"
-coverage = "^7.2.1"
-pytest-order = "^1.0.1"
 pytest-cov = "^4.1.0"
+coverage = "^7.4.0"
+pytest = "^8.1.1"
+pytest-order = "^1.2.0"
+pytest-tap = "^3.4"
+pytest-asyncio = "^0.23.6"
+pytest-clarity = "^1.0.1"
+textual-dev = "^1.4.0"
+
+
+[tool.poetry.group.cli.dependencies]
+asyncer = "^0.0.3"
+typer = "^0.9.0"
+rich = "^13.7.1"
+
+
+[tool.poetry.group.tui.dependencies]
+textual = "^0.53.1"
+textual-dev = "^1.5.1"
+
+
+[tool.poetry.group.sync.dependencies]
+sqlalchemy = "^2.0.28"
+alembic = "^1.13.1"
+
+
+[tool.poetry.group.test.dependencies]
+coverage = "^7.4.3"
+pytest-asyncio = "^0.23.5.post1"
+
+[tool.poetry.scripts]
+gal = "gallagher.cli:app"
+gcon = "gallagher.tui:main"
 
 [build-system]
 requires = ["poetry-core"]


Test patch:
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
new file mode 100644
index 00000000..602ae405
--- /dev/null
+++ b/.github/workflows/run-tests.yml
@@ -0,0 +1,33 @@
+name: Testing
+on:
+  push:
+    branches:
+      - master
+  pull_request:
+    branches:
+      - master
+
+jobs:
+  test:
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+      - name: Setup Python
+        uses: actions/setup-python@v4
+        with:
+          python-version-file: pyproject.toml
+      - name: Install Task
+        uses: arduino/setup-task@v1
+        with:
+          version: 3.x
+          repo-token: ${{ secrets.GITHUB_TOKEN }}
+      - name: Install dependencies
+        run: |
+          pip install poetry
+          poetry install
+      - name: Run tests
+        run: |
+          export GACC_API_KEY=${{ secrets.GACC_API_KEY }}
+          task test
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 00000000..d280de04
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,2 @@
+[pytest]
+asyncio_mode = auto
\ No newline at end of file
diff --git a/tests/__init__.py b/tests/__init__.py
index 62bb40b8..243b2176 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -3,8 +3,17 @@
   Test suite for the Gallagher Python idiomatic client library,
   using pytest run via Taskfile.
 
+  The pytest.mark.asyncio marker can be omitted entirely in auto mode 
+  where the asyncio marker is added automatically to async test functions.
+  
+  Refer to pytest.ini for configuration
+  https://pytest-asyncio.readthedocs.io/en/latest/reference/markers/index.html
+
+  TODO: check if setup and teardown can be turned into async
+
 """
 
+
 def setup_module(module):
     """ The Gallagher API client requires a test key, this is 
     set in the environment variable GACC_API_KEY.
@@ -14,7 +23,7 @@ def setup_module(module):
     """
     import os
     api_key = os.environ.get("GACC_API_KEY")
-    
+
     from gallagher import cc
     cc.api_key = api_key
 
diff --git a/tests/test_alarm.py b/tests/test_alarm.py
new file mode 100644
index 00000000..b304123b
--- /dev/null
+++ b/tests/test_alarm.py
@@ -0,0 +1,22 @@
+""" Alarms are raised by the command centre, we want
+to make sure that we are getting valid responses.
+
+"""
+
+
+async def test_alarms_list():
+    """ Get a list of item types and iterates through it
+    these are a summary response
+
+    """
+    from gallagher.cc.alarms import (
+        Alarms
+    )
+    from gallagher.dto.response import (
+        AlarmResponse
+    )
+
+    response = await Alarms.list()
+    assert type(response) is AlarmResponse
+    assert type(response.alarms) is list
+    assert len(response.alarms) > 0
diff --git a/tests/test_card_types.py b/tests/test_card_types.py
index 08f6c8ee..0325b5c5 100644
--- a/tests/test_card_types.py
+++ b/tests/test_card_types.py
@@ -3,11 +3,15 @@
 """
 
 
-def test_get_card_types():
-    from gallagher.cc.card.card_type import CardType
-    from gallagher.dto.card_type import CardTypeResponse
+async def test_get_card_types():
+    from gallagher.cc.cardholders.card_type import (
+        CardType
+    )
+    from gallagher.dto.response import (
+        CardTypeResponse
+    )
 
-    response = CardType.list()
+    response = await CardType.list()
     assert type(response) is CardTypeResponse
     assert type(response.results) is list
     assert len(response.results) > 0
diff --git a/tests/test_cardholder.py b/tests/test_cardholder.py
index 2eb2788d..f515e6ad 100644
--- a/tests/test_cardholder.py
+++ b/tests/test_cardholder.py
@@ -1,3 +1,42 @@
+""" Cardholders
+
 """
 
-"""
\ No newline at end of file
+
+async def test_cardholder_list():
+
+    from gallagher.cc.cardholders.cardholders import (
+        Cardholder
+    )
+    from gallagher.dto.response import (
+        CardholderSummaryResponse
+    )
+
+    response = await Cardholder.list()
+    assert type(response) is CardholderSummaryResponse
+    assert type(response.results) is list
+    assert len(response.results) > 0
+
+
+async def test_cardholder_detail():
+
+    from gallagher.cc.cardholders.cardholders import (
+        Cardholder
+    )
+    from gallagher.dto.detail import (
+        CardholderDetail,
+    )
+    from gallagher.dto.response import (
+        CardholderSummaryResponse,
+    )
+
+    response = await Cardholder.list()
+    assert type(response) is CardholderSummaryResponse
+
+    for cardholder_summary in response.results:
+        # Get the detail of the cardholder for comparison
+        cardholder_detail_response = await Cardholder.retrieve(
+            cardholder_summary.id
+        )
+        assert type(cardholder_detail_response) is CardholderDetail
+        assert (cardholder_detail_response.id == cardholder_summary.id)
diff --git a/tests/test_day_category.py b/tests/test_day_category.py
index b36947fc..813e625c 100644
--- a/tests/test_day_category.py
+++ b/tests/test_day_category.py
@@ -3,12 +3,16 @@
 """
 
 
-def test_day_category():
+async def test_day_category():
 
-    from gallagher.cc.alarms.day_category import DayCategory
-    from gallagher.dto.day_category import DayCategoryResponse
+    from gallagher.cc.alarms.day_category import (
+        DayCategory
+    )
+    from gallagher.dto.response import (
+        DayCategoryResponse
+    )
 
-    response = DayCategory.list()
+    response = await DayCategory.list()
     assert type(response) is DayCategoryResponse
     assert type(response.results) is list
     assert len(response.results) > 0
diff --git a/tests/test_discover.py b/tests/test_discover.py
new file mode 100644
index 00000000..29932a01
--- /dev/null
+++ b/tests/test_discover.py
@@ -0,0 +1,19 @@
+""" Discover the API.
+
+"""
+
+
+async def test_discover():
+    """
+
+    """
+    from gallagher.cc.core import APIEndpoint
+
+    from gallagher.dto.detail import (
+        FeaturesDetail,
+    )
+    from gallagher.dto.response import (
+        DiscoveryResponse,
+    )
+
+    assert 1 == 1
diff --git a/tests/test_divisions.py b/tests/test_divisions.py
index b01e8280..1aeb9954 100644
--- a/tests/test_divisions.py
+++ b/tests/test_divisions.py
@@ -1,8 +1,34 @@
-# def test_division_list():
-#     from gallagher.cc import Division
-#     from gallagher.schema import Response
-
-#     response = Division.list()
-#     assert type(response) is Response
-#     assert type(response.results) is list
-#     assert len(response.results) > 0
\ No newline at end of file
+async def test_division_list():
+    from gallagher.cc.alarms.divisions import (
+        Division
+    )
+    from gallagher.dto.response import (
+        DivisionDetailResponse
+    )
+
+    response = await Division.list()
+    assert type(response) is DivisionDetailResponse
+    assert type(response.results) is list
+    assert len(response.results) > 0
+
+
+async def test_division_detail():
+    from gallagher.cc.alarms.divisions import (
+        Division
+    )
+    from gallagher.dto.detail import (
+        DivisionDetail,
+    )
+    from gallagher.dto.response import (
+        DivisionDetailResponse,
+    )
+
+    response = await Division.list()
+    assert type(response) is DivisionDetailResponse
+    assert type(response.results) is list
+
+    for division_summary in response.results:
+        # Get the detail of the division
+        division_detail_response = await Division.retrieve(division_summary.id)
+        assert type(division_detail_response) is DivisionDetail
+        assert division_detail_response.id == division_summary.id
diff --git a/tests/test_events.py b/tests/test_events.py
new file mode 100644
index 00000000..3a04cd7f
--- /dev/null
+++ b/tests/test_events.py
@@ -0,0 +1,26 @@
+
+
+async def test_event_types():
+
+    from gallagher.cc.alarms.events import (
+        EventType
+    )
+    from gallagher.dto.response import (
+        EventTypeResponse,
+    )
+
+    response = await EventType.list()
+    assert type(response) is EventTypeResponse
+
+
+async def test_event_summary():
+
+    from gallagher.cc.alarms.events import (
+        Event
+    )
+    from gallagher.dto.response import (
+        EventSummaryResponse,
+    )
+
+    response = await Event.list()
+    assert type(response) is EventSummaryResponse
diff --git a/tests/test_items.py b/tests/test_items.py
index dd39f7e6..2003931e 100644
--- a/tests/test_items.py
+++ b/tests/test_items.py
@@ -4,46 +4,61 @@
 """
 
 
-def test_items_types_list():
+async def test_items_types_list():
     """ Get a list of item types and iterates through it
     these are a summary response
 
     """
-    from gallagher.cc.alarms.items import ItemsTypes
-    from gallagher.dto.items import ItemTypesResponse
+    from gallagher.cc.alarms.items import (
+        ItemsTypes
+    )
+    from gallagher.dto.response import (
+        ItemTypesResponse
+    )
 
-    response = ItemsTypes.list()
+    response = await ItemsTypes.list()
     assert type(response) is ItemTypesResponse
     assert type(response.item_types) is list
     assert len(response.item_types) > 0
 
 
-def test_items_list():
+async def test_items_list():
     """ Get a list of items and this should feed into fetching
     each one of these on it's own.
 
     """
-    from gallagher.cc.alarms.items import Item
-    from gallagher.dto.items import ItemsSummaryResponse
+    from gallagher.cc.alarms.items import (
+        Item
+    )
+    from gallagher.dto.response import (
+        ItemsSummaryResponse
+    )
 
-    response = Item.list()
+    response = await Item.list()
     assert type(response) is ItemsSummaryResponse
     assert type(response.results) is list
     assert len(response.results) > 0
 
 
-def test_item_detail():
+async def test_item_detail():
     """ Get each item in the list and make sure it's a valid item
 
     """
-    from gallagher.cc.alarms.items import Item
-    from gallagher.dto.items import ItemsSummaryResponse, \
-        ItemDetail
+    from gallagher.cc.alarms.items import (
+        Item
+    )
+    from gallagher.dto.summary import (
+        ItemSummary,
+    )
+    from gallagher.dto.response import (
+        ItemsSummaryResponse,
+    )
 
-    response: ItemsSummaryResponse = Item.list()
+    response: ItemsSummaryResponse = await Item.list()
     assert type(response) is ItemsSummaryResponse
 
     for item_summary in response.results:
         # Get the detail of the item
-        item_detail_response = Item.retrieve(item_summary.id)
-        assert type(item_detail_response) is ItemDetail
+        item_detail_response = await Item.retrieve(item_summary.id)
+        assert type(item_detail_response) is ItemSummary
+        assert (item_detail_response.id == item_summary.id)
diff --git a/tests/test_pdf.py b/tests/test_pdf.py
new file mode 100644
index 00000000..f9bf0b31
--- /dev/null
+++ b/tests/test_pdf.py
@@ -0,0 +1,13 @@
+async def test_pdf_list():
+
+    from gallagher.cc.cardholders.cardholders import (
+        PdfDefinition
+    )
+    from gallagher.dto.response import (
+        PdfResponse
+    )
+
+    response = await PdfDefinition.list()
+    assert type(response) is PdfResponse
+    assert type(response.results) is list
+    assert len(response.results) > 0
diff --git a/tests/test_schedule.py b/tests/test_schedule.py
index 7549a546..ac92cb80 100644
--- a/tests/test_schedule.py
+++ b/tests/test_schedule.py
@@ -4,12 +4,16 @@
 """
 
 
-def test_schedules_list():
+async def test_schedules_list():
 
-    from gallagher.cc.alarms.schedule import Schedule
-    from gallagher.dto.schedule import ScheduleSummaryResponse
+    from gallagher.cc.alarms.schedule import (
+        Schedule
+    )
+    from gallagher.dto.response import (
+        ScheduleSummaryResponse,
+    )
 
-    response = Schedule.list()
+    response = await Schedule.list()
     assert type(response) is ScheduleSummaryResponse
     assert type(response.results) is list
     assert len(response.results) > 0
diff --git a/tests/test_version.py b/tests/test_version.py
index 8b0c9991..77e8770f 100644
--- a/tests/test_version.py
+++ b/tests/test_version.py
@@ -1,7 +1,10 @@
-"""
+""" Tests to see that are running the tests against the right version
+
+
 """
 
 from gallagher import __version__
 
-def test_version():
-    assert __version__ == '0.1.0'
+
+async def test_version():
+    assert __version__ == '0.1.0-alpha.3'