Repository: anomaly/gallagher (language: python)

Issue:
Synchronisation of command centre data with a local data source (preferably a SQL backend)
Consider the [Cardholder changes](https://gallaghersecurity.github.io/cc-rest-docs/ref/cardholders.html) endpoint which essentially provides a list of events that occurred against a cardholder. These sorts of endpoints provide valuable information for third party integrations such as the ability to provide a history of when a customer has accessed a door.

Gallagher cloud does not provide the ability to add `webhooks` to receive events as they occur (which probably makes sense from a deployment and possibly security perspective).

Built on top of the base API client we should provide the ability to synchronise with the command centre, these would have to be written to a backend to keep a shadow of what is available on the command centre. The ida of this is not to query the command centre when the application is rendering interfaces.

This ticket has to be expanded with the details, possibly requires an RFC.
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
Provide a set of examples references from the documentation
**Is your feature request related to a problem? Please describe.**
It would be good to have runnable `examples` for the end user to be able to almost copy and paste and run on their console. Particularly when it comes to using the API client.

**Describe the solution you'd like**
Inspired by the developer experience provided by companies like Stripe who do an excellent job at providing accurate examples of runnable code.

While we reference examples in our documentation we should also maintain a folder of `examples` which the developers can vendor in.

Note that we have a strong set of `tests` that we can draw from and the `cli` demonstrates the use of most of the endpoints.

The aim of this is to extend these working examples and keep them in sync.

> A point to consider is how we are going to keep these up to date as the project evolves.

**Describe alternatives you've considered**
NA

**Additional context**
See projects shillelagh for [examples](https://github.com/betodealmeida/shillelagh/tree/main/examples)
Synchronisation of command centre data with a local data source (preferably a SQL backend)
Consider the [Cardholder changes](https://gallaghersecurity.github.io/cc-rest-docs/ref/cardholders.html) endpoint which essentially provides a list of events that occurred against a cardholder. These sorts of endpoints provide valuable information for third party integrations such as the ability to provide a history of when a customer has accessed a door.

Gallagher cloud does not provide the ability to add `webhooks` to receive events as they occur (which probably makes sense from a deployment and possibly security perspective).

Built on top of the base API client we should provide the ability to synchronise with the command centre, these would have to be written to a backend to keep a shadow of what is available on the command centre. The ida of this is not to query the command centre when the application is rendering interfaces.

This ticket has to be expanded with the details, possibly requires an RFC.
Provide a SQL (preferably a SQLAlchemy `dialect`) interface to query the REST API
As our ambition around the library grows, the sync feature #9 looks to be a lot more useful than I previously imagined. One of my thoughts was to treat this as if they were two databases sources and use a layer like SQLAlchemy to keep them in sync.

Sync is a difficult problem (see also [SQL Sync for Schema with SQLAlchemy](https://www.dolthub.com/blog/2020-08-24-schema-support-in-sql-sync/)
[python-sync-db](https://github.com/bintlabs/python-sync-db)) at the best of times. My initial research lead me down the path of writing a [SQLAlchemy dialect](https://docs.sqlalchemy.org/en/20/dialects/#external-dialects) primarily thinking about if it's possible to wrap a REST service as a SQL source.

I found a number of articles and discussions which leads to believe this is a possible way forward:
- [How to add support for a new SQLAlchemy dialect](https://legacy.docs.greatexpectations.io/en/latest/guides/how_to_guides/miscellaneous/how_to_add_and_test_a_new_sqlalchemydataset_class.html)
- Discussion on Stackoverflow on [How to write my own dialect in sqlalchemy to adapt HTTP API?](https://stackoverflow.com/questions/43929132/how-to-write-my-own-dialect-in-sqlalchemy-to-adapt-http-api)

Digging through the list of [external dialects](https://docs.sqlalchemy.org/en/20/dialects/#external-dialects) I found [betodealmeida](https://github.com/betodealmeida) / [gsheets-db-api](https://github.com/betodealmeida/gsheets-db-api) which provides that we can converse with a REST API via a SQLAlchemy dialect. This project links to [Shillelagh](https://github.com/betodealmeida/shillelagh/?tab=readme-ov-file) which is a library is an implementation of the [Python DB API 2.0](https://www.python.org/dev/peps/pep-0249/) based on [SQLite](https://sqlite.org/index.html) (using the [APSW](https://rogerbinns.github.io/apsw/) library)

Other resources:
- [Dolt](https://www.dolthub.com) version controlled MySQL database

The requirement is thus to research the above resources and outline the possibility of using a SQLALchemy dialect to write the sync module with the view of being able to use the REST client to communicate with the Gallagher proxy.

## Why SQL?

One of the major questions around this is `Why SQL?`, we could head down the route of using an object database. The question to consider is if the end user / customer would benefit from the data being available in an object database?

With a foundation of writing to SQL backend we also stand the advantage of writing to / reading from corporate database backends.
Design pattern for `next`, `previous`, and `updates` endpoints
The library now uses `async` methods, hence we should be able to design a nice pattern that allows us to follow `next`, `previous`, and `updates` endpoints.

These are available as part of a response, hence we should be able to ask the response to follow the responses in either direction.

The real challenge here is that the URLs are dynamically generated so we have to discover the URLs once the original response is received. 
Design pattern for `next`, `previous`, and `updates` endpoints
The library now uses `async` methods, hence we should be able to design a nice pattern that allows us to follow `next`, `previous`, and `updates` endpoints.

These are available as part of a response, hence we should be able to ask the response to follow the responses in either direction.

The real challenge here is that the URLs are dynamically generated so we have to discover the URLs once the original response is received. 
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.
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.
Provide a SQL (preferably a SQLAlchemy `dialect`) interface to query the REST API
As our ambition around the library grows, the sync feature #9 looks to be a lot more useful than I previously imagined. One of my thoughts was to treat this as if they were two databases sources and use a layer like SQLAlchemy to keep them in sync.

Sync is a difficult problem (see also [SQL Sync for Schema with SQLAlchemy](https://www.dolthub.com/blog/2020-08-24-schema-support-in-sql-sync/)
[python-sync-db](https://github.com/bintlabs/python-sync-db)) at the best of times. My initial research lead me down the path of writing a [SQLAlchemy dialect](https://docs.sqlalchemy.org/en/20/dialects/#external-dialects) primarily thinking about if it's possible to wrap a REST service as a SQL source.

I found a number of articles and discussions which leads to believe this is a possible way forward:
- [How to add support for a new SQLAlchemy dialect](https://legacy.docs.greatexpectations.io/en/latest/guides/how_to_guides/miscellaneous/how_to_add_and_test_a_new_sqlalchemydataset_class.html)
- Discussion on Stackoverflow on [How to write my own dialect in sqlalchemy to adapt HTTP API?](https://stackoverflow.com/questions/43929132/how-to-write-my-own-dialect-in-sqlalchemy-to-adapt-http-api)

Digging through the list of [external dialects](https://docs.sqlalchemy.org/en/20/dialects/#external-dialects) I found [betodealmeida](https://github.com/betodealmeida) / [gsheets-db-api](https://github.com/betodealmeida/gsheets-db-api) which provides that we can converse with a REST API via a SQLAlchemy dialect. This project links to [Shillelagh](https://github.com/betodealmeida/shillelagh/?tab=readme-ov-file) which is a library is an implementation of the [Python DB API 2.0](https://www.python.org/dev/peps/pep-0249/) based on [SQLite](https://sqlite.org/index.html) (using the [APSW](https://rogerbinns.github.io/apsw/) library)

Other resources:
- [Dolt](https://www.dolthub.com) version controlled MySQL database

The requirement is thus to research the above resources and outline the possibility of using a SQLALchemy dialect to write the sync module with the view of being able to use the REST client to communicate with the Gallagher proxy.

## Why SQL?

One of the major questions around this is `Why SQL?`, we could head down the route of using an object database. The question to consider is if the end user / customer would benefit from the data being available in an object database?

With a foundation of writing to SQL backend we also stand the advantage of writing to / reading from corporate database backends.
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.
Personal Data Fields being parsed ahead of exposing PDF aliases
Major refactor of the way `personalDataDefinitions` are parsed for the cardholder, refs:
- #1 

Initially I was going down a path of implementing a cache of the personalDataFields which is unnecessary as the cardholder detail response will always return what we need. Hence it makes sense to parse the personalDataDefinitions (see [commit](https://github.com/anomaly/gallagher/commit/1c06118baecfffb3103ff2508bf91f9015c4322a)) and then make the top level keys accessible as aliases.


Discussion / hints:

While experimenting with responses to implement #5 I found that the discovery endpoint does not return an endpoint for features not licensed by the instance.

We should depend on the responses being set to `None` for the particular endpoints and an exception being thrown by the client.





I would really like a very `pyndatic` solution to this, and hence have [started a discussion](https://github.com/pydantic/pydantic/discussions/8596) on the pydantic Github project. 

Post a resolution this should be merged in our documentation.
For the purposes of the Gallagher API, it should be noted that 
```
https://commandcentre-api-au.security.gallagher.cloud/api/personal_data_fields/
```
returns a valid set of Personal Data Fields for the command centre we are communicating with. I propose that we call and `memoize` #14 the response and use this as part of the validation process to ensure that unexpected keys don't pass through the validation process.
I would really like a very `pyndatic` solution to this, and hence have [started a discussion](https://github.com/pydantic/pydantic/discussions/8596) on the pydantic Github project. 

Post a resolution this should be merged in our documentation.
For the purposes of the Gallagher API, it should be noted that 
```
https://commandcentre-api-au.security.gallagher.cloud/api/personal_data_fields/
```
returns a valid set of Personal Data Fields for the command centre we are communicating with. I propose that we call and `memoize` #14 the response and use this as part of the validation process to ensure that unexpected keys don't pass through the validation process.

I would really like a very `pyndatic` solution to this, and hence have [started a discussion](https://github.com/pydantic/pydantic/discussions/8596) on the pydantic Github project. 

Post a resolution this should be merged in our documentation.
For the purposes of the Gallagher API, it should be noted that 
```
https://commandcentre-api-au.security.gallagher.cloud/api/personal_data_fields/
```
returns a valid set of Personal Data Fields for the command centre we are communicating with. I propose that we call and `memoize` #14 the response and use this as part of the validation process to ensure that unexpected keys don't pass through the validation process.


Fix patch:
diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml
index 61ab6a7a..897bfbba 100644
--- a/.github/workflows/deploy-docs.yml
+++ b/.github/workflows/deploy-docs.yml
@@ -14,5 +14,6 @@ jobs:
         with:
           python-version: 3.x
       - run: pip install mkdocs
-      - run: pip install mkdocs-terminal
+      - run: pip install mkdocs-material
+      - run: pip install mkdocs-typer
       - run: cd docs && mkdocs gh-deploy --force --clean --verbose
diff --git a/.gitignore b/.gitignore
index 71f79760..4dc82914 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
+# SQlite databases
+*.sqlite
+
 # managed by direnv
 .envrc
 
diff --git a/CITATION.cff b/CITATION.cff
new file mode 100644
index 00000000..5b10a096
--- /dev/null
+++ b/CITATION.cff
@@ -0,0 +1,7 @@
+@misc{gallagher_github,
+    author = {Anomaly},
+    title = {Gallagher},
+    howpublished = {GitHub Repository},
+    url = {https://github.com/anomaly/gallagher},
+    note = {Accessed: 2024-05-02},
+}
diff --git a/CLI.md b/CLI.md
new file mode 100644
index 00000000..a66dddb6
--- /dev/null
+++ b/CLI.md
@@ -0,0 +1,47 @@
+# Command Line Interface
+
+> The CLI is developed using [Typer](https://typer.tiangolo.com) which builds on the [infamous](https://click.palletsprojects.com/en/8.1.x/)
+
+Over the course of developing the Python idiomatic client for Gallagher, it became evident to us that the primary utility of the application will come from use facing tools that make the library immediately useful. This document outlines the scope of intent of the CLI and what feature it will provide (or initially anyway).
+
+## Objectives
+
+A big motivation for building this client library is efficiency in integrating Gallagher Command Centre into other systems. Integration primarily presents itself in the following forms:
+
+- Querying the state of the Command Centre and reacting to the information
+- Mutating the state of the system (e.g add a card holder, pass, access group, etc)
+- Synchronizing the information back to ta local system (talked in detail about in [SQL](SQL.md))
+
+While the API client is designed for structured programmatic access to the API, these use facing tools are designed to automate everyday use cases without needing to code.
+
+## Audience
+
+While the REST API client is aimed at developers (primarily Python developers) the CLI is intended for use by higher level uses for automation. The CLI still requires some technical knowledge and it targeted towards high level system integrator to build automation around the Gallagher ecosystem.
+
+## Design
+
+Following the [Unix philosophy](https://en.wikipedia.org/wiki/Unix_philosophy) of `Make each program do one thing well.` or summarised by [Peter H. Salus](https://en.wikipedia.org/wiki/Peter_H._Salus) as:
+
+- Write programs that do one thing and do it well.
+- Write programs to work together.
+- Write programs to handle text streams, because that is a universal interface.
+
+Each component presented by this project aims to do just that.
+
+> The primary objective of the CLI is to interact with the JSON API through the command line so users can build high level automation without having to interact directly with the JSON payloads. It uses the Python API client to ensure strict data rules when interacting with the API endpoints, thus preventing corruption.
+
+The project must provide complete documentation for the tool to be useful to an end user / system administrator.
+
+## Scope
+
+The initial version 
+
+- Cardholder
+- Visits
+- Alarms
+
+
+
+## Resources
+
+- [Command Line Interface Guidelines - clig.dev](https://clig.dev)
\ No newline at end of file
diff --git a/HARDWARE.md b/HARDWARE.md
new file mode 100644
index 00000000..590633eb
--- /dev/null
+++ b/HARDWARE.md
@@ -0,0 +1,28 @@
+# Hardware
+
+The following are some notes and resources I used to get our [Controller 7000 Single door](https://products.security.gallagher.com/security/global/en/products/access-control-hardware/controller-7000-single-door/p/BVC400011) PoE+ controller with a [T15 Multi Tech Reader](https://products.security.gallagher.com/security/global/en/products/access-control-hardware/readers-and-terminals/t15-multi-tech-reader/p/BVC300480) online with our Command Centre hosted on an AWS EC2 instance.
+
+> [!WARNING]\
+> Our setup was completely for demonstration purposes and was never intended for use in a production environment. Please consult a professional for any production setups.
+
+A big thank you to [Gallagher Security](http://security.gallagher.com) for providing the hardware and software for this project.
+
+## Shopping list
+
+- [Controller 7000 Single door](https://products.security.gallagher.com/security/global/en/products/access-control-hardware/controller-7000-single-door/p/BVC400011)
+- [T15 Multi Tech Reader](https://products.security.gallagher.com/security/global/en/products/access-control-hardware/readers-and-terminals/t15-multi-tech-reader/p/BVC300480)
+- [Gallagher Command Centre](http://security.gallagher.com) software
+- [Gallagher Mobile Connect](http://security.gallagher.com) app
+
+## Reference videos
+
+Official video tutorials from Gallagher Security:
+
+- [Getting a Controller Online](https://www.youtube.com/watch?v=qDNjwEC2blU)
+- [Getting a Controller Online using webpage adressing](https://www.youtube.com/watch?v=a4CCq_Ip0LQ)
+
+Official documentation:
+
+- [Controller 7000 Single door](https://products.security.gallagher.com/security/global/en/products/access-control-hardware/controller-7000-single-door/p/BVC400011)
+
+- [Controller 7000 Single door documents](https://media.gallagher.com/web/1a00857c7c9c8d7/single-door-controller-7000-qr-code/?viewType=grid)
diff --git a/README.md b/README.md
index acb14499..61d261c0 100644
--- a/README.md
+++ b/README.md
@@ -1,30 +1,126 @@
 # Gallagher Python Toolkit
 
-> Python idiomatic REST API client, a command line interface and a text based console for Gallagher Command Centre API
+> The missing toolkit for extending Gallagher Command Centre
 
-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.
+[![PyPI version](https://badge.fury.io/py/gallagher.svg)](https://badge.fury.io/py/gallagher)
+[![Python Version](https://img.shields.io/pypi/pyversions/gallagher)](https://pypi.org/project/gallagher/)
+[![Build Status](https://github.com/anomaly/gallagher/actions/workflows/run-tests.yml/badge.svg?branch=master)](https://github.com/anomaly/gallagher/actions?query=branch%3Amaster)
 
-This API client is a Python wrapper around their REST API and is designed to work locally or via the Cloud API Gateway.
+<div align="center">
+<img src="https://raw.githubusercontent.com/anomaly/gallagher/master/assets/logo-gpt.png" alt="Gallagher Python Toolkit Logo" height=128 width=128/>
+</div>
+
+Gallagher Security manufacture a variety of perimeter [security products](https://security.gallagher.com). At the hear of these is the [Command Centre](https://products.security.gallagher.com/security/au/en_AU/products/software/command-centre/p/C201311) software. Command Centre is deployed locally (in simplistic terms, the complexity varies for every use case). Version `8.6` introduced a REST API which allows you to interact with the system via HTTP requests locally or via Gallagher's [Cloud API Gateway](https://gallaghersecurity.github.io/docs/Command%20Centre%20Cloud%20Api%20Gateway%20TIP.pdf) which eliminates the need for maintaining proxies and VPNs.
+
+Our Python Toolkit focuses on enhancing the developer experience (DX) around the REST API. In principle we provide the following:
+
+- **Python SDK** an idiomatic client (including `asyncio` support) to extend the CC functionality.
+- **Command Line Interface** (CLI) to build powerful pipeline-based workflows.
+- **Terminal User Interface** (TUI) for easy interactions with the Command Centre.
+- **SQL interface** query the REST API as if it were a database or interact with via an ORM.
+
+> [!NOTE]\
+> This project is **NOT** affiliated with Gallagher Security. All trademarks are the property of their respective owners.
 
 While Gallagher maintain a set of [Swagger definitions](https://github.com/gallaghersecurity/cc-rest-docs) for their API, they are primarily intended to generate the documentation [published on Github](https://gallaghersecurity.github.io/cc-rest-docs/ref/index.html). They use a tool called [Spectacle](https://github.com/sourcey/spectacle). Gallagher explicitly state that the Swagger definitions are not intended to be used to generate code. Due to this the API client is hand built and not auto-generated.
 
+> [!IMPORTANT]\
 > Due to custom annotations the YAML files will not parse with any standard parser.
 
-The client was designed while building products around the Gallagher API. It's design is highly opinionated and does not conform with how Gallagher design software interfaces. If you've worked with [stripe-python](https://github.com/stripe/stripe-python) the syntax may feel familiar.
+Everything this project provides hinges upon our Python SDK, designed to enhance the developer experience. It's design is highly opinionated from our experience in building APIs, we ensure conformance with Gallagher software design interfaces.
+
+> [!TIP]\
+> If you've worked with [stripe-python](https://github.com/stripe/stripe-python) the syntax may feel familiar.
+
+If you are using one of our user facing tools, it's not important for you to understand how the SDK works, however since it underpins everything, here's a rather sample example:
 
 ```python
-from gallagher import cc, const
+# Import core python libs
+import os
+import asyncio
+
+# Import the client and models
+from gallagher import (
+    cc,
+)
+from gallagher.dto.summary import (
+    CardholderSummary,
+)
+from gallagher.cc.cardholders import (
+    Cardholder
+)
+
+# Set the API key from the environment
+api_key = os.environ.get("GACC_API_KEY")
+cc.api_key = api_key
+
+# Async support gives us back a coroutine
+ch_coro = Cardholder.list()
+
+# Run the coroutine to get the cardholder
+cardholders = asyncio.run(ch_coro)
+cardholder = cardholders.results[0]
+
+# This is now a pydantic object
+type(cardholder) == CardholderSummary
+
+# Print out some details from the object
+cardholder.href
+cardholder.first_name
+```
+
+> [!IMPORTANT]\
+> Gallagher infrastructure deals with perimeter security. We take this extremely seriously and providing a complete test suite to provide that our software meets all standards. These tests constantly run against our _demo_ command centre hosted on the cloud.
+
+The rest of the README touches upon each of the tools we provide. If you like what you see so far we recommend you [head over to our documentation](https://anomaly.github.io/gallagher).
+
+## Using the CLI and TUI
+
+Our CLI is designed to automate custom workflows via scripts. Inspired by the greatest Unix tools out there, it does one thing and it does it well, leaving you to integrate it into a pipeline. The utility is able to speaking machine readable formats like JSON, YAML and CSV as well as producing formatted output.
+
+Here's an example of fetching the details of a `cardholder`:
+
+```
+(gallagher-py3.11) ➜  gallagher git:(alpha-3) gala ch get 8272
+ person
+                  id 8272
+          first_name Jerry
+           last_name Zurcher
+          short_name None
+         description None
+          authorised yes
+
+  disable_cipher_pad no
+            division 2
+ hrefs
+                edit edit
+```
+
+## Interacting via SQL
+
+[Shillelagh](https://shillelagh.readthedocs.io/en/latest/) is a Python library that allows you to interact with REST APIs as if they were SQL databases, including the ability to provide a SQLAlchemy `dialect` allowing you to treat endpoints as a virtual table.
+
+Assuming you had the SQL extensions installed, a simplistic example of querying Cardholders from the command would look like this:
 
-cc.api_key = "GH_"
+```sql
+🍀> SELECT * FROM "https://commandcentre-api-au.security.gallagher.cloud/api/cardholders" WHERE id=8427;
+```
+
+which would return a result set of:
 
-cc.Customer.list()
 ```
+first_name    last_name    authorised    id
+------------  -----------  ------------ ----
+Cammy         Albares      True         8427
+(1 row in 0.23s)
 
-> Note this project is **NOT** officially affiliated with Gallagher Security
+```
 
-## API Notes
+## Command Centre API Notes
 
-The Gallagher API uses `href` attributes to provide a the destination of referenced objects in the responses. These are prefixed with the server origin i.e if you are using the Cloud Gateway then all your URLs will be prefixed with the appropriate gateway's address.
+The Gallagher API the principles of [HATEOAS](https://en.wikipedia.org/wiki/HATEOAS) which ensures that the API is self-descriptive and future proof.
+
+A `href` attribute provides a the destination of referenced objects in the responses. These are full qualified and will be prefixed with the server origin i.e if you are using the Cloud Gateway then all your URLs will be prefixed with the appropriate gateway's address.
 
 These appear in various forms, starting from as simple as the `href` itself:
 
@@ -34,7 +130,7 @@ These appear in various forms, starting from as simple as the `href` itself:
 }
 ```
 
-through to self recursive references with additional attributes:
+through to self recursive references (where the data is nested) with additional attributes:
 
 ```json
 "parent": {
@@ -43,7 +139,48 @@ through to self recursive references with additional attributes:
 }
 ```
 
-> Above examples have been taken from the Gallagher documentation
+> [!CAUTION]\
+> Following the design patterns outlined by HATEOAS, you must never hardcode any URLs. You should hit the base API URL which returns the `hrefs` of all other resources.
+> If you are using the Python SDK, then you don't have to worry about this, the client will handle this for you.
+
+## Python SDK Design
+
+This API client primarily depends on the following libraries:
+
+- [httpx](https://www.python-httpx.org), fo transporting and parsing HTTP requests
+- [pydantic](https://pydantic.dev), for validating responses and constructing request bodies
+
+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.
+
+> [!IMPORTANT]
+> It's **not recommended** to run tests against a production system.
+
+### Data Transfer Objects
+
+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
+- **Response** is a collection of `Summary` objects with other paths like `next` and `previous` for pagination and `updates` for polling results
+- **Payload** is used to send a request to the API
+
+In summary the properties of each are as follows:
+
+- `Refs` are the minimal pathway to an object
+- `Summary` builds on a `Ref` and provides a subset of the attributes
+- `Detail` builds on a `Summary` and provides the full set of attributes
+- `Response` encapsulates a collection of `Summary` objects, they typically have `next` and `previous` paths for pagination
+- `Payload` are verbose and match the schema definition on the documentation
+
+Each `resource` endpoint subclasses the `APIEndpoint` which marks a resource as `fetchable`, `queryable`, `creatable`, `updatable` and `deletable`. This is determined by the configuration defined using an `EndpointConfig` class.
+
+> [!TIP]
+> The above is meant to be a summary, please see [our documentation](https://anomaly.github.io/gallagher) for more details.
+
+### Schema Design Patterns
 
 Our `schemas` provide a set of `Mixins` that are used to construct the Models. These are repeatable patterns that need not be repeated. The typical patter would be to subclass from the `Mixins` e.g:
 
@@ -59,6 +196,49 @@ class AccessGroupRef(
     name: str
 ```
 
+where the `HrefMixin` (see also `OptionalHrefMixin` for use where the `href` is not always present) provides the `href` attribute:
+
+```python
+class HrefMixin(BaseModel):
+    """ Href
+
+    This mixin is used to define the href field for all
+    responses from the Gallagher API.
+    """
+    href: str
+```
+
+These `Mixin` classes can also be used to declare attributes that seek to use the same pattern:
+
+````python
+class DivisionDetail(
+    AppBaseModel,
+    IdentityMixin,
+):
+    """ Defines a Division on the Gallagher Command Centre
+    """
+
+    name: str
+    description: Optional[str] = None
+    server_display_name: Optional[str] = None
+    parent: OptionalHrefMixin = None
+
+### Schemas
+
+Our `schemas` provide a set of `Mixins` that are used to construct the Models. These are repeatable patterns that need not be repeated. The typical patter would be to subclass from the `Mixins` e.g:
+
+```python
+from .utils import AppBaseModel, IdentityMixin, HrefMixin
+
+class AccessGroupRef(
+    AppBaseModel,
+    HrefMixin
+):
+    """ Access Groups is what a user is assigned to to provide access to doors
+    """
+    name: str
+````
+
 where the `HrefMixin` provides the `href` attribute:
 
 ```python
@@ -78,7 +258,7 @@ class DivisionDetail(
     AppBaseModel,
     IdentityMixin,
 ):
-    """
+    """ Outlines the definition of a Division on the Gallagher Command Centre
     """
 
     name: str
@@ -117,46 +297,45 @@ In this example the `AppGroupRef` has a `name` attribute which is not present in
 
 > Please see the schema section for naming conventions for `schema` classes
 
-## Design
-
-This API client primarily depends on the following libraries:
-
-- [httpx](https://www.python-httpx.org), fo transporting and parsing HTTP requests
-- [pydantic](https://pydantic.dev), for validating responses and constructing request bodies
-
-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.
-
-> It's **not recommended** to run tests against a production system.
-
-### Data Transfer Objects
-
-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
-
-In summary:
+where `parent` is simply an `href` without any other attributes. In the cases where these attributes have more than just an `href` we defined `Reference` classes:
 
-- `Refs` are the minimal pathway to an object
-- `Summary` builds on a `Ref` and provides a subset of the attributes
-- `Detail` builds on a `Summary` and provides the full set of attributes
+```python
+class AccessGroupRef(
+    AppBaseModel,
+    HrefMixin
+):
+    """ Access Groups is what a user is assigned to to provide access to doors
+    """
+    name: str
+```
 
-### Resources
+and use them to populate the attributes:
 
-Resources are `fetchable`, `queryable`, `creatable`, `updatable` and `deletable`.
+```python
+class VisitorTypeDetail(
+    AppBaseModel,
+    IdentityMixin
+):
+    """
+    """
+    access_group : AccessGroupRef
+    host_access_groups: list[AccessGroupSummary]
+    visitor_access_groups: list[AccessGroupSummary]
+```
 
-### Responses
+In this example the `AppGroupRef` has a `name` attribute which is not present in the `HrefMixin` class.
 
-Responses can be the object itself or a response layout
+> Please see the schema section for naming conventions for `schema` classes
 
 ## 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.
+All of the above requires you to have an understanding of the Gallagher Command Centre and how to configure it.
 
-Before you being, please ensure:
+> [!IMPORTANT]\
+> You should either refer to the Gallagher Documentation for detailed information on configuring the Command Centre. You also require a license from Gallagher for this feature to work.
+> If you are unsure, please contact your Gallagher representative as misconfiguration can lead to security vulnerabilities.
+
+Your checklist, before you proceed to using the REST features:
 
 - You are running Command Centre version `8.60` or higher, older versions predate the gateway so cannot support it
 - The gateway enabled at the system level
@@ -169,7 +348,7 @@ To check the system level gateway status:
 - Find the `Command Centre Cloud` item and double-click it
 - Switch to the `Configuration` page, it should look something like this:
 
-![Command Centre Cloud Configuration](assets/gallagher-command-centre-properties.png)
+![Command Centre Cloud Configuration](https://raw.githubusercontent.com/anomaly/gallagher/master/assets/gallagher-command-centre-properties.png)
 
 To check your API key:
 
@@ -178,8 +357,35 @@ To check your API key:
 - Find the item that represents your REST Client
 - Switch to the `Connections` page, it should look something like this
 
-![Command Centre Cloud Connections](assets/gallagher-rest-properties.png)
+![Command Centre Cloud Connections](https://raw.githubusercontent.com/anomaly/gallagher/master/assets/gallagher-rest-properties.png)
+
+> [!CAUTION]\
+> All operations require the use of the API key. Never distribute your API key or store it somewhere that is not secure.
+> For security it's recommended you read the API key from an environment variable.
+
+## Resources
+
+The following are resources that were discoverd during the design and development of this library.
+
+### Python Libraries
+
+> [!TIP]
+> Following are Python libraries that I have found during the development of the Gallagher tools. They are not necessarily in use at the moment but a reference in case we need the functionality.
+
+- [plotext](https://github.com/piccolomo/plotext?tab=readme-ov-file) - plots directly on your terminal (something I found when I was exploring apps like [dolphie](https://github.com/charles-001/dolphie))
+- [rich-pixels](https://github.com/darrenburns/rich-pixels) - a [Rich-compatible](https://github.com/Textualize/rich) library for writing pixel images and other colourful grids to the terminal by @darrenburns
+- [PyFilesystem](https://github.com/pyfilesystem/pyfilesystem2) - a Python file system abstraction layer
+
+### Articles
+
+- [A year of building for the terminal](https://textual.textualize.io/blog/2022/12/20/a-year-of-building-for-the-terminal/) by [@darrenburns](https://github.com/darrenburns)
 
 ## License
 
-Distributed under the MIT License.
+Distributed under the MIT License except Artwork and Branding assets.
+
+## Credits
+
+- [Matthew Skiles](https://matthewskiles.com) for the beautiful logo for the project.
+- [Orion Edwards](https://github.com/borland) for all his support on getting @devraj started with the Gallagher API.
+- [Mick Lambert](https://www.linkedin.com/in/michael-lambert-au/), [Tim Harris](https://www.linkedin.com/in/timharris01/), [Andrew Donkin](https://github.com/andrewdonkin) from the Gallagher team for all their support.
diff --git a/SQL.md b/SQL.md
new file mode 100644
index 00000000..0a6647d4
--- /dev/null
+++ b/SQL.md
@@ -0,0 +1,130 @@
+    # SQL Interface
+
+A central goal of the Python tools for Gallagher is assist enterprise to integrate corporate systems with the command centre. Use cases would involve systems like payroll, human resources applications that would want to toggle access or additionally keep abreast of events that have taken place in the command centre inline situ.
+
+## Why SQL?
+
+Copied verbatam from [shillelagh](https://github.com/betodealmeida/shillelagh?tab=readme-ov-file#why-sql) repository
+
+> Sharks have been around for a long time. They're older than trees and the rings of Saturn, actually! The reason they haven't changed that much in hundreds of millions of years is because they're really good at what they do.
+>
+> SQL has been around for some 50 years for the same reason: it's really good at what it does.
+
+[How to pronounce `shillelagh`](https://youtu.be/QHDZtvfTkz4?feature=shared) Shea-lay-lee
+
+### ORM integration
+
+## Adapter Design
+
+`shillelagh` has appropriate documentation to [build your own adapters](https://shillelagh.readthedocs.io/en/latest/development.html#), the examples on the page go from implementing a simple adapter for the Weather API through to some tips to implement a more complex adapter that reads from an API e.g Google Sheets. It's recommended to study the source code to understand patterns of the how to provide data for more complex implementations.
+
+Few things to address design wise are:
+
+- Methods that allows shillelagh to [calculate the cost of a query](https://shillelagh.readthedocs.io/en/latest/development.html#estimating-query-cost)
+- Strategies in mapping URLs to virtual tables
+- How to dynamically provide a list of columns and results based on the table
+
+> I spend some significant time in debugging registering my adapter and [wrote some notes to that effect](https://github.com/anomaly/gallagher/issues/31#issuecomment-2111223261), they are mostly around my strategy of how I got things going and using the `logger` to debug issues when using the interactive console.
+
+The project features an extensive set of data transfer objects, strong design patterns and test suites to ensure that the interactions with the API are reliable.
+
+The DTOs in particular capture the rules set out by the API documentation, primarily designed to enhance the developer experience.
+
+It would thus make sense to extend the `EndpointConfig` class to include SQL specific configuration and the framework to inject the required configuration into the adapter.
+
+The reason for extending `EndpointConfig` is to ensure that all related configuration is maintained in the base classes. The `adapter` amongst other things is an optional extension that:
+
+- depends on the base API client to exist and function in full
+- is optionally installed by the user
+
+### Table names
+
+The URL of th endpoint maps as the table name e.g:
+
+```sql
+🍀> SELECT * FROM "https://commandcentre-api-au.security.gallagher.cloud/api/cardholders";
+```
+
+where `https://commandcentre-api-au.security.gallagher.cloud/api/cardholders` is the table. The `adapter` would have acknowledged that it can handle this endpoint. If we support more than one than one endpoint then we would have to validate that the URL is one of the many.
+
+### Field list
+
+Each DTO is already aware of the fields and their types. If a type is a primitive then it's easy enough to handle.
+
+If a field happens to be a relationship then we would have to return the ID of the object (if this is supported, not sure what to do when all it has is a href) to make it behave more SQL like. I think the child object should determine which field should be returned as the ID and the configuration should be part of the child object.
+
+`SELECT` queries often let you specify while columns you wish to return. This also happens to be the case for the Gallagher API. See [#36](https://github.com/anomaly/gallagher/issues/36) for more information. It would make sense to combine these two patterns.
+
+> Shillelagh require us to send back a field called `rowid` which has a unique identifier, we are currently using the `id` of the object as the `rowid`.
+
+For partial queries we should take advantage of the fact that the CC API allows us to send a list of fields to return. This is a good way to reduce the amount of data that is returned.
+
+### Search fields
+
+Search fields are determined by the API per endpoint. These should be defined as `tuples` in the `EndpointConfig` class (note that tuples are immutable hence it's ideal for a configuration).
+
+### Result Order
+
+At this stage we support the defaults as defined by Shillelagh and allow the user to order results by any of the fields in the DTO.
+
+The CC API does provide a `sort` query parameter that can be used to sort the results. Where possible we should use this parameter to avoid local computation costs. Note also the use of `-id` the negative sign to sort in descending order.
+
+### Offset and Limit
+
+The Gallagher API certain supports pagination, it does return a `next` link, and we might have to filter the results locally on the adapter. We should also cross reference our ticket on performance tuning [#14](https://github.com/anomaly/gallagher/issues/14)
+
+### Query costing
+
+Shillelagh has a method to calculate the cost of a query.
+
+### Mapping pyndatic attributes to shillelagh
+
+Our pyndatic classes use python typing to annotate fields for parsing. Shillelagh provides the `fields` package that allows developers of adapters to provide definition of fields handled by each virtual table.
+
+To make this manageable for developers we need to provide a way to:
+
+- register which endpoints are suited to work with our shillelagh adapters
+- automatically map the fields from the pyndatic classes to the shillelagh fields
+
+## Proposed API syntax
+
+```python
+    dto_list: Optional[any] = None  # DTO to be used for list requests
+    dto_retrieve: Optional[any] = None  # DTO to be used for retrieve requests
+```
+
+`dto_list` object field should be used to determine fields that are returned when a list of objects is requested. The `dto_retrieve` object field should be used to determine fields that are returned when a single object is requested.
+
+```python
+    search_fields: Optional[Tuple[str]] = None  # Fields that can be searched
+```
+
+```python
+    sql = False  # If the endpoint supports SQL queries
+    sql_limit_supported = False  # If the endpoint supports SQL LIMIT
+    sql_offset_supported = False # If the endpoint supports SQL OFFSET
+```
+
+SQL parsing things to check:
+
+- The fields in a select exists in the DTO class
+- Does an endpoint support offsets and limits
+- Examples of joins, group by, order by, where clauses
+
+Each top level package in gallagher.cc will defined a `tuple` called `__shillelagh__` this deliberately mounts classes that are to be used by the shillelagh adapter.
+
+The framework calls the `get_columns` method which is to return a `Dict [str, Field]` where the key is the name of the field and the value is an instance of a `Field` class.
+
+It's advisable to instantiate a list of columns in `init` and not compute them in the `get_columns` method.
+
+### Mutations
+
+Not all endpoints support mutations. The configuration of each DTO endpoint should reflect this. Our adapter will have to reflect this and proxy the operation to the DTO endpoint.
+
+## Static references to Classes
+
+I've decided to ad static references to the classes that support SQL to avoid using introspection.
+
+## SQLAlchemy `dialect` Design
+
+SQLAlchemy is a popular ORM (one of choice at Anomaly) and is supported by shillelagh. The `dialect` essentially makes available the endpoint as a virtual table to the ORM.
diff --git a/TODO.md b/TODO.md
index 6bb851c5..1e8806a4 100644
--- a/TODO.md
+++ b/TODO.md
@@ -10,8 +10,8 @@ The following is tracking which endpoints are available along with their test ca
 - [ ] Alarm search
 - [ ] Alarm updates
 - [x] Alarm summary
-- [ ] Alarm detail
-- [ ] Alarm history entry
+- [x] Alarm detail
+- [x] Alarm history entry
 - [ ] Alarm update request
 - [ ] Event search
 - [x] Event summary
diff --git a/TUI.md b/TUI.md
new file mode 100644
index 00000000..1aa0b6ec
--- /dev/null
+++ b/TUI.md
@@ -0,0 +1,19 @@
+# Terminal UI
+
+> The TUI is developed using the superpowers given to Python developers by [Textual](https://textual.textualize.io)
+
+Cardholder management
+
+- List cardholders
+- Create cardholder
+- Update cardholder
+- Delete cardholder
+- Attach cards to a cardholder
+- Detach cards from a cardholder
+- Visit details for a cardholder
+
+Alarm management
+
+- List alarms
+- Acknowledge alarms
+- Comment on an alarm
diff --git a/Taskfile.yml b/Taskfile.yml
index d6141f68..89d17a55 100644
--- a/Taskfile.yml
+++ b/Taskfile.yml
@@ -10,6 +10,15 @@ tasks:
       - poetry build
   test:
     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
+
+      this does not run coverage or provide tap output
+    cmds:
+      - poetry run coverage run -m pytest -s tests/{{.CLI_ARGS}}
+  test:tap:
+    desc: runs tests with tap output
     summary:
       runs all the tests inside the virtualenv, optionally
       provide the name of the test as an argument to run a single test
@@ -25,12 +34,45 @@ tasks:
     desc: runs coverage inside the server container
     cmds:
       - poetry run coverage report -m
+  lint:
+    desc: runs the linter
+    cmds:
+      - poetry run flake8 gallagher tests
+  black:
+    desc: runs black on the codebase
+    cmds:
+      - poetry run black gallagher tests
   dev:textual:
     desc: runs the textual cli
     cmds:
-      - poetry run textual --
-  dev:gcon:
+      - poetry run textual --dev --
+  dev:tui:
     desc: runs text gallagher console in dev mode
     cmds:
-      - poetry run textual run --dev gallagher.console:main
+      - poetry run textual run --dev gallagher.tui:main
+  dev:py:
+    desc: runs python in the poetry shell
+    cmds: 
+      - poetry run python -- {{.CLI_ARGS}}
+  dev:docs:
+    desc: run the mkdocs server with appropriate flags
+    cmds: 
+      - cd docs && mkdocs serve --open -a localhost:8001
+  debug:get:
+    desc: use httpie to get payload from CC
+    summary: |
+      runs a httpie get against the gallagher aus gateway
+      appends the required headers, assumes the environment
+      variables are set.
+
+      you must pass in the partial url (past /api), 
+      along with any parameters.
+
+      by no suffix is provided CC returns a list of hateoas
+      compliant endpoints.
+    cmds:
+      - |
+        http get \
+        https://commandcentre-api-au.security.gallagher.cloud/api/{{.CLI_ARGS}} \
+        "Authorization: GGL-API-KEY $GACC_API_KEY"
 
diff --git a/docs/docs/cli.md b/docs/docs/cli.md
index c74a96d7..75588431 100644
--- a/docs/docs/cli.md
+++ b/docs/docs/cli.md
@@ -2,46 +2,16 @@
 
 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 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
-```
+::: mkdocs-typer
+    :module: gallagher.cli
+    :command: app
 
-which will list the available commands:
+## Resources
 
-```
- 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
+- [Command Line Interface Guidelines - clig.dev](https://clig.dev)
diff --git a/docs/docs/design.md b/docs/docs/design.md
index 90509957..d8733d7d 100644
--- a/docs/docs/design.md
+++ b/docs/docs/design.md
@@ -1,14 +1,27 @@
-# Design
+# Contribution's Manual
 
-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
+## CLI
+
+## TUI
+
+## SQL
+
+## SDK
+
+A central feature to this project is the API client, focused on a superior developer experience and performance we spent substantial time in designing the Python interface to ensure it scales. We also ensure that we follow forward compatibility design patterns outlined by Gallagher (e.g [HATEOAS](https://gallaghersecurity.github.io/cc-rest-docs/ref/events.html)) so you as a developer don't have to worry about it.
+
+While it's optional to read this chapter if you are simply using the API client or the tools. If you choose to develop the client further then this is a must read.
 
 ## 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:
+Data Transfer Objects (DTOs) are used to parse the JSON payloads exchanged with the Gallagher REST endpoints. This is to ensure strict validation of the payloads. At it's heart each one of these classes are a `pydatic` parser.
+
+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
+- **Responses** are resposnes sent back from the server, these will typically contain a set of `Summary` or `Detail` objects. When fetching _detailed_ responses for an object the server will often respond with a `Detail` object without a wrapper `Response` object.
 
 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.
 
@@ -16,6 +29,34 @@ Ensure that each Endpoint defines their own DTOs so you can test them for authen
 
 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.
 
+### Utilities
+
+Mixins:
+
+- `IdentityMixin` - provides an `id` field
+- `HrefMixin` - provides an `href` field
+- `OptionalHrefMixin` - provides an `href` field that is optional
+
+Base models:
+
+- `AppBaseModel` - `IdentityMixin` and `HrefMixin`
+- `AppBaseResponseModel` - distinguishes between a model vs a response that encapsulates a objects
+- `AppBaseResponseWithFollowModel` - a response model with follow paths
+
+### Writing DTO classes
+
+Write about using OptionalHrefMixin
+
+Naming convensions of DTO classes.
+
+Organising DTO classes, where do extension resources like `CardholderAccessGroupSummary` go?
+
+### Python Reserved Words
+
+`from_optional_datetime` and `until_optional_datetime`
+
+`type` as a variable name is acceptable for now
+
 ## API Client Core
 
 The `core` package in `cc` provides two important classes:
@@ -29,7 +70,7 @@ Never hard code URLs as this violates the [HATEOAS](https://en.wikipedia.org/wik
 
 Additionally each configuration will provide references to DTO classes that is used to parse responses, and details of the body.
 
-```
+```py
 class Alarms(
     APIEndpoint
 ):
@@ -40,19 +81,129 @@ class Alarms(
     async def get_config(cls) -> EndpointConfig:
         return EndpointConfig(
             endpoint=Capabilities.CURRENT.features.alarms.alarms,
-            dto_list=AlarmResponse,
-            dto_retrieve=AlarmZoneSummary,
+            dto_list=AlarmSummaryResponse,
+            dto_retrieve=AlarmSummary,
         )
 ```
 
-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.
+The above example shows the `Alarms` class which is a consumer of the `alarms` endpoint. It nominates `AlarmSummaryResponse` as the class the infrastructure will use to parse `list` responses and `AlarmSummary` 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
+### Designing Endpoint Consumers
+
+Each API consumer inherits from `APIEndpont` which is defined in `gallagher.cc.core`. Before each endpoint is executed we run an internal discovery process (see the `_discover` method in `APIEndoint` class).
+
+We do this to be forwards compatible (see [HATEOAS chapter](https://gallaghersecurity.github.io/cc-rest-docs/ref/events.html) in Gallagher's documentation), but caches the response across a session (a session being an application life cycle) to increase API round trip performance.
+
+The discovered state of the server is stored in a singleton, that's used by all the API endpoints. This can be found in the `core` package, as the `CURRENT` attribute of the `Capabilities` class. This is always an instance of `DiscoveryResponse`. Because this is instantiated as part of the bootstrap, initially all the URLs are set to `None`, the values are populated ahead of the first API call made to the server.
+
+For this reason all `APIEndpoint` classes return a configuration as a result of a function called `get_config` (an `async` method that at a `class` scope) as opposed to a statically assigned class variable (otherwise the URLs would always result to be the initial `None` value).
+
+> [!TIP]
+> If you want to force discovery of the endpoints call `expire_discovery` on the `APIEndpoint` before calling the API endpoint.
+
+```python
+from ..core import (
+    Capabilities,
+    APIEndpoint,
+    EndpointConfig
+)
+
+from ...dto.detail import (
+    DivisionDetail,
+)
+
+from ...dto.response import (
+    DivisionSummaryResponse,
+)
+
+class Division(APIEndpoint):
+    """
+    Gallagher advises against hard coding the URLs for divisions, and instead
+    recommends using the /api endpoint to discover the URLs from
+    events.divisions.href and alarms.division.href.
+
+    """
+
+    @classmethod
+    async def get_config(cls) -> EndpointConfig:
+        return EndpointConfig(
+            endpoint=Capabilities.CURRENT.features.divisions.divisions,
+            dto_list=DivisionSummaryResponse,
+            dto_retrieve=DivisionDetail,
+        )
+```
+
+## Extending DTOs for the CLI
+
+```python
+    @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"
+```
+
+```py
+class CardholderSummaryResponse(
+    AppBaseResponseModel,
+):
+    """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"
+```
 
 ## Layout
 
 Layout of our files
+
+```
+.
+├── assets
+├── docs
+│   └── docs
+├── gallagher
+│   ├── cc
+│   │   ├── alarms
+│   │   ├── cardholders
+│   │   └── status_overrides
+│   ├── cli
+│   ├── dto
+│   │   ├── detail
+│   │   ├── ref
+│   │   ├── response
+│   │   └── summary
+│   └── tui
+└── tests
+```
+
+## Toolchain
+
+Majority of the development was conducted on macOS, but the toolchain should be compatible with any operating system. All elements of the project were developed in `python` with standard tooling
+
+In addition we use:
+
+- `task` - as the task runner of choice, it's widely avilable on platforms and supports Github actions. All endpoints are documented within the command line tool.
+- `poetry` as our package manager of choice for the python project
diff --git a/docs/docs/index.md b/docs/docs/index.md
index 79a9a107..36f05ab1 100644
--- a/docs/docs/index.md
+++ b/docs/docs/index.md
@@ -1,39 +1,63 @@
-# Welcome to Gallagher Python
+# Gallagher Python Toolkit
 
-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).
+Gallagher Security manufacture a variety of perimeter [security products](https://security.gallagher.com). At the hear of these is the [Command Centre](https://products.security.gallagher.com/security/au/en_AU/products/software/command-centre/p/C201311) software. Command Centre is deployed locally (in simplistic terms, the complexity varies for every use case). Version `8.6` introduced a REST API which allows you to interact with the system via HTTP requests locally or via Gallagher's [Cloud API Gateway](https://gallaghersecurity.github.io/docs/Command%20Centre%20Cloud%20Api%20Gateway%20TIP.pdf) which eliminates the need for maintaining proxies and VPNs.
 
-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.
+Our Python Toolkit focuses on enhancing the developer experience (DX) around the REST API. In principle we provide the following:
 
-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.
+- **Python SDK** an idiomatic client (including `asyncio` support) to extend the CC functionality.
+- **Command Line Interface** (CLI) to build powerful pipeline-based workflows.
+- **Terminal User Interface** (TUI) for easy interactions with the Command Centre.
+- **SQL interface** query the REST API as if it were a database or interact with via an ORM.
 
-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
 
-> **Note:** This project is not affiliated with Gallagher in any way.
+    While Anomaly is part of Gallagher's Technical Partner Program, this project is not officially affiliated with Gallagher.
 
-## Features
+## How to get started?
 
-- pydantic models for all API payloads
-- HTTP transport using httpx
-- Full test coverage
-- A completely python based interface to interacting with Gallagher Command Centre
+The offerings of this project are aimed at two significant groups of users:
 
-## Developing the client
+- Software engineers who are looking to integrate Gallagher's technology into their systems
+- Advanced users who are looking to automate their workflows
 
-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`.
+If you are a developer, the first things that might excite you are:
 
-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.
+- [Python idiomatic client](./python-sdk.md) that adheres to the standards outlined by Gallagher
+- [SQL interface to the REST API](./sql.md) that allows you to interact with the API using SQL queries
 
-To contribute to the library, please fork this repository and lodge a pull request for us to accept your changes.
+As an advanced user or integrator, we recommend at looking at:
 
-## Contributing to the documentation
+- [Command line interface](./cli.md) that allows you to interact with the API from the terminal
+- [Terminal interface](./tui.md) that provides a subset of the command centre functions in a different viewpoint
 
-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.
+All of our tools are written using the Python programming languages. We provide binaries for most popular operating systems. If you are a developer and would like to contribute to the project, a good place to start is our [design document](./design.md).
 
-Some handy commands to get you started:
+## Motivation
 
-- `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.
+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).
 
-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.
+While this is all you need to get started, it can be overwhelming to interact with the API directly.
+
+Our Python Toolkit aims to encapsulate the design requirements of the API and provide a Python interface for you to build your integrations. Simply put:
+
+!!! tip
+
+    "You write Python, we speak REST to Gallagher's standards."
+
+As we provided the viability of our commercial projects around Gallagher's infrastructure our support load went through the roof, the CLI, TUI and SQL interfaces are a formal expression of scripts, REST payload snippets that we used in the early days to perform tasks like adding cards to cardholders or keeping track of visits.
+
+The API client draws inspiration from the works of [Stripe](https://stripe.com) or [pyndatic](https://pydantic.dev) who are known for providing a superb developer experience. Our aim is to provide a similar quality of developer experience for Gallagher projects.
+
+While the entire project was built in self interest, we hope that many of you out there enjoy the developer experience if brings.
+
+## Background
+
+[Anomaly](https://www.anomaly.ltd) has a long history of working with APIs. In the early 2010s, during the early days of JSON APIs, we build a framework in Python called [prestans](https://github.com/anomaly/prestans/), it was designed to encompass the best practices of RESTful API design by providing a programmatic interface in Python. It allowed us to build large scale applications without losing quality as the team scaled up.
+
+prestans has since been superseded by projects like pydantic and FastAPI, what we learnt from building it stayed with us. We've played on our strengths of deeply understanding the underlying protocol, design patterns and philosophy of the Gallagher API and encapsulated it as a set of developer products to empower others to build on top of their infrastructure.
+
+Born out of solving our client's use cases, we built the Python client to interact with the API to ensure quality and achieve a great developer experience as our offerings scale. As our support requests grew it was evident that we could carve out a set of tools to enhance automation and integrations.
+
+We believe that open sourcing these tools will result in a vibrant community of developers that will be able to leverage Gallagher's technology to build incredibly practical applications.
+
+The design pattern of the API client is opinionated from our experience as software engineers. We goto great lengths to document our thought process and expression of the work we do. We look forward to your comments and feedback and can't wait to see what you build with our tools.
diff --git a/docs/docs/installation.md b/docs/docs/installation.md
new file mode 100644
index 00000000..ab9ac6bd
--- /dev/null
+++ b/docs/docs/installation.md
@@ -0,0 +1,66 @@
+# Installation & Usage
+
+What you need to install will widely depend on if you are looking to use our tools, building software with our libraries or contributing to the project. This guide will get you set up for each one of the use cases. While the end user tools do not require software development experience, the other development tools are biased towards Python developers.
+
+## Requirements
+
+You will require Python 3.10 or above to run the SDK and tools. We provide containerised versions and binaries for most popular platforms. Please keep reading for specific instructions.
+
+## Installation
+
+In the most trivial cases we recommend installation via `PyPI`. The package is designed such that you only install what you need. The SDK is required by all our tools, hence it's the lowest common denominator.
+
+The SDK can be installed by adding it as a dependency to your project:
+
+```bash
+poetry add gallagher
+```
+
+If you are feeling adventurous you can install everything by:
+
+```bash
+poetry add gallagher[all]
+```
+
+### SDK
+
+### Command Line Interface
+
+### Terminal User Interface
+
+### SQL Support
+
+## Developer Notes
+
+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.
+
+### Taskfile
+
+[Task](https://taskfile.dev) is a task runner / build tool that aims to be simpler and easier to use than, for example, GNU Make. Gallagher Python Toolkit uses Task to run common tasks such as testing, linting, and building the documentation. First follow the [installation steps](https://taskfile.dev/installation/) to install Task on your system.
+
+All the `tasks` are quite logically grouped and most of them will need you to have a `virtualenv` initialised via `poetry`.
+
+> Our Github workflows use [Task](https://taskfile.dev/installation/#github-actions) via the Github action.
+
+Some of the `task` targets take parameters e.g.
+
+`task test` will run the entire test suite, while `task test -- test_cardholder.py` will run only the tests in `test_cardholder.py`.
+
+### Building the Docs
+
+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:
+
+- `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.
+
+We have a wrapper for running the `mkdocs` web server at `task dev:docs` which runs the server on port `8001`.
+
+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/python-sdk.md b/docs/docs/python-sdk.md
new file mode 100644
index 00000000..7e62fa31
--- /dev/null
+++ b/docs/docs/python-sdk.md
@@ -0,0 +1,156 @@
+# Python API Client
+
+Gallagher Python Toolkit started life with the API Client, and it underpins all the utilities GPT ships. The central theme in the API client is data validation for responses and payloads. We take the approach of "nothing can go wrong" when we are interacting with a Gallagher Command Centre. We put in enormous effort into maintaining this SDK with the following aims:
+
+- A stellar developer experience for everyone building applications
+- Reliability and the ability to constantly test changes against a real Command Centre
+- Backwards compatibility to ensure your application don't break with our changes
+
+Our aims is for you to write Python and we worry about how to best work with the Command Centre API.
+
+# Setup
+
+For most use cases we recommend installing a stable version from `pypi`.
+
+Install via `pip` as follows:
+
+```bash
+pip install gallagher
+```
+
+or if you are using `poetry`:
+
+```bash
+poetry add gallagher
+```
+
+For production application please make sure you target a particular version of the API client to avoid breaking changes.
+
+# Features
+
+- **Stellar DX** - We've put immense effort in designing the programming interface to provide a stellar developer experience. If you've worked with well designed API clients like Stripe, you should feel right at home.
+- **Merciless validation** - built on top of [pydantic](https://pydantic.dev) the SDK ensures extremely strong data validation, reenforced with a complete set of tests.
+- **Designed to Perform** - throughout the development we identified and fine tuned every portion to steal milliseconds back without compromising on the reliability.
+- **Future proof** - HATEOAS support ensures that the SDK is future proof and designed to standards outlined by Gallagher.
+- **Built for tomorrow** - `asyncio` support ensures that the SDK is ready for the upcoming future of Python.
+
+# Data Transfer Objects (DTO) Premiere
+
+The Data Transfer Objects or DTOs are the centre piece of the Python SDK. These are built using the much loved [pyndatic](https://pydantic.dev) library. The aim is strict validation of responses and request payloads to ensure that the SDK never falls out of line with Gallagher' REST API.
+
+Before every pull request is merged into our main branch, we run a full test suite against a demo Command Centre. See the [actions](https://github.com/anomaly/gallagher/actions) tab for a status of the latest build.
+
+DTOs are classified into:
+
+- `Refs` are the minimal pathway to an object
+- `Summary` builds on a `Ref` and provides a subset of the attributes
+- `Detail` builds on a `Summary` and provides the full set of attributes
+
+A DTO is a subclass of one and only one of the above base classes. They deliberately do not inherit from each other so you can test of truths i.e a `ref` will never equate to a `summary` or a `detail`.
+
+In addition to DTOs, you will see a number of :
+
+- `Response` objects, which encapsulates a collection of `Summary` objects, they typically have `next` and `previous` paths for pagination
+- `Payload` are the objects that are sent to the Command Centre to create or update an object
+
+If you are fetching a `detail` then they are returned on their own as part of the response. They typically contain `href` to related objects.
+
+# Basic Usage
+
+```python title="Basic Usage"
+# Import core python libs
+import os
+import asyncio
+
+# Import the client and models
+from gallagher import (
+    cc,
+)
+from gallagher.dto.summary import (
+    CardholderSummary,
+)
+from gallagher.cc.cardholders import (
+    Cardholder
+)
+
+# Set the API key from the environment
+api_key = os.environ.get("GACC_API_KEY")
+cc.api_key = api_key
+
+# Async support gives us back a coroutine
+ch_coro = Cardholder.list()
+
+# Run the coroutine to get the cardholder
+cardholders = asyncio.run(ch_coro)
+cardholder = cardholders.results[0]
+
+# This is now a pydantic object
+type(cardholder) == CardholderSummary
+
+# Print out some details from the object
+cardholder.href
+cardholder.first_name
+```
+
+## Lists
+
+## Detail
+
+## Search
+
+## Creating
+
+## Deletion
+
+# Next and Updates
+
+# Additional Features
+
+In addition to a nicely validated wrapper around the data sent by the Command Centre API, we provide provide the following helper interface to keep your interaction `pythonic` wherever possible.
+
+## Personal Data Definitions
+
+Personal Data Definitions are fields associated to a cardholder and are defined at a Command Centre level. These are dynamically discovered by calling the `/api/personal_data_fields`. When you fetch a cardholder detail, the API returns the `personal_data_fields` as part of the response in the following manner:
+
+- children of the `personalDataFields` key in the cardholder detail
+- accessible via key name prefixed with the `@` symbol i.e the personal data field `Email` is accessible via the key `@Email`
+
+!!! tip
+
+    Note that the `personDataFields` has a `list` of objects, and each object has a single key which is the nae of the personal data field and the value is the related data.
+
+To make things more `pythonic` i.e consider the following payload (partially represented):
+
+```json
+{
+  "@Cardholder UID": "2",
+  "@City": "Hamilton",
+  "@Company Name": "Gallagher Group",
+  "@Country": "New Zealand",
+  "@Email": "emma.bennett@gallagher.co",
+  "@Personal URL": "C:\\DemoFiles\\CardholderURLs\\Emma Bennet.htm",
+  "@Phone": "+64 7 838 9800",
+  "@Photo": {
+    "href": "https://commandcentre-api-au.security.gallagher.cloud/api/cardholders/340/personal_data/6550"
+  },
+  "firstName": "Emma",
+  "href": "https://commandcentre-api-au.security.gallagher.cloud/api/cardholders/340",
+  "id": "340",
+  "lastName": "Bennett",
+  "lastSuccessfulAccessTime": "2014-10-16T02:56:43Z"
+}
+```
+
+and we had used the API client to fetch the cardholder detail (partial example):
+
+```python title="Personal Data Fields"
+cardholder = await Cardholder.retrieve(340)
+```
+
+you could access the `Email` field either via the dictionary as `cardholder.personal_data_definitions['@Email']` or the parsed shortcut `cardholder.pdf.email`.
+
+The above is achieved by dynamically populating a placeholder object with dynamically generated keys. These are parsed and populate _once_ when the object has successfully parsed the `JSON` payload.
+
+!!! tip
+
+    See pyndatic's [Model validator](https://docs.pydantic.dev/latest/concepts/validators/#model-validators) feature in v2, in particular the `@model_validator(mode='after')` constructor.
diff --git a/docs/docs/sql.md b/docs/docs/sql.md
new file mode 100644
index 00000000..07672c2b
--- /dev/null
+++ b/docs/docs/sql.md
@@ -0,0 +1,1 @@
+# SQL and SQLAlchemy
diff --git a/docs/docs/tui.md b/docs/docs/tui.md
new file mode 100644
index 00000000..d63e540c
--- /dev/null
+++ b/docs/docs/tui.md
@@ -0,0 +1,1 @@
+# Terminal User Interface
\ No newline at end of file
diff --git a/docs/docs/usage.md b/docs/docs/usage.md
deleted file mode 100644
index e2fab4ec..00000000
--- a/docs/docs/usage.md
+++ /dev/null
@@ -1,11 +0,0 @@
-# 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 540da967..70173c8e 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -1,6 +1,28 @@
-site_name: Gallagher Idiomatic client for Python
+site_name: Gallagher Python Toolkit
 site_url: https://anomaly.github.io/gallagher/
 repo_url: https://github.com/anomaly/gallagher/
 site_author: Dev Mukherjee
+
 theme:
-  name: readthedocs
+  name: material
+
+markdown_extensions:
+    - mkdocs-typer
+    - codehilite
+    - pymdownx.highlight:
+        anchor_linenums: true
+        line_spans: __span
+        pygments_lang_class: true
+    - pymdownx.inlinehilite
+    - pymdownx.snippets
+    - pymdownx.superfences
+    - admonition
+
+nav:
+  - Introduction: index.md
+  - Installation & Usage: installation.md
+  - SQL and SQLAlchemy: sql.md
+  - Command Line Interface: cli.md
+  - Terminal User Interface: tui.md 
+  - Python SDK: python-sdk.md
+  - Contributor's Manual: design.md
diff --git a/examples/README.md b/examples/README.md
new file mode 100644
index 00000000..df635b4e
--- /dev/null
+++ b/examples/README.md
@@ -0,0 +1,1 @@
+# Examples
diff --git a/examples/alarm.py b/examples/alarm.py
new file mode 100644
index 00000000..4246f332
--- /dev/null
+++ b/examples/alarm.py
@@ -0,0 +1,4 @@
+""" Alarms
+
+"""
+
diff --git a/examples/timemachie.ps1 b/examples/timemachie.ps1
new file mode 100644
index 00000000..927c4a7d
--- /dev/null
+++ b/examples/timemachie.ps1
@@ -0,0 +1,36 @@
+# Define variables
+$serviceName = "MSSQLSERVER" # Change this to your SQL Server service name
+$serverInstance = "localhost" # Change this as necessary
+$databaseName = "YourDatabaseName"
+$dbFilesPath = "C:\Path\To\Database\Files"
+$backupPath = "C:\Path\To\Backup\Location"
+
+# Import SQL Server module
+Import-Module "SqlServer"
+
+# Stop SQL Server Service
+Stop-Service -Name $serviceName -Force
+Write-Host "Service stopped."
+
+# Detach the database
+Invoke-Sqlcmd -ServerInstance $serverInstance -Query "EXEC sp_detach_db '$databaseName'"
+Write-Host "Database detached."
+
+# Copy database files
+$mdfFile = "$dbFilesPath\$databaseName.mdf"
+$ldfFile = "$dbFilesPath\$databaseName_Log.ldf"
+Copy-Item -Path $mdfFile -Destination $backupPath -Force
+Copy-Item -Path $ldfFile -Destination $backupPath -Force
+Write-Host "Database files copied."
+
+# Reattach the database
+$mdfFileBackup = "$backupPath\$databaseName.mdf"
+$ldfFileBackup = "$backupPath\$databaseName_Log.ldf"
+Invoke-Sqlcmd -ServerInstance $serverInstance -Query "CREATE DATABASE $databaseName ON (FILENAME = '$mdfFileBackup'), (FILENAME = '$ldfFileBackup') FOR ATTACH"
+Write-Host "Database reattached."
+
+# Start SQL Server Service
+Start-Service -Name $serviceName
+Write-Host "Service started."
+
+Write-Host "Operation completed successfully."
\ No newline at end of file
diff --git a/gallagher/cc/__init__.py b/gallagher/cc/__init__.py
index ca1a02e7..e35a195a 100644
--- a/gallagher/cc/__init__.py
+++ b/gallagher/cc/__init__.py
@@ -2,29 +2,29 @@
 
     After enabling the Gateway for your server and REST API user, you need 
     only to replace the local server's URL with the cloud API Gateway one.
-    
+
     Example Local request:
 
-            <client certificate thumbprint e89ef121958...>
-            GET https://servername.yourcompany.local:8904/api/cardholders
-            Authorization: GGL-API-KEY F6F0-C8F0-
+        <client certificate thumbprint e89ef121958...>
+        GET https://servername.yourcompany.local:8904/api/cardholders
+        Authorization: GGL-API-KEY F6F0-C8F0-
         
     Equivalent Gateway request:
 
-          <client certificate thumbprint e89ef121958...>
-          GET https://commandcentre-api-au.security.gallagher.cloud/api/cardholders
-          Authorization: GGL-API-KEY F6F0-C8F0-60AF-
+        <client certificate thumbprint e89ef121958...>
+        GET https://commandcentre-api-au.security.gallagher.cloud/api/cardholders
+        Authorization: GGL-API-KEY F6F0-C8F0-60AF-
 
     The API client takes of appending the headers onto the requests.
 
-
+    The packages mimic the documentation structure presented by Gallagher on their
+    Github pages https://gallaghersecurity.github.io/cc-rest-docs/ref/index.html
 """
 
 from typing import Optional
 
 from ..const import URL
 
-
 # Follow the instructions in the Gallagher documentation
 # to obtain an API key
 api_key: str = None
diff --git a/gallagher/cc/alarms/__init__.py b/gallagher/cc/alarms/__init__.py
index e9cb0185..e1f67ac9 100644
--- a/gallagher/cc/alarms/__init__.py
+++ b/gallagher/cc/alarms/__init__.py
@@ -1,33 +1,189 @@
 """ Alarms
 
+Command Centre raised alarms for 
 
 """
 
+from typing import Optional
+
 from ..core import (
     APIEndpoint,
     EndpointConfig,
-    Capabilities
+    Capabilities,
+)
+
+from ...dto.ref import (
+    AlarmRef,
 )
 
 from ...dto.summary import (
-    AlarmZoneSummary
+    AlarmSummary,
+)
+
+from ...dto.detail import (
+    AlarmDetail,
 )
 
 from ...dto.response import (
-    AlarmResponse,
+    AlarmSummaryResponse,
+)
+
+from ...dto.payload import (
+    AlarmCommentPayload,
 )
 
 
 class Alarms(
-    APIEndpoint
+    APIEndpoint,
 ):
-    """ Alarms
+    """Alarms
+
+    Provides a standard set of operations for Alarms and in addition
+    allows you to follow resulting actions to act on Alarms.
+
+    To each action you are required to pass in a Ref, Summary or Detail
+    and it picks the appropriate href to follow through
+
+    Current supported actions:
+    - follow:
+    - comment:
+    - mark_as_viewed:
+    - mark_as_acknowledged:
+    - mark_as_processed:
+    - mark_as_force_processed:
     """
 
     @classmethod
     async def get_config(cls) -> EndpointConfig:
+        """Return the configuration for Alarms
+
+        Arguments:
+        cls: class reference
+        """
         return EndpointConfig(
             endpoint=Capabilities.CURRENT.features.alarms.alarms,
-            dto_list=AlarmResponse,
-            dto_retrieve=AlarmZoneSummary,
+            dto_list=AlarmSummaryResponse,
+            dto_retrieve=AlarmDetail,
         )
+
+    @classmethod
+    async def follow(cls, href: str) -> bool:
+        """ """
+        return False
+
+    @classmethod
+    async def mark_as_viewed(
+        cls,
+        alarm: AlarmRef | AlarmSummary | AlarmDetail,
+        comment: Optional[str],
+    ) -> bool:
+        """ Mark an alarm as viewed
+
+        arguments:
+        alarm: AlarmRef | AlarmSummary | AlarmDetail
+            The alarm to mark as viewed
+        comment: Optional[str]
+            A comment to add to the alarm 
+        """
+        await cls._post(
+            alarm.view.href,
+            AlarmCommentPayload(comment=comment) if comment else None,
+        )
+
+        return alarm.href is not None
+
+    @classmethod
+    async def comment(
+        cls,
+        alarm: AlarmRef | AlarmSummary | AlarmDetail,
+        comment: str,
+    ) -> bool:
+        """Comment on an alarm without changing status
+
+        This will add a comment to the history of the alarm without
+        changing the status of the alarm.
+
+        You must pass a Ref, Summary or Detail which will be used
+        to get the href of the Alarm
+
+        arguments:
+        alarm: AlarmRef | AlarmSummary | AlarmDetail
+            The alarm to comment on
+        comment: str
+            The comment to add to the alarm
+        """
+
+        await cls._post(
+            alarm.comment.href,
+            AlarmCommentPayload(comment=comment),
+        )
+
+        return alarm.href is not None
+
+    @classmethod
+    async def mark_as_acknowledged(
+        cls,
+        alarm: AlarmRef | AlarmSummary | AlarmDetail,
+        comment: Optional[str],
+    ) -> bool:
+        """ Mark an alarm as acknowledged
+
+        arguments:
+        alarm: AlarmRef | AlarmSummary | AlarmDetail
+            The alarm to mark as acknowledged
+        comment: Optional[str]
+            A comment to add to the alarm
+        """
+        await cls._post(
+            alarm.acknowledge.href,
+            AlarmCommentPayload(comment=comment) if comment else None,
+        )
+
+        return alarm.href is not None
+
+    @classmethod
+    async def mark_as_processed(
+        cls,
+        alarm: AlarmRef | AlarmSummary | AlarmDetail,
+        comment: Optional[str],
+    ) -> bool:
+        """ Mark an alarm as processed
+
+        arguments:
+        alarm: AlarmRef | AlarmSummary | AlarmDetail
+            The alarm to mark as processed
+        comment: Optional[str]
+            A comment to add to the alarm 
+        """
+        await cls._post(
+            alarm.process.href,
+            AlarmCommentPayload(comment=comment) if comment else None,
+        )
+
+        return alarm.href is not None
+
+    @classmethod
+    async def mark_as_force_processed(
+        cls,
+        alarm: AlarmRef | AlarmSummary | AlarmDetail,
+    ) -> bool:
+        """ Mark an alarm as force processed
+
+        arguments:
+        alarm: AlarmRef | AlarmSummary | AlarmDetail
+            The alarm to mark as force processed
+
+        returns:
+        bool: True if the alarm was force processed 
+        """
+        await cls._post(
+            alarm.force_process.href,
+            None,
+        )
+
+        return alarm.href is not None
+
+
+__shillelagh__ = (
+    Alarms,
+)
\ No newline at end of file
diff --git a/gallagher/cc/alarms/day_category.py b/gallagher/cc/alarms/day_category.py
index 021d26bb..0c3f4eb1 100644
--- a/gallagher/cc/alarms/day_category.py
+++ b/gallagher/cc/alarms/day_category.py
@@ -3,26 +3,18 @@
 
 """
 
-from ..core import (
-    Capabilities,
-    APIEndpoint,
-    EndpointConfig
-)
+from ..core import Capabilities, APIEndpoint, EndpointConfig
 
-from ...dto.response import (
-    DayCategoryResponse
-)
+from ...dto.response import DayCategoryResponse
 
 
 class DayCategory(APIEndpoint):
-    """ Day Categories
-    """
+    """Day Categories"""
 
     @classmethod
     async def get_config(cls) -> EndpointConfig:
         return EndpointConfig(
-            endpoint=Capabilities.CURRENT.features.day_categories
-            .day_categories,
+            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 661c7e44..30ef091e 100644
--- a/gallagher/cc/alarms/divisions.py
+++ b/gallagher/cc/alarms/divisions.py
@@ -1,26 +1,22 @@
-"""
+""" Division
 
 """
 
-from ..core import (
-    Capabilities,
-    APIEndpoint,
-    EndpointConfig
-)
+from ..core import Capabilities, APIEndpoint, EndpointConfig
 
 from ...dto.detail import (
     DivisionDetail,
 )
 
 from ...dto.response import (
-    DivisionDetailResponse,
+    DivisionSummaryResponse,
 )
 
 
 class Division(APIEndpoint):
     """
     Gallagher advises against hardcoding the URLs for divisions, and instead
-    recommends using the /api endpoint to discover the URLs from 
+    recommends using the /api endpoint to discover the URLs from
     events.divisions.href and alarms.division.href.
 
     """
@@ -29,6 +25,6 @@ class Division(APIEndpoint):
     async def get_config(cls) -> EndpointConfig:
         return EndpointConfig(
             endpoint=Capabilities.CURRENT.features.divisions.divisions,
-            dto_list=DivisionDetailResponse,
+            dto_list=DivisionSummaryResponse,
             dto_retrieve=DivisionDetail,
         )
diff --git a/gallagher/cc/alarms/events.py b/gallagher/cc/alarms/events.py
index 54daef02..a6677f91 100644
--- a/gallagher/cc/alarms/events.py
+++ b/gallagher/cc/alarms/events.py
@@ -4,11 +4,7 @@
 authenticates at a device, usually by badging a card.
 """
 
-from ..core import (
-    Capabilities,
-    APIEndpoint,
-    EndpointConfig
-)
+from ..core import Capabilities, APIEndpoint, EndpointConfig
 
 from ...dto.detail import (
     EventDetail,
@@ -20,12 +16,8 @@
 )
 
 
-class Event(
-    APIEndpoint
-):
-    """ Event 
-
-    """
+class Event(APIEndpoint):
+    """Event"""
 
     @classmethod
     async def get_config(cls) -> EndpointConfig:
@@ -36,12 +28,8 @@ async def get_config(cls) -> EndpointConfig:
         )
 
 
-class EventType(
-    APIEndpoint
-):
-    """ EventType
-
-    """
+class EventType(APIEndpoint):
+    """EventType"""
 
     @classmethod
     async def get_config(cls) -> EndpointConfig:
diff --git a/gallagher/cc/alarms/items.py b/gallagher/cc/alarms/items.py
index 62175e71..da199cac 100644
--- a/gallagher/cc/alarms/items.py
+++ b/gallagher/cc/alarms/items.py
@@ -20,7 +20,7 @@
 
 class ItemsTypes(APIEndpoint):
     """
-     Gallagher
+    Gallagher
     """
 
     @classmethod
@@ -35,7 +35,7 @@ async def get_config(cls) -> EndpointConfig:
 class Item(APIEndpoint):
     """
     Gallagher advises against hardcoding the URLs for divisions, and instead
-    recommends using the /api endpoint to discover the URLs from 
+    recommends using the /api endpoint to discover the URLs from
     events.divisions.href and alarms.division.href.
 
     """
diff --git a/gallagher/cc/alarms/schedule.py b/gallagher/cc/alarms/schedule.py
index 49dceeda..385c1d19 100644
--- a/gallagher/cc/alarms/schedule.py
+++ b/gallagher/cc/alarms/schedule.py
@@ -2,20 +2,13 @@
 
 """
 
-from ..core import (
-    Capabilities,
-    APIEndpoint,
-    EndpointConfig
-)
+from ..core import Capabilities, APIEndpoint, EndpointConfig
 
-from ...dto.response import (
-    ScheduleSummaryResponse
-)
+from ...dto.response import ScheduleSummaryResponse
 
 
 class Schedule(APIEndpoint):
-    """ Schedules
-    """
+    """Schedules"""
 
     @classmethod
     async def get_config(cls) -> EndpointConfig:
diff --git a/gallagher/cc/cardholders/__init__.py b/gallagher/cc/cardholders/__init__.py
index 2eb2788d..166731e2 100644
--- a/gallagher/cc/cardholders/__init__.py
+++ b/gallagher/cc/cardholders/__init__.py
@@ -1,3 +1,64 @@
+""" Cardholder related 
+
 """
 
-"""
\ No newline at end of file
+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
+
+
+
+__shillelagh__ = (
+    Cardholder,
+)
\ No newline at end of file
diff --git a/gallagher/cc/cardholders/card_type.py b/gallagher/cc/cardholders/card_type.py
index 2aece3f9..60580c96 100644
--- a/gallagher/cc/cardholders/card_type.py
+++ b/gallagher/cc/cardholders/card_type.py
@@ -1,23 +1,18 @@
 """
 
 """
-from ..core import (
-    Capabilities,
-    APIEndpoint,
-    EndpointConfig
-)
 
-from ...dto.response import (
-    CardTypeResponse
-)
+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.
+    """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.  
+    biometrics. The card type is used to dynamically determine the types
+    of credentials available on this particular instance.
     """
 
     @classmethod
diff --git a/gallagher/cc/cardholders/cardholders.py b/gallagher/cc/cardholders/cardholders.py
deleted file mode 100644
index 6a96164e..00000000
--- a/gallagher/cc/cardholders/cardholders.py
+++ /dev/null
@@ -1,69 +0,0 @@
-"""
-
-"""
-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
index 8a8cd422..c7f5a149 100644
--- a/gallagher/cc/core.py
+++ b/gallagher/cc/core.py
@@ -11,14 +11,24 @@
 endpoint will be assigned to None but will self heal as part of 
 the bootstrapping process.
 """
-from typing import Optional
+
+from typing import (
+    Optional,
+    Tuple,
+)
+
 from datetime import datetime
 from dataclasses import dataclass
 
+from http import HTTPStatus  # Provides constants for HTTP status codes
+
 import httpx
 
-from gallagher.exception import (
-    UnlicensedFeatureException
+from gallagher.exception import UnlicensedFeatureException
+
+from ..dto.utils import (
+    AppBaseModel,
+    AppBaseResponseWithFollowModel,
 )
 
 from ..dto.detail import (
@@ -29,20 +39,28 @@
     DiscoveryResponse,
 )
 
+from ..exception import (
+    UnlicensedFeatureException,
+    NotFoundException,
+    AuthenticationError,
+    DeadEndException,
+    PathFollowNotSupportedError,
+)
+
 
-def check_api_key_format(api_key):
-    """ Validates that the Gallagher Key is in the right format.
+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)
+    api_tokens = api_key.split("-")
+    return api_tokens.count() == 8
 
 
-def get_authorization_headers():
-    """ Creates an authorization header for Gallagher API calls
+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.
@@ -59,16 +77,18 @@ def get_authorization_headers():
 
     """
     from . import api_key
+
     return {
-        'Authorization': f'GGL-API-KEY {api_key}'
+        "Content-Type": "application/json",
+        "Authorization": f"GGL-API-KEY {api_key}",
     }
 
 
 @dataclass
 class EndpointConfig:
-    """ A configuration for an API endpoint
+    """A configuration for an API endpoint
 
-    Each API endpoint has a configuration that defines how 
+    Each API endpoint has a configuration that defines how
     the APIBase should attempt to parse responses from the
     Gallagher servers.
 
@@ -82,25 +102,26 @@ class EndpointConfig:
     __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
+
+    fields: Tuple[str] = ()  # Optional list of fields, blank = all
+    search: Tuple[str] = () # If the endpoint supports search, blank = none
 
     @classmethod
     async def validate_endpoint(cls):
-        """ Check to see if the feature is licensed and available
+        """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"
-            )
+            raise UnlicensedFeatureException("Endpoint not defined")
 
 
 class Capabilities:
@@ -116,15 +137,14 @@ class Capabilities:
     # 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
+    # This value is memoized and should be performant
     CURRENT = DiscoveryResponse(
         version="0.0.0",  # Indicates that it's not been discovered
-        features=FeaturesDetail()
+        features=FeaturesDetail(),
     )
 
-
 class APIEndpoint:
-    """ Base class for all API objects
+    """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.
@@ -138,24 +158,37 @@ class APIEndpoint:
     # lifecycle methods and use to cache the configuration object
     __config__ = None
 
+    @classmethod
+    async def expire_discovery(cls):
+        """Expires endpoint discovery information
+
+        Use this with caution as it significantly increases roundtrip times
+        and decreases API client performance.
+
+        Unless the server instance updates mid cycle, there should be no
+        reason for these discovered URLs to change.
+        """
+        Capabilities.CURRENT = DiscoveryResponse(
+            version="0.0.0",  # Indicates that it's not been discovered
+            features=FeaturesDetail(),
+        )
+
     @classmethod
     async def get_config(cls) -> EndpointConfig:
-        """ Returns the configuration for the endpoint
+        """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"
-        )
+        raise NotImplementedError("get_config method not implemented")
 
     @classmethod
     async def _discover(cls):
-        """ The Command Centre root API endpoint 
+        """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. 
+        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.
@@ -168,10 +201,13 @@ async def _discover(cls):
 
         Note that references to Capabilities.CURRENT as a singleton, while
         cls.method when executing a class method.
+
+        :params class cls: The class that is calling the method
         """
 
-        if Capabilities.CURRENT.version != "0.0.0" and\
-                type(Capabilities.CURRENT.good_known_since) is datetime:
+        if Capabilities.CURRENT.version != "0.0.0" and isinstance(
+            Capabilities.CURRENT._good_known_since, 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
@@ -181,17 +217,16 @@ async def _discover(cls):
         # 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(),
+                headers=_get_authorization_headers(),
             )
 
             await _httpx_async.aclose()
 
-            parsed_obj = DiscoveryResponse.model_validate(
-                response.json()
-            )
+            parsed_obj = DiscoveryResponse.model_validate(response.json())
 
             # Assign the capabilities to the class, this should
             # result in the endpoint
@@ -208,88 +243,64 @@ async def _discover(cls):
             # 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
+            # an instance of a pydantic 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
+        """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()
-            )
+        :param int skip: fetch responses from this anchor
+        """
+        await cls._discover()  # Discover must be here for dynamic config
 
-            return parsed_obj
+        return await cls._get(
+            cls.__config__.endpoint.href,
+            cls.__config__.dto_list,
+        )
 
     @classmethod
     async def retrieve(cls, id):
-        """ Retrieve a single object for the given resource
+        """Retrieve a single object for the given resource
 
-        Most objects have an ID which is numeral or UUID. 
+        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()
-            )
+        :param int id: identifier of the object to be fetched
+        """
+        await cls._discover()  # Discover must be here for dynamic config
 
-            return parsed_obj
+        return await cls._get(
+            f"{cls.__config__.endpoint.href}/{id}",
+            cls.__config__.dto_retrieve,
+        )
 
     @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
+    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.
@@ -303,9 +314,9 @@ async def search(cls,
         await cls._discover()
 
         params = {
-            'top': top,
-            'sort': sort,
-            'fields': fields,
+            "top": top,
+            "sort": sort,
+            "fields": fields,
         }
 
         # Adds arbitrary fields to the search, these will be different
@@ -315,15 +326,177 @@ async def search(cls,
         async with httpx.AsyncClient() as _httpx_async:
 
             response = await _httpx_async.get(
-                f'{cls.__config__.endpoint.href}',
+                f"{cls.__config__.endpoint.href}",
                 params=params,
-                headers=get_authorization_headers(),
+                headers=_get_authorization_headers(),
             )
 
             await _httpx_async.aclose()
 
-            parsed_obj = cls.__config__.dto_list.model_validate(
-                response.json()
-            )
+            parsed_obj = cls.__config__.dto_list.model_validate(response.json())
 
             return parsed_obj
+
+    # Follow links methods, these are valid based on if the response
+    # classes make available a next, previous or update href, otherwise
+    # the client raises an NotImplementedError
+
+    @classmethod
+    async def next(cls, response):
+        """Fetches the next set of results
+
+        This is only valid if the response object has a next href
+        """
+        await cls._discover()
+
+        # If the cls.__config__ is not of type AppBaseResponseWithFollowModel
+        # then we should raise an exception
+        if not issubclass(cls.__config__.dto_list, AppBaseResponseWithFollowModel):
+            """A response model must have a next, previous or update"""
+            raise PathFollowNotSupportedError(
+                "Endpoint does not support previous, next or updates"
+            )
+
+        if not response.next:
+            """We have no where to go based on the passed response"""
+            raise DeadEndException("No further paths to follow for this endpoint")
+
+        return await cls._get(
+            response.next.href,
+            cls.__config__.dto_list,
+        )
+
+    @classmethod
+    async def previous(cls, response):
+        """Fetches the previous set of results
+
+        This is only valid if the response object has a previous href
+        """
+        await cls._discover()
+
+        # If the cls.__config__ is not of type AppBaseResponseWithFollowModel
+        # then we should raise an exception
+        if not issubclass(cls.__config__.dto_list, AppBaseResponseWithFollowModel):
+            """A response model must have a next, previous or update"""
+            raise PathFollowNotSupportedError(
+                "Endpoint does not support previous, next or updates"
+            )
+
+        if not response.previous:
+            raise DeadEndException("No path leads further back than this")
+
+        return await cls._get(
+            cls.response.previous.href,
+            cls.__config__.dto_list,
+        )
+
+    @classmethod
+    async def poll(cls, response):
+        """Fetches the updated set of results
+
+        Update follow the same pattern as next and previous, except
+        it keeps yielding results until the server has no more updates
+        """
+        await cls._discover()
+
+        # If the cls.__config__ is not of type AppBaseResponseWithFollowModel
+        # then we should raise an exception
+        if not issubclass(cls.__config__.dto_list, AppBaseResponseWithFollowModel):
+            """A response model must have a next, previous or update"""
+            raise PathFollowNotSupportedError(
+                "Endpoint does not support previous, next or updates"
+            )
+
+        return await cls._get(
+            cls.response.update.href,
+            cls.__config__.dto_list,
+        )
+
+    # Proposed methods for internal use
+    @classmethod
+    async def _get(
+        cls,
+        url: str,
+        response_class: AppBaseModel | None,
+    ):
+        """Generic _get method for all endpoints
+
+        This is to be used if we can find a prepared url endpoint, this
+        is useful for follow on endpoints like commenting, next, previous
+        etc. The response_class is used to parse the returned object.
+
+        Note: that this does not run discover as it expects the endpoint to
+        be fully formed and callable. If you are calling this from a generic
+        endpoint like `list` please ensure you've called _discover first.
+
+        :param str url: URL to fetch the data from
+        :param AppBaseModel response_class: DTO to be used for list requests
+        """
+        async with httpx.AsyncClient() as _httpx_async:
+
+            try:
+
+                response = await _httpx_async.get(
+                    f"{url}",  # required to turn pydantic object to str
+                    headers=_get_authorization_headers(),
+                )
+
+                await _httpx_async.aclose()
+
+                if response.status_code == HTTPStatus.OK:
+
+                    if not response_class:
+                        return
+
+                    parsed_obj = response_class.model_validate(response.json())
+
+                    return parsed_obj
+
+                elif response.status_code == HTTPStatus.NOT_FOUND:
+                    raise NotFoundException()
+                elif response.status_code == HTTPStatus.FORBIDDEN:
+                    raise UnlicensedFeatureException()
+                elif response.status_code == HTTPStatus.UNAUTHORIZED:
+                    raise AuthenticationError()
+
+            except httpx.RequestError as e:
+                pass
+
+    @classmethod
+    async def _post(
+        cls,
+        url: str,
+        payload: AppBaseModel | None,
+        response_class: AppBaseModel | None = None,
+    ):
+        """ """
+        async with httpx.AsyncClient() as _httpx_async:
+
+            try:
+
+                response = await _httpx_async.post(
+                    f"{url}",  # required to turn pydantic object to str
+                    json=payload.model_dump() if payload else None,
+                    headers=_get_authorization_headers(),
+                )
+
+                await _httpx_async.aclose()
+
+                if response.status_code == HTTPStatus.OK:
+
+                    if not response_class:
+                        # TODO: should this return a boolean?
+                        """No response to parse"""
+                        return True
+
+                    parsed_obj = response_class.model_validate(response.json())
+
+                    return parsed_obj
+
+                elif response.status_code == HTTPStatus.FORBIDDEN:
+                    raise UnlicensedFeatureException()
+                elif response.status_code == HTTPStatus.UNAUTHORIZED:
+                    raise AuthenticationError()
+
+            except httpx.RequestError as e:
+                raise (e)
diff --git a/gallagher/cc/status_overrides/__init__.py b/gallagher/cc/status_overrides/__init__.py
index e69de29b..abf3316e 100644
--- a/gallagher/cc/status_overrides/__init__.py
+++ b/gallagher/cc/status_overrides/__init__.py
@@ -0,0 +1,6 @@
+"""
+
+"""
+
+__shillelagh__ = (
+)
\ No newline at end of file
diff --git a/gallagher/cli/__init__.py b/gallagher/cli/__init__.py
index 01b52b4c..62052e30 100644
--- a/gallagher/cli/__init__.py
+++ b/gallagher/cli/__init__.py
@@ -7,33 +7,44 @@
 The cli should be pretty self documenting, however see docs/ for
 official documentation.
 """
+
 import os
 
-from gallagher import (
-    cc,
-    __version__
-)
+from gallagher import cc, __version__
 
 from .utils import AsyncTyper
 
 from .alarms import app as alarms_app
+from .divisions import app as divisions_app
 from .cardholders import app as cardholders_app
 from .events import app as events_app
 
+_help_text = """
+gala is a command line interface for the gallagher security command centre.\n
+It works by accessing and mutating objects on the command centre via it's\n
+JSON API, directly or via the cloud gateway.\n
+\n
+It's intent it to make available scriptable endpoints to ease automation.
+"""
+
 # 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()
+app = AsyncTyper(
+    help=_help_text,
+)
+
 # Load up all sub commands
-app.add_typer(alarms_app, name="alarms")
+app.add_typer(alarms_app, name="alarm")
+app.add_typer(divisions_app, name="div")
 app.add_typer(cardholders_app, name="ch")
-app.add_typer(events_app, name="events")
+app.add_typer(events_app, name="event")
 
 if __name__ == "__main__":
-    """ In case you are invoking this via Python directly
+    """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`
diff --git a/gallagher/cli/alarms.py b/gallagher/cli/alarms.py
index 4fd3ce4c..c8155115 100644
--- a/gallagher/cli/alarms.py
+++ b/gallagher/cli/alarms.py
@@ -1,12 +1,161 @@
-""" Alarms
+""" Alarms CLI
 
+Sub commands to interact with the alarms data
 
 """
 
+from typing import Optional
+from typing_extensions import Annotated
+
+import typer
+
+from rich.console import Console
+from rich.progress import Progress, TaskID
+from rich.table import Table
 
 from .utils import AsyncTyper
 
+from ..exception import (
+    NotFoundException,
+)
+
+from gallagher.cc.alarms import (
+    Alarms,
+)
 
 app = AsyncTyper(
-    help="list or query alarms in the command centre"
+    help="list, query, follow, act on alarms in the command centre"
 )
+
+
+@app.command("list")
+async def list():
+    """list current alarms"""
+
+    console = Console()
+
+    with console.status("[bold green]Fetching alarms...", spinner="dots"):
+
+        alarms = await Alarms.list()
+        table = Table(title="Alarms")
+
+        for header in alarms.cli_header:
+            table.add_column(header)
+
+        for row in alarms.__rich_repr__():
+            table.add_row(*row)
+
+        console.print(table)
+
+
+@app.command("get")
+async def get(
+    id: Annotated[int, typer.Argument(help="alarm id")],
+):
+    """get alarm details
+    
+    These are details of the alarm that are given by the command centre\n
+    - Item one\n
+    - Items two
+    """
+    console = Console()
+    with console.status("[bold green]Fetching alarm...", spinner="dots"):
+
+        try:
+            alarm = await Alarms.retrieve(id)
+            [console.print(r) for r in alarm.__rich_repr__()]
+        except NotFoundException as e:
+            console.print(f"[bold]No alarm with id={id} found[/bold]")
+            raise typer.Exit(code=1)
+
+@app.command("history")
+async def history(
+    id: Annotated[int, typer.Argument(help="alarm id")],
+):
+    """show history of an alarm"""
+    console = Console()
+
+    raise NotImplementedError("Not implemented")
+
+@app.command("tail")
+async def tail():
+    """watch for alarm updates"""
+    console = Console()
+
+    raise NotImplementedError("Not implemented")
+
+
+@app.command("comment")
+async def comment(
+    id: Annotated[int, typer.Argument(help="alarm id")],
+    message: Annotated[
+        str,
+        typer.Option(
+            "-m",
+            "--message",
+            help="comment to add to history",
+        ),
+    ],
+):
+    """comment on an alarm"""
+    console = Console()
+    with console.status(
+        "[magenta] Commenting on alarm ...",
+    ) as status:
+        try:
+            console.log("Finding alarm ...")
+            comment_detail = await Alarms.retrieve(id)
+            console.log("Adding comment to history ...")
+            await Alarms.comment(comment_detail, message)
+            console.print("[green]Comment posted successfully[/green]")
+        except NotFoundException as e:
+            console.print(f"[red bold]No alarm with id={id} found")
+            raise typer.Exit(code=1)
+
+
+@app.command("ack")
+async def acknowledge(
+    id: Annotated[int, typer.Argument(help="alarm id")],
+    message: Annotated[
+        Optional[str],
+        typer.Option(
+            "-m",
+            "--message",
+            help="comment to add to history",
+        ),
+    ] = None,
+):
+    """acknowledge an alarm, optionally with a comment"""
+    console = Console()
+
+
+@app.command("view")
+async def acknowledge(
+    id: Annotated[int, typer.Argument(help="alarm id")],
+    message: Annotated[
+        Optional[str],
+        typer.Option(
+            "-m",
+            "--message",
+            help="comment to add to history",
+        ),
+    ] = None,
+):
+    """mark alarm as viewed, optionally with a comment"""
+    console = Console()
+
+
+@app.command("process")
+async def acknowledge(
+    id: Annotated[int, typer.Argument(help="alarm id")],
+    message: Annotated[
+        Optional[str],
+        typer.Option(
+            "-m",
+            "--message",
+            help="comment to add to history",
+        ),
+    ] = None,
+):
+    """mark alarm as processed, optionally with a comment"""
+    console = Console()
diff --git a/gallagher/cli/cardholders.py b/gallagher/cli/cardholders.py
index a2c3c00b..1d2707ac 100644
--- a/gallagher/cli/cardholders.py
+++ b/gallagher/cli/cardholders.py
@@ -1,14 +1,18 @@
 """ Cardholder cli commands mounted at ch
 
 """
-from rich import print as rprint
+
+import typer
+
 from rich.console import Console
 from rich.table import Table
 
 from .utils import AsyncTyper
 
-from gallagher.cc.cardholders.cardholders import (
-    Cardholder
+from gallagher.cc.cardholders import Cardholder
+
+from gallagher.exception import (
+    NotFoundException,
 )
 
 app = AsyncTyper(
@@ -18,13 +22,10 @@
 
 @app.command("list")
 async def list():
-    """ list all cardholders
-    """
+    """list all cardholders"""
     console = Console()
-    with console.status(
-        "[bold green]Fetching cardholders...",
-        spinner="clock"
-    ):
+    with console.status("[bold green]Fetching cardholders...", spinner="dots"):
+
         cardholders = await Cardholder.list()
 
         table = Table(title="Cardholders")
@@ -39,7 +40,13 @@ async def list():
 
 @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__()]
+    """get a cardholder by id"""
+    console = Console()
+    with console.status("[bold]Finding cardholder...", spinner="dots"):
+
+        try:
+            cardholder = await Cardholder.retrieve(id)
+            [console.print(r) for r in cardholder.__rich_repr__()]
+        except NotFoundException as e:
+            console.print(f"[bold]No cardholder with id={id} found[/bold]")
+            raise typer.Exit(code=1)
diff --git a/gallagher/cli/divisions.py b/gallagher/cli/divisions.py
new file mode 100644
index 00000000..0e863890
--- /dev/null
+++ b/gallagher/cli/divisions.py
@@ -0,0 +1,39 @@
+""" Divisions
+
+"""
+
+from rich import print as rprint
+from rich.console import Console
+from rich.table import Table
+
+from .utils import AsyncTyper
+
+from gallagher.cc.alarms.divisions import Division
+
+
+app = AsyncTyper(
+    help="query or modify divisions"
+)
+
+
+@app.command("list")
+async def list():
+    """list all divisions"""
+    console = Console()
+    with console.status("[bold green]Fetching divisions...", spinner="clock"):
+        divisions = await Division.list()
+        table = Table(title="Divisions")
+        for header in divisions.cli_header:
+            table.add_column(header)
+
+        for row in divisions.__rich_repr__():
+            table.add_row(*row)
+
+        console.print(table)
+
+
+@app.command("get")
+async def get(id: int):
+    """get a division by id"""
+    division = await Division.retrieve(id)
+    [rprint(r) for r in division.__rich_repr__()]
diff --git a/gallagher/cli/events.py b/gallagher/cli/events.py
index 5ea2b24f..ccdcfad7 100644
--- a/gallagher/cli/events.py
+++ b/gallagher/cli/events.py
@@ -3,10 +3,7 @@
 
 """
 
-
 from .utils import AsyncTyper
 
 
-app = AsyncTyper(
-    help="query command centre events"
-)
+app = AsyncTyper(help="query command centre events")
diff --git a/gallagher/cli/utils.py b/gallagher/cli/utils.py
index d2733375..14625528 100644
--- a/gallagher/cli/utils.py
+++ b/gallagher/cli/utils.py
@@ -15,6 +15,7 @@
 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 (
diff --git a/gallagher/const.py b/gallagher/const.py
index f45764dd..a7f3e952 100644
--- a/gallagher/const.py
+++ b/gallagher/const.py
@@ -6,31 +6,23 @@
 
 
 class URL:
-    """ DNS names for the cloud gateways
+    """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
+    """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",
-        "3.106.100.112"
-    ]
+    CLOUD_GATEWAY_AU = ["3.106.1.6", "3.106.100.112"]
 
-    CLOUD_GATEWAY_US = [
-        "44.193.42.111",
-        "3.209.194.103"
-    ]
+    CLOUD_GATEWAY_US = ["44.193.42.111", "3.209.194.103"]
diff --git a/gallagher/dto/detail/__init__.py b/gallagher/dto/detail/__init__.py
index e0df6b56..4180c9d5 100644
--- a/gallagher/dto/detail/__init__.py
+++ b/gallagher/dto/detail/__init__.py
@@ -7,6 +7,10 @@
     AccessGroupDetail,
 )
 
+from .alarm import (
+    AlarmDetail,
+)
+
 from .card_type import (
     CardTypeDetail,
 )
diff --git a/gallagher/dto/detail/access_group.py b/gallagher/dto/detail/access_group.py
index 00b9baf5..4dda75c8 100644
--- a/gallagher/dto/detail/access_group.py
+++ b/gallagher/dto/detail/access_group.py
@@ -3,10 +3,11 @@
 from ..utils import (
     AppBaseModel,
     IdentityMixin,
-    HrefMixin
+    HrefMixin,
+    OptionalHrefMixin,
 )
 from ..summary import (
-    AlarmZoneSummary,
+    AlarmSummary,
     AccessSummary,
     SaltoAccessItemSummary,
 )
@@ -15,28 +16,26 @@
     PDFRef,
 )
 
-from .division import (
-    DivisionDetail
-)
+from .division import DivisionDetail
 
 
 class AccessGroupDetail(
     AppBaseModel,
     HrefMixin,
 ):
-    """
-    """
+    """ """
+
     name: str
     description: Optional[str]
     parent: Optional[AccessGroupRef]
     division: IdentityMixin
-    cardholders: Optional[HrefMixin]
+    cardholders: OptionalHrefMixin
     server_display_name: Optional[str]
 
     description: Optional[str]
     parent: Optional[AccessGroupRef]
     division: DivisionDetail
-    cardholders: Optional[HrefMixin]
+    cardholders: OptionalHrefMixin
     server_display_name: Optional[str]
     children: list[AccessGroupRef]
     personal_data_definitions: list[PDFRef]
@@ -64,4 +63,4 @@ class AccessGroupDetail(
 
     access: list[AccessSummary]
     salto_access: list[SaltoAccessItemSummary]
-    alarm_zones: list[AlarmZoneSummary]
+    alarm_zones: list[AlarmSummary]
diff --git a/gallagher/dto/detail/alarm.py b/gallagher/dto/detail/alarm.py
index e69de29b..d89bf612 100644
--- a/gallagher/dto/detail/alarm.py
+++ b/gallagher/dto/detail/alarm.py
@@ -0,0 +1,94 @@
+""" Alarm Details
+
+The following are used to parse the details of an alarm
+
+"""
+
+from typing import Optional
+from datetime import datetime
+
+from ..utils import (
+    AppBaseModel,
+    HrefMixin,
+    OptionalHrefMixin,
+    IdentityMixin,
+)
+
+from ..ref import (
+    CardholderRef,
+    OperatorRef,
+    InstructionRef,
+)
+
+from ..summary import (
+    EventTypeSummary,
+    AlarmSourceSummary,
+)
+
+
+class AlarmHistoryDetail(
+    AppBaseModel,
+):
+    """Alarm History presented in the details"""
+
+    action: str
+    time: datetime
+    comment: str  # Comment provided as part of an action
+    operator: OperatorRef
+
+
+class AlarmDetail(
+    AppBaseModel,
+    HrefMixin,
+    IdentityMixin,
+):
+    """AlarmSummary gives us unactioned events from the CC
+
+    While the detail and summary would typically differ, it seems that
+    the API endpoints return more detail in the summary endpoints.
+
+    Note: that we have a number of hrefs that we should follow to
+    HATEOAS compliance as per the documentation.
+
+    """
+
+    time: datetime
+    message: str
+    source: AlarmSourceSummary
+    type: str
+    event_type: Optional[EventTypeSummary] = None
+    priority: int
+    state: str
+    active: bool
+    division: HrefMixin
+    event: OptionalHrefMixin = None
+    note_presets: list[str] = []
+
+    # The following URLS should be used to follow through
+    # on various actions that the system allows
+    view: HrefMixin
+    comment: HrefMixin
+    acknowledge: OptionalHrefMixin = None
+    acknowledge_with_comment: OptionalHrefMixin = None
+    process: OptionalHrefMixin = None
+    process_with_comment: OptionalHrefMixin = None
+    force_process: OptionalHrefMixin = None
+
+    # Details fields, these are what don't appear in the summary
+    details: Optional[str] = ""
+    history: Optional[list[AlarmHistoryDetail]] = []
+    instruction: Optional[InstructionRef] = None
+    cardholder: Optional[CardholderRef] = None
+
+    def __rich_repr__(self):
+        return (
+            f"[blue bold] alarm",
+            f"{'id':>20} {self.id}",
+            f"{'priority':>20} {self.priority}",
+            f"{'type':>20} {self.type}",
+            f"{'message':>20} {self.message}",
+            f"{'active':>20} {'yes' if self.active else 'no'}",
+            f"",
+            f"[blue bold] hrefs",
+            f"{'acknowledge':>20} [link={self.acknowledge.href}]edit[/link]",
+        )
diff --git a/gallagher/dto/detail/card.py b/gallagher/dto/detail/card.py
new file mode 100644
index 00000000..7547c700
--- /dev/null
+++ b/gallagher/dto/detail/card.py
@@ -0,0 +1,2 @@
+""" Card details """
+
diff --git a/gallagher/dto/detail/card_type.py b/gallagher/dto/detail/card_type.py
index 0efb4861..533c8984 100644
--- a/gallagher/dto/detail/card_type.py
+++ b/gallagher/dto/detail/card_type.py
@@ -6,24 +6,20 @@
     IdentityMixin,
 )
 
-from ..summary import (
-    CardExpiryTypeSummary
-)
+from ..summary import CardExpiryTypeSummary
 
+from ..enum import CredentialsClass
+
+
+class CardTypeDetail(AppBaseModel, IdentityMixin, HrefMixin):
+    """Card Types are cards mobile or physical that are supported at a site"""
 
-class CardTypeDetail(
-    AppBaseModel,
-    IdentityMixin,
-    HrefMixin
-):
-    """ Card Types are cards mobile or physical that are supported at a site
-    """
     name: str
     minimum_number: Optional[str] = None
     maximum_number: Optional[str] = None
     initial_card_state: str
     facility_code: str
-    credential_class: str
+    credential_class: CredentialsClass
     available_card_states: list[str] = None
     default_expiry: Optional[CardExpiryTypeSummary] = None
     send_registration_email: Optional[bool] = False
diff --git a/gallagher/dto/detail/cardholder.py b/gallagher/dto/detail/cardholder.py
index f4057af6..b32f399b 100644
--- a/gallagher/dto/detail/cardholder.py
+++ b/gallagher/dto/detail/cardholder.py
@@ -1,4 +1,8 @@
-from typing import Optional
+""" Cardholder Detail """
+from typing import Optional, Any
+from typing_extensions import Self
+
+from pydantic import model_validator, Field
 
 from ..utils import (
     AppBaseModel,
@@ -7,21 +11,81 @@
 )
 
 from ..ref import (
+    CardholderExtendedRef,
     DivisionRef,
+    PlaceholderRef,
+    RoleRef,
+)
+
+from ..summary import (
+    CardholderCardSummary,
+    CardholderAccessGroupSummary,
+    PdfSummary,
 )
 
+class CardholderRelationshipDetail(
+    AppBaseModel,
+    HrefMixin,
+):
+    """ A role that a cardholder has 
+
+    Think of this as a representation of a many to many relationship
+    where this cardholder has a role in the system. Other users can
+    have the same role.
+    """
+    role: RoleRef
+    cardholder: CardholderExtendedRef
+
+
+class CardholderPersonalDataField(
+    AppBaseModel,
+    HrefMixin,
+):
+    """ A PDF field as defined in the Cardholders personal data
+
+    A definition that defines the cardholder's personal data. This
+    is essentially the Summary of the Pdf field, along with a string
+    value, which also is accessible using the key at the dictionary level.
+    """
+    definition: PdfSummary
+    value: str | HrefMixin
+    notifications: Optional[bool] = False # Local to the @Email field
+
+class CardholderPersonalDataDefinition(
+    AppBaseModel,
+):
+    """ A personal data definition for the cardholder
+
+    This is sent back as the personalDataDefinitions field in the
+    CardholderDetail response. It has a peculiar structure that 
+    we have outlined in the parse_personal_data_definitions method
+    """
+    name: str
+    contents: CardholderPersonalDataField
+
+
+class PdfAccessorWrapper:
+    """ Wrapper that holds a dictionary of accessible PDF 
 
+    The aim is to make these accessible in as python a way as possible
+
+    Note: this is not a pydantic class, and is used as a placeholder
+    to dynamically populate the dictionary of personal data fields
+    """
+    pass
+    
 class CardholderDetail(
     AppBaseModel,
     IdentityMixin,
 ):
-    """ Displays a table of cardholders
+    """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
@@ -41,8 +105,75 @@ class CardholderDetail(
     user_extended_access_time: bool = False
     windows_login_enabled: bool = False
 
+    personal_data_definitions: list[CardholderPersonalDataDefinition] = []
+    cards: list[CardholderCardSummary] = []
+    access_groups: list[CardholderAccessGroupSummary] = []
+    # operator_groups
+    # competencies
+
+    edit: HrefMixin
+    update_location: HrefMixin
+    notes: Optional[str] = None
+
+    # notifications
+    relationships: list[CardholderRelationshipDetail] = []
+    # lockers
+    # elevator_groups
+    updates: PlaceholderRef
+    # redactions
+
+    # Note this is not a pyndatic class and hence utils.py
+    # has the configuration set to allow arbitrary types
+    pdf: PdfAccessorWrapper = PdfAccessorWrapper()
+
+    @model_validator(mode='before')
+    @classmethod
+    def parse_personal_data_definitions(cls, data: Any) -> Any:
+        """ Rewrite the personalDataDefinition for it to be parseable
+
+        The personalDataDefinitions field is a list of objects, each one of 
+        which has a single object with key being the personal data field name
+        and the value being the value of the field.
+
+        This method rewrites that to be an object with name and contents fields
+        which allows us to use pydantic to parse it and make it available to the
+        user as a list of objects.
+
+        The results of this will also be used to dynamically parse the values
+        for the cardholder's PDF fields.
+        """
+
+        if 'personalDataDefinitions' in data:
+            data['personalDataDefinitions'] = [
+                {
+                    'name': name, 
+                    'contents': contents
+                } \
+                    for item in data['personalDataDefinitions'] \
+                    for name, contents in item.items()
+            ]
+
+        return data
+    
+    @model_validator(mode='after')
+    def populate_pdf_accessor(self) -> Self:
+        # For each key in the personal_data_definitions, create a new
+        # attribute on the pdf object with the key as the name
+        for pdf_field in self.personal_data_definitions:
+            # Note this sets a reference from personal_data_definitions
+            # if you compare these in the tests, they should be the same
+            setattr(
+                self.pdf, 
+                # replace spaces with underscores and make it lowercase
+                # ignore the prefixed @ symbol
+                pdf_field.name[1:].replace(' ', '_').lower(),
+                pdf_field.contents
+            )
+
+        return self
+
     def __rich_repr__(self):
-        return [
+        return (
             f"[blue bold] person",
             f"{'id':>20} {self.id}",
             f"{'first_name':>20} {self.first_name}",
@@ -55,4 +186,4 @@ def __rich_repr__(self):
             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/discover.py b/gallagher/dto/detail/discover.py
index bbfe613d..e55da599 100644
--- a/gallagher/dto/detail/discover.py
+++ b/gallagher/dto/detail/discover.py
@@ -1,6 +1,4 @@
-from typing import (
-    Optional
-)
+from typing import Optional
 
 from ..utils import (
     AppBaseModel,
@@ -38,7 +36,7 @@
 class FeaturesDetail(
     AppBaseModel,
 ):
-    """ A detailed list of features that are available on the server.
+    """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
@@ -47,53 +45,31 @@ class FeaturesDetail(
 
     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()
+
+    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
index fba21564..2b7e332b 100644
--- a/gallagher/dto/detail/division.py
+++ b/gallagher/dto/detail/division.py
@@ -3,23 +3,28 @@
 from ..utils import (
     AppBaseModel,
     IdentityMixin,
-    HrefMixin
+    HrefMixin,
+    OptionalHrefMixin,
 )
 
 
-class DivisionDetail(
-    AppBaseModel,
-    IdentityMixin,
-    HrefMixin
-):
-    """
-    """
+class DivisionDetail(AppBaseModel, IdentityMixin, HrefMixin):
+    """ """
 
     name: str
     description: Optional[str] = None
     server_display_name: Optional[str] = None
-    parent: Optional[HrefMixin] = None
+    parent: OptionalHrefMixin = None
 
     # TODO: Looks like we don't have access to visitor management
     # on our test instance at the moment
     # visitor_management: visitor.VisitorManagementSummary
+
+    def __rich_repr__(self):
+        return (
+            self.id,
+            self.name,
+            self.server_display_name \
+                if self.server_display_name \
+                    else "unavailable",
+        )
diff --git a/gallagher/dto/detail/event.py b/gallagher/dto/detail/event.py
index 85d933c6..d6df502c 100644
--- a/gallagher/dto/detail/event.py
+++ b/gallagher/dto/detail/event.py
@@ -22,8 +22,8 @@ class EventDetail(
     HrefMixin,
     IdentityMixin,
 ):
-    """ Details of an event that took place on a server
-    """
+    """Details of an event that took place on a server"""
+
     server_display_name: str
     time: datetime
     message: Optional[str]
diff --git a/gallagher/dto/detail/pdf.py b/gallagher/dto/detail/pdf.py
index a72fe814..93ab5686 100644
--- a/gallagher/dto/detail/pdf.py
+++ b/gallagher/dto/detail/pdf.py
@@ -2,18 +2,38 @@
 
 from ..utils import (
     AppBaseModel,
-    IdentityMixin,
     HrefMixin,
 )
 
+from ..ref import (
+    DivisionRef,
+    AccessGroupRef,
+)
+
+from ..enum import PdfType
+
+class PdfDetail(AppBaseModel, HrefMixin):
+    """Personal Data Fields are custom fields for a card holder"""
 
-class PdfDetail(
-    AppBaseModel,
-    HrefMixin
-):
-    """ Personal Data Fields are custom fields for a card holder
-    """
     name: str
-    description: Optional[str]
-    division: IdentityMixin
-    server_display_name: Optional[str]
+    server_display_name: Optional[str] = None
+    description: Optional[str] = None
+    division: DivisionRef
+    type: PdfType
+    default: Optional[str] = None
+    required: bool = False
+    unique: bool = False
+    default_access: str
+    operator_access: Optional[str] = None
+    sort_priority: int
+
+    access_groups: list[AccessGroupRef]
+
+    regex: Optional[str] = None
+    regex_description: Optional[str] = None
+    notification_default: bool = False
+    image_width: int = 0
+    image_height: int = 0
+    image_format: Optional[str] = None
+    content_type: Optional[str] = None
+    is_profile_image: bool = False
diff --git a/gallagher/dto/detail/role.py b/gallagher/dto/detail/role.py
index 91fd587d..04b5a46b 100644
--- a/gallagher/dto/detail/role.py
+++ b/gallagher/dto/detail/role.py
@@ -1,23 +1,13 @@
 from typing import Optional
 
-from ..utils import (
-    AppBaseModel,
-    IdentityMixin,
-    HrefMixin
-)
+from ..utils import AppBaseModel, IdentityMixin, HrefMixin
 
-from ..ref import (
-    DivisionRef
-)
+from ..ref import DivisionRef
 
 
-class RoleDetail(
-    AppBaseModel,
-    IdentityMixin,
-    HrefMixin
-):
-    """
-    """
+class RoleDetail(AppBaseModel, IdentityMixin, HrefMixin):
+    """ """
+
     name: str
     server_display_name: str
     description: Optional[str]
diff --git a/gallagher/dto/enum.py b/gallagher/dto/enum.py
new file mode 100644
index 00000000..c630c2e2
--- /dev/null
+++ b/gallagher/dto/enum.py
@@ -0,0 +1,41 @@
+""" Enumerations
+
+This module contains enumerations used in the DTOs. Enums can be shared
+across ref, detail, response, and summary DTOs.
+
+Each one of these aligns with the API documentation and is used to
+ensure that we are receive or send the correct values.
+"""
+from enum import Enum
+
+class PdfType(str, Enum):
+    """ Personal Data Field Types
+
+    Types of acceptable personal data fields allowed by the server
+    these are in accordance with the API endpoints
+    """
+    STRING = "string"
+    IMAGE = "image"
+    STR_ENUM = "strEnum"
+    NUMERIC = "numeric"
+    DATE = "date"
+    ADDRESS = "address"
+    PHONE = "phone"
+    EMAIL = "email"
+    MOBILE = "mobile"
+
+
+class CredentialsClass(str, Enum):
+    """ Credential Class related to different card types
+
+    A card type must be one of the following credential classes
+    """
+    CARD = "card"
+    DIGITAL_ID = "digitalId"
+    GOV_PASS = "govPass"
+    MOBILE = "mobile"
+    PVI = "piv"
+    PIVI = "pivi"
+    TRACKING_TAG = "trackingTag"
+    TRANSACT = "transact"
+    FIOD2 = "fido2"
\ No newline at end of file
diff --git a/gallagher/dto/payload/__init__.py b/gallagher/dto/payload/__init__.py
new file mode 100644
index 00000000..e0752050
--- /dev/null
+++ b/gallagher/dto/payload/__init__.py
@@ -0,0 +1,7 @@
+""" Data Payloads for mutating the API
+
+"""
+
+from .alarms import (
+    AlarmCommentPayload,
+)
diff --git a/gallagher/dto/payload/alarms.py b/gallagher/dto/payload/alarms.py
new file mode 100644
index 00000000..0fb84462
--- /dev/null
+++ b/gallagher/dto/payload/alarms.py
@@ -0,0 +1,12 @@
+from ..utils import (
+    AppBaseModel,
+)
+
+
+class AlarmCommentPayload(AppBaseModel):
+    """Comment Payload
+
+    This is the payload used to create a comment on an object.
+    """
+
+    comment: str
diff --git a/gallagher/dto/ref/__init__.py b/gallagher/dto/ref/__init__.py
index 8530250a..07ea7f49 100644
--- a/gallagher/dto/ref/__init__.py
+++ b/gallagher/dto/ref/__init__.py
@@ -12,18 +12,20 @@
 mostly because of the nature of the data that the command centre exposes.
 """
 
-from .access_group import (
-    AccessGroupRef
-)
+from .access_group import AccessGroupRef
 
 from .alarm import (
     AlarmRef,
     AlarmZoneRef,
 )
 
+from .card_type import (
+    CardTypeRef,
+)
+
 from .cardholder import (
     CardholderRef,
-    CardholderEventRef,
+    CardholderExtendedRef,
 )
 
 from .day_category import (
@@ -70,6 +72,10 @@
     EventGroupRef,
 )
 
+from .ext import (
+    PlaceholderRef,
+)
+
 from .items import (
     ItemRef,
 )
@@ -87,6 +93,19 @@
     ScheduleRef,
 )
 
+
+from .misc import (
+    InstructionRef,
+)
+
+from .operator import (
+    OperatorRef,
+)
+
+from .role import (
+    RoleRef,
+)
+
 from .zone import (
     AccessZoneRef,
 )
diff --git a/gallagher/dto/ref/access_group.py b/gallagher/dto/ref/access_group.py
index b198cc62..b8716e5e 100644
--- a/gallagher/dto/ref/access_group.py
+++ b/gallagher/dto/ref/access_group.py
@@ -1,13 +1,10 @@
 from ..utils import (
-    AppBaseModel,
-    HrefMixin
+    AppBaseModel, 
+    OptionalHrefMixin,
 )
 
 
-class AccessGroupRef(
-    AppBaseModel,
-    HrefMixin
-):
-    """ Access Groups is what a user is assigned to to provide access to doors
-    """
+class AccessGroupRef(AppBaseModel, OptionalHrefMixin,):
+    """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
index 6241f051..57dd8d44 100644
--- a/gallagher/dto/ref/alarm.py
+++ b/gallagher/dto/ref/alarm.py
@@ -4,19 +4,13 @@
 )
 
 
-class AlarmRef(
-    AppBaseModel,
-    HrefMixin
-):
-    """ AlarmRef represents a single alarm
-    """
+class AlarmRef(AppBaseModel, HrefMixin):
+    """AlarmRef represents a single alarm"""
+
     state: str
 
 
-class AlarmZoneRef(
-    AppBaseModel,
-    HrefMixin
-):
-    """ AccessZone represents
-    """
+class AlarmZoneRef(AppBaseModel, HrefMixin):
+    """AccessZone represents"""
+
     name: str
diff --git a/gallagher/dto/ref/card.py b/gallagher/dto/ref/card.py
new file mode 100644
index 00000000..85dba5ce
--- /dev/null
+++ b/gallagher/dto/ref/card.py
@@ -0,0 +1,14 @@
+""" Card references """
+from typing import Optional
+
+from ..utils import (
+    AppBaseModel,
+    HrefMixin,
+)
+
+class CardTypeRef(
+    AppBaseModel,
+    HrefMixin,
+):
+    name: str
+    
\ No newline at end of file
diff --git a/gallagher/dto/ref/card_type.py b/gallagher/dto/ref/card_type.py
index e69de29b..ff9403eb 100644
--- a/gallagher/dto/ref/card_type.py
+++ b/gallagher/dto/ref/card_type.py
@@ -0,0 +1,16 @@
+"""
+"""
+
+from ..utils import (
+    AppBaseModel,
+    IdentityMixin,
+    HrefMixin,
+    OptionalHrefMixin,
+)
+
+class CardTypeRef(
+    AppBaseModel,
+    HrefMixin,
+):
+    """ Reference for a card type """
+    name: str
\ No newline at end of file
diff --git a/gallagher/dto/ref/cardholder.py b/gallagher/dto/ref/cardholder.py
index 1043cf79..2ca20b02 100644
--- a/gallagher/dto/ref/cardholder.py
+++ b/gallagher/dto/ref/cardholder.py
@@ -4,7 +4,7 @@
     AppBaseModel,
     HrefMixin,
     IdentityMixin,
-    OptionalHref,
+    OptionalHrefMixin,
 )
 
 
@@ -12,22 +12,23 @@ class CardholderRef(
     AppBaseModel,
     HrefMixin,
 ):
-    """ Reference to a Cardholder
-    """
+    """Reference to a Cardholder"""
+
     name: str
 
 
-class CardholderEventRef(
+class CardholderExtendedRef(
     AppBaseModel,
     IdentityMixin,
-    OptionalHref,
+    OptionalHrefMixin,
 ):
-    """ Cardholder ref used with events
+    """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/ref/day_category.py b/gallagher/dto/ref/day_category.py
index 9f3a4cdf..68797786 100644
--- a/gallagher/dto/ref/day_category.py
+++ b/gallagher/dto/ref/day_category.py
@@ -3,17 +3,15 @@
 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 ..utils import (
-    AppBaseModel,
-    HrefMixin
-)
+
+from ..utils import AppBaseModel, HrefMixin
 
 
 class DayCategoryRef(
     AppBaseModel,
     HrefMixin,
 ):
-    """ A reference to a day category
+    """A reference to a day category
 
     This is what is sent by the day_category endpoint as of v9
     the references can be used from other endpoints.
diff --git a/gallagher/dto/ref/discover.py b/gallagher/dto/ref/discover.py
index 194bd667..d6689a46 100644
--- a/gallagher/dto/ref/discover.py
+++ b/gallagher/dto/ref/discover.py
@@ -4,171 +4,167 @@
 
 """
 
-from typing import (
-    Optional,
-)
-
 from ..utils import (
     AppBaseModel,
-    OptionalHref,
+    OptionalHrefMixin,
 )
 
 
 class FeatureAccessGroupsRef(
     AppBaseModel,
 ):
-    access_groups: Optional[OptionalHref] = OptionalHref()
+    access_groups: OptionalHrefMixin = None
 
 
 class FeatureAccessZonesRef(
     AppBaseModel,
 ):
-    access_zones: Optional[OptionalHref] = OptionalHref()
+    access_zones: OptionalHrefMixin = None
 
 
 class FeatureAlarmZonesRef(
     AppBaseModel,
 ):
-    alarm_zones: Optional[OptionalHref] = OptionalHref()
+    alarm_zones: OptionalHrefMixin = None
 
 
 class FeatureAlarmsRef(
     AppBaseModel,
 ):
-    alarms: Optional[OptionalHref] = OptionalHref()
-    divisions: Optional[OptionalHref] = OptionalHref()
-    updates: Optional[OptionalHref] = OptionalHref()
+    alarms: OptionalHrefMixin = None
+    divisions: OptionalHrefMixin = None
+    updates: OptionalHrefMixin = None
 
 
 class FeatureCardTypesRef(
     AppBaseModel,
 ):
-    assign: Optional[OptionalHref] = OptionalHref()
-    card_types: Optional[OptionalHref] = OptionalHref()
+    assign: OptionalHrefMixin = None
+    card_types: OptionalHrefMixin = None
 
 
 class FeatureCardholdersRef(
     AppBaseModel,
 ):
-    cardholders: Optional[OptionalHref] = OptionalHref()
-    changes: Optional[OptionalHref] = OptionalHref()
-    update_location_access_zones: Optional[OptionalHref] = OptionalHref()
+    cardholders: OptionalHrefMixin = None
+    changes: OptionalHrefMixin = None
+    update_location_access_zones: OptionalHrefMixin = None
 
 
 class FeatureCompetenciesRef(
     AppBaseModel,
 ):
-    competencies: Optional[OptionalHref] = OptionalHref()
+    competencies: OptionalHrefMixin = None
 
 
 class FeatureDayCategoriesRef(
     AppBaseModel,
 ):
-    day_categories: Optional[OptionalHref] = OptionalHref()
+    day_categories: OptionalHrefMixin = None
 
 
 class FeatureDivisionsRef(
     AppBaseModel,
 ):
-    divisions: Optional[OptionalHref] = OptionalHref()
+    divisions: OptionalHrefMixin = None
 
 
 class FeatureDoorsRef(
     AppBaseModel,
 ):
-    doors: Optional[OptionalHref] = OptionalHref()
+    doors: OptionalHrefMixin = None
 
 
 class FeatureElevatorsRef(
     AppBaseModel,
 ):
-    elevator_groups: Optional[OptionalHref] = OptionalHref()
+    elevator_groups: OptionalHrefMixin = None
 
 
 class FeatureEventsRef(
     AppBaseModel,
 ):
-    divisions: Optional[OptionalHref] = OptionalHref()
-    event_groups: Optional[OptionalHref] = OptionalHref()
-    events: Optional[OptionalHref] = OptionalHref()
-    updates: Optional[OptionalHref] = OptionalHref()
+    divisions: OptionalHrefMixin = None
+    event_groups: OptionalHrefMixin = None
+    events: OptionalHrefMixin = None
+    updates: OptionalHrefMixin = None
 
 
 class FeatureFenceZonesRef(
     AppBaseModel,
 ):
-    fence_zones: Optional[OptionalHref] = OptionalHref()
+    fence_zones: OptionalHrefMixin = None
 
 
 class FeatureInputsRef(
     AppBaseModel,
 ):
-    inputs: Optional[OptionalHref] = OptionalHref()
+    inputs: OptionalHrefMixin = None
 
 
 class FeatureInterlockGroupsRef(
     AppBaseModel,
 ):
-    interlock_groups: Optional[OptionalHref] = OptionalHref()
+    interlock_groups: OptionalHrefMixin = None
 
 
 class FeatureItemsRef(
     AppBaseModel,
 ):
-    item_types: Optional[OptionalHref] = OptionalHref()
-    items: Optional[OptionalHref] = OptionalHref()
-    updates: Optional[OptionalHref] = OptionalHref()
+    item_types: OptionalHrefMixin = None
+    items: OptionalHrefMixin = None
+    updates: OptionalHrefMixin = None
 
 
 class FeatureLockerBanksRef(
     AppBaseModel,
 ):
-    locker_banks: Optional[OptionalHref] = OptionalHref()
+    locker_banks: OptionalHrefMixin = None
 
 
 class FeatureMacrosRef(
     AppBaseModel,
 ):
-    macros: Optional[OptionalHref] = OptionalHref()
+    macros: OptionalHrefMixin = None
 
 
 class FeatureOperatorGroupsRef(
     AppBaseModel,
 ):
-    operator_groups: Optional[OptionalHref] = OptionalHref()
+    operator_groups: OptionalHrefMixin = None
 
 
 class FeatureOutputsRef(
     AppBaseModel,
 ):
-    outputs: Optional[OptionalHref] = OptionalHref()
+    outputs: OptionalHrefMixin = None
 
 
 class FeaturePersonalDataFieldsRef(
     AppBaseModel,
 ):
-    personal_data_fields: Optional[OptionalHref] = OptionalHref()
+    personal_data_fields: OptionalHrefMixin = None
 
 
 class FeatureReceptionsRef(
     AppBaseModel,
 ):
-    receptions: Optional[OptionalHref] = OptionalHref()
+    receptions: OptionalHrefMixin = None
 
 
 class FeatureRolesRef(
     AppBaseModel,
 ):
-    roles: Optional[OptionalHref] = OptionalHref()
+    roles: OptionalHrefMixin = None
 
 
 class FeatureSchedulesRef(
     AppBaseModel,
 ):
-    schedules: Optional[OptionalHref] = OptionalHref()
+    schedules: OptionalHrefMixin = None
 
 
 class FeatureVisitsRef(
     AppBaseModel,
 ):
-    visits: Optional[OptionalHref] = OptionalHref()
+    visits: OptionalHrefMixin = None
diff --git a/gallagher/dto/ref/division.py b/gallagher/dto/ref/division.py
index 10a33d17..162812f5 100644
--- a/gallagher/dto/ref/division.py
+++ b/gallagher/dto/ref/division.py
@@ -1,18 +1,11 @@
-from ..utils import (
-    AppBaseModel,
-    IdentityMixin,
-    HrefMixin
-)
+from ..utils import AppBaseModel, IdentityMixin, HrefMixin
 
 
-class DivisionRef(
-    AppBaseModel,
-    IdentityMixin,
-    HrefMixin
-):
-    """ Division reference is used to link to a division
+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
index ad91b6d3..b4e8b6f3 100644
--- a/gallagher/dto/ref/door.py
+++ b/gallagher/dto/ref/door.py
@@ -1,14 +1,7 @@
-from ..utils import (
-    AppBaseModel,
-    HrefMixin
-)
+from ..utils import AppBaseModel, HrefMixin
 
 
-class DoorRef(
-    AppBaseModel,
-    HrefMixin
-):
-    """ Door 
+class DoorRef(AppBaseModel, HrefMixin):
+    """Door"""
 
-    """
     name: str
diff --git a/gallagher/dto/ref/event.py b/gallagher/dto/ref/event.py
index df4312bd..f66dfe36 100644
--- a/gallagher/dto/ref/event.py
+++ b/gallagher/dto/ref/event.py
@@ -10,9 +10,10 @@ class EventGroupRef(
     AppBaseModel,
     IdentityMixin,
 ):
-    """ Event Group Reference
+    """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/ext.py b/gallagher/dto/ref/ext.py
new file mode 100644
index 00000000..b5708675
--- /dev/null
+++ b/gallagher/dto/ref/ext.py
@@ -0,0 +1,22 @@
+""" Extra models that may be temporary
+
+There are some models defined in the Gallagher API that seem to be 
+placeholders for features to come in future version of command centre.
+"""
+
+from typing import Optional
+
+from ..utils import (
+    AppBaseModel,
+    IdentityMixin,
+)
+
+class PlaceholderRef(
+    AppBaseModel,
+):
+    """Placeholder Ref
+
+    This is a placeholder for a reference to a future object
+    """
+
+    reserved: Optional[str] = "for future use"
diff --git a/gallagher/dto/ref/items.py b/gallagher/dto/ref/items.py
index 78eb5186..7172a2cb 100644
--- a/gallagher/dto/ref/items.py
+++ b/gallagher/dto/ref/items.py
@@ -1,14 +1,7 @@
+from ..utils import AppBaseModel, HrefMixin
 
-from ..utils import (
-    AppBaseModel,
-    HrefMixin
-)
 
+class ItemRef(AppBaseModel, HrefMixin):
+    """Reference to an ItemType"""
 
-class ItemRef(
-    AppBaseModel,
-    HrefMixin
-):
-    """ Reference to an ItemType
-    """
     name: str
diff --git a/gallagher/dto/ref/misc.py b/gallagher/dto/ref/misc.py
new file mode 100644
index 00000000..23ff88c9
--- /dev/null
+++ b/gallagher/dto/ref/misc.py
@@ -0,0 +1,22 @@
+""" Misc models
+
+These are miscellaneous models that are used in places like alarms,
+they may need to be refactored into other packages as we see fit. 
+"""
+
+from ..utils import (
+    AppBaseModel,
+    HrefMixin,
+)
+
+
+class InstructionRef(
+    AppBaseModel,
+    HrefMixin,
+):
+    """Operator reference
+
+    At present used in Alarm details, revise as required.
+    """
+
+    name: str
diff --git a/gallagher/dto/ref/operator.py b/gallagher/dto/ref/operator.py
index e69de29b..b4f740dc 100644
--- a/gallagher/dto/ref/operator.py
+++ b/gallagher/dto/ref/operator.py
@@ -0,0 +1,21 @@
+""" Operator
+
+An operator is someone who has operated an door and caused an
+event like an alarm.
+
+"""
+
+from ..utils import (
+    AppBaseModel,
+)
+
+
+class OperatorRef(
+    AppBaseModel,
+):
+    """Operator reference
+
+    At present used in Alarm details, revsie as required.
+    """
+
+    name: str
diff --git a/gallagher/dto/ref/pdf.py b/gallagher/dto/ref/pdf.py
index bd573baa..95caef07 100644
--- a/gallagher/dto/ref/pdf.py
+++ b/gallagher/dto/ref/pdf.py
@@ -7,17 +7,16 @@
 - Keys are prefixed with @
 - Presence of space in the field name
 """
+
 from ..utils import (
     AppBaseModel,
+    IdentityMixin,
     HrefMixin,
 )
 
 
-class PDFRef(
-    AppBaseModel,
-    HrefMixin
-):
-    """ Personal Data Fields are custom fields for a card holder
+class PDFRef(AppBaseModel, IdentityMixin, 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
@@ -26,4 +25,5 @@ class PDFRef(
     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/role.py b/gallagher/dto/ref/role.py
index e69de29b..53c577a7 100644
--- a/gallagher/dto/ref/role.py
+++ b/gallagher/dto/ref/role.py
@@ -0,0 +1,14 @@
+"""
+"""
+
+from ..utils import (
+    AppBaseModel,
+    HrefMixin,
+)
+
+class RoleRef(
+    AppBaseModel,
+    HrefMixin,
+):
+    name: str
+    
\ No newline at end of file
diff --git a/gallagher/dto/ref/salto.py b/gallagher/dto/ref/salto.py
index 76649ede..5ae23496 100644
--- a/gallagher/dto/ref/salto.py
+++ b/gallagher/dto/ref/salto.py
@@ -1,21 +1,15 @@
-from ..utils import (
-    AppBaseModel,
-    HrefMixin
-)
+from ..utils import AppBaseModel, HrefMixin
 
 
 class SaltoItemTypeRef(
     AppBaseModel,
 ):
-    """ A Salto Item Type is a SALTO product category e.g door
-    """
+    """A Salto Item Type is a SALTO product category e.g door"""
+
     value: str
 
 
-class SaltoItemRef(
-    AppBaseModel,
-    HrefMixin
-):
-    """ A Salto Item is a particular product e.g Salto CU5000
-    """
+class SaltoItemRef(AppBaseModel, HrefMixin):
+    """A Salto Item is a particular product e.g Salto CU5000"""
+
     name: str
diff --git a/gallagher/dto/ref/schedule.py b/gallagher/dto/ref/schedule.py
index a81daa36..f6b10084 100644
--- a/gallagher/dto/ref/schedule.py
+++ b/gallagher/dto/ref/schedule.py
@@ -2,16 +2,10 @@
 
 """
 
-from ..utils import (
-    AppBaseModel,
-    HrefMixin
-)
+from ..utils import AppBaseModel, HrefMixin
 
 
-class ScheduleRef(
-    AppBaseModel,
-    HrefMixin
-):
-    """ Schedule is a time
-    """
+class ScheduleRef(AppBaseModel, HrefMixin):
+    """Schedule is a time"""
+
     pass
diff --git a/gallagher/dto/ref/zone.py b/gallagher/dto/ref/zone.py
index 64adac8e..b2eb8615 100644
--- a/gallagher/dto/ref/zone.py
+++ b/gallagher/dto/ref/zone.py
@@ -5,11 +5,7 @@
 )
 
 
-class AccessZoneRef(
-    AppBaseModel,
-    IdentityMixin,
-    HrefMixin
-):
-    """ AccessZone represents
-    """
+class AccessZoneRef(AppBaseModel, IdentityMixin, HrefMixin):
+    """AccessZone represents"""
+
     name: str
diff --git a/gallagher/dto/response/__init__.py b/gallagher/dto/response/__init__.py
index e9fed1fc..ae8edaf9 100644
--- a/gallagher/dto/response/__init__.py
+++ b/gallagher/dto/response/__init__.py
@@ -4,7 +4,7 @@
 """
 
 from .alarm import (
-    AlarmResponse,
+    AlarmSummaryResponse,
 )
 
 from .card_type import (
@@ -24,7 +24,7 @@
 )
 
 from .division import (
-    DivisionDetailResponse,
+    DivisionSummaryResponse,
 )
 
 from .door import (
diff --git a/gallagher/dto/response/alarm.py b/gallagher/dto/response/alarm.py
index 2afc3424..d4d9122d 100644
--- a/gallagher/dto/response/alarm.py
+++ b/gallagher/dto/response/alarm.py
@@ -1,17 +1,38 @@
 from ..utils import (
-    AppBaseModel,
+    AppBaseResponseModel,
     HrefMixin,
 )
 
 from ..summary import (
-    AlarmZoneSummary,
+    AlarmSummary,
 )
 
 
-class AlarmResponse(
-    AppBaseModel,
+class AlarmSummaryResponse(
+    AppBaseResponseModel,
 ):
-    """ AlarmResponse represents a single alarm
-    """
-    alarms: list[AlarmZoneSummary]
+    """AlarmSummaryResponse represents a single alarm"""
+
+    alarms: list[AlarmSummary]
     updates: HrefMixin
+
+    @property
+    def result_set(self) -> list[AlarmSummary]:
+        """ Wrap summary response target property
+
+        the sql interface will call this property and each summary
+        response is expected to override this and return the appropriate
+        target property
+        """
+        return self.alarms
+
+    @property
+    def cli_header(self):
+        return ("id", "priority", "time", "type")
+
+    def __rich_repr__(self):
+        return (r.__rich_repr__() for r in self.alarms)
+
+    def __str__(self):
+        return f"{len(self.results)} cardholders"
+
diff --git a/gallagher/dto/response/card_type.py b/gallagher/dto/response/card_type.py
index 482978c7..96272a0b 100644
--- a/gallagher/dto/response/card_type.py
+++ b/gallagher/dto/response/card_type.py
@@ -1,19 +1,16 @@
 from typing import Optional
 
 from ..utils import (
-    AppBaseModel,
-    HrefMixin
+    HrefMixin,
+    AppBaseResponseWithFollowModel,
 )
 
-from ..detail import (
-    CardTypeDetail
-)
+from ..detail import CardTypeDetail
 
 
 class CardTypeResponse(
-    AppBaseModel,
+    AppBaseResponseWithFollowModel,
 ):
-    """ Card Types are cards mobile or physical that are supported at a site
-    """
+    """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
index 41c2f2f5..e65e1e82 100644
--- a/gallagher/dto/response/cardholder.py
+++ b/gallagher/dto/response/cardholder.py
@@ -1,37 +1,42 @@
 """
 
 """
+
 from ..utils import (
-    AppBaseModel,
+    AppBaseResponseModel,
 )
 
-from ..summary import (
-    CardholderSummary
-)
+from ..summary import CardholderSummary
 
 
 class CardholderSummaryResponse(
-    AppBaseModel
+    AppBaseResponseModel,
 ):
-    """ Summary response for cardholder list and search
+    """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 result_set(self) -> list[CardholderSummary]:
+        """ Wrap summary response target property
+
+        the sql interface will call this property and each summary
+        response is expected to override this and return the appropriate
+        target property
+        """
+        return self.results
+
     @property
     def cli_header(self):
-        return [
-            "Id",
-            "First name",
-            "Last name",
-            "Authorised"
-        ]
+        return ("id", "first name", "last name", "authorised")
 
     def __rich_repr__(self):
-        return [r.__rich_repr__() for r in self.results]
+        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
index 0fa10138..85b3946e 100644
--- a/gallagher/dto/response/day_category.py
+++ b/gallagher/dto/response/day_category.py
@@ -1,8 +1,8 @@
 from typing import Optional
 
 from ..utils import (
-    AppBaseModel,
-    HrefMixin
+    HrefMixin,
+    AppBaseResponseWithFollowModel,
 )
 
 from ..ref import (
@@ -11,10 +11,8 @@
 
 
 class DayCategoryResponse(
-    AppBaseModel,
+    AppBaseResponseWithFollowModel,
 ):
-    """ The response has a list of results and a link to the next page
-    """
+    """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
index a42be437..29587cc6 100644
--- a/gallagher/dto/response/discover.py
+++ b/gallagher/dto/response/discover.py
@@ -3,7 +3,7 @@
 )
 
 from ..utils import (
-    AppBaseModel,
+    AppBaseResponseModel,
 )
 
 from ..detail import (
@@ -12,9 +12,9 @@
 
 
 class DiscoveryResponse(
-    AppBaseModel,
+    AppBaseResponseModel,
 ):
-    """ A response that outlines the capability of the server
+    """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
@@ -25,16 +25,15 @@ class DiscoveryResponse(
 
     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.  
+    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()
+    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
-        """
+        """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
index 37eec892..2b900b35 100644
--- a/gallagher/dto/response/division.py
+++ b/gallagher/dto/response/division.py
@@ -1,21 +1,39 @@
 from typing import Optional
 
 from ..utils import (
-    AppBaseModel,
     HrefMixin,
+    AppBaseResponseWithFollowModel,
 )
 
-from ..detail import (
-    DivisionDetail
-)
+from ..summary import DivisionSummary
+
+from ..detail import DivisionDetail
 
 
-class DivisionDetailResponse(
-    AppBaseModel
+class DivisionSummaryResponse(
+    AppBaseResponseWithFollowModel,
 ):
-    """ Division
+    """Division Summary
+
+    This should return the summary and as per the documentation
+    the detail should have more than what we have here
+
+    TODO: check this is different
 
     """
 
-    results: list[DivisionDetail]
-    next: Optional[HrefMixin] = None
+    results: list[DivisionSummary]
+
+    @property
+    def cli_header(self):
+        return (
+            "id",
+            "name",
+            "server display name",
+        )
+
+    def __rich_repr__(self):
+        return (r.__rich_repr__() for r in self.results)
+
+    def __str__(self):
+        return f"{len(self.results)} divisions"
diff --git a/gallagher/dto/response/door.py b/gallagher/dto/response/door.py
index da5b906f..e8e39dd1 100644
--- a/gallagher/dto/response/door.py
+++ b/gallagher/dto/response/door.py
@@ -1,5 +1,5 @@
 from ..utils import (
-    AppBaseModel,
+    AppBaseResponseModel,
 )
 
 from ..summary import (
@@ -8,9 +8,8 @@
 
 
 class DoorSummaryResponse(
-    AppBaseModel
+    AppBaseResponseModel,
 ):
-    """
+    """ """
 
-    """
     results: list[DoorSummary]
diff --git a/gallagher/dto/response/event.py b/gallagher/dto/response/event.py
index 9ac45a93..703dee3c 100644
--- a/gallagher/dto/response/event.py
+++ b/gallagher/dto/response/event.py
@@ -1,9 +1,10 @@
 from typing import Optional
 
 from ..utils import (
-    AppBaseModel,
     HrefMixin,
     IdentityMixin,
+    AppBaseResponseModel,
+    AppBaseResponseWithFollowModel,
 )
 
 from ..summary import (
@@ -13,23 +14,20 @@
 
 
 class EventSummaryResponse(
-    AppBaseModel,
+    AppBaseResponseWithFollowModel,
 ):
 
     events: list[EventSummary]
 
-    next: HrefMixin
-    previous: HrefMixin
-    updates: HrefMixin
-
 
 class EventTypeResponse(
-    AppBaseModel,
+    AppBaseResponseModel,
 ):
-    """ Event Type Response
+    """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/items.py b/gallagher/dto/response/items.py
index 159cd7d8..7a22cc3d 100644
--- a/gallagher/dto/response/items.py
+++ b/gallagher/dto/response/items.py
@@ -1,32 +1,31 @@
 from typing import Optional
 
 from ..utils import (
-    AppBaseModel,
-    HrefMixin
+    HrefMixin,
+    AppBaseResponseModel,
+    AppBaseResponseWithFollowModel,
 )
 
-from ..summary import (
-    ItemSummary,
-    ItemTypeSummary
-)
+from ..summary import ItemSummary, ItemTypeSummary
 
 
 class ItemsSummaryResponse(
-    AppBaseModel,
+    AppBaseResponseWithFollowModel,
 ):
-    """  ItemsResponse is the list of items from the API
+    """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,
+    AppBaseResponseModel,
 ):
-    """ Every security centre can provide a list of item types
+    """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/pdf.py b/gallagher/dto/response/pdf.py
index ebf64486..5f1965a0 100644
--- a/gallagher/dto/response/pdf.py
+++ b/gallagher/dto/response/pdf.py
@@ -8,8 +8,9 @@
 href references or as individual fields in the user's profile.
 
 """
+
 from ..utils import (
-    AppBaseModel,
+    AppBaseResponseModel,
 )
 
 from ..ref import (
@@ -18,12 +19,13 @@
 
 
 class PdfResponse(
-    AppBaseModel,
+    AppBaseResponseModel,
 ):
-    """ Personal Definition fields
+    """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/schedule.py b/gallagher/dto/response/schedule.py
index 3c0479fe..10aee254 100644
--- a/gallagher/dto/response/schedule.py
+++ b/gallagher/dto/response/schedule.py
@@ -1,15 +1,13 @@
 from ..utils import (
-    AppBaseModel,
+    AppBaseResponseModel,
 )
 
-from ..summary import (
-    ScheduleSummary
-)
+from ..summary import ScheduleSummary
 
 
 class ScheduleSummaryResponse(
-    AppBaseModel
+    AppBaseResponseModel,
 ):
-    """ Schedule is a time
-    """
+    """Schedule is a time"""
+
     results: list[ScheduleSummary]
diff --git a/gallagher/dto/summary/__init__.py b/gallagher/dto/summary/__init__.py
index 12b0795d..09211ad6 100644
--- a/gallagher/dto/summary/__init__.py
+++ b/gallagher/dto/summary/__init__.py
@@ -1,16 +1,17 @@
-"""
+""" Summary
 
 
 """
 
 from .access_group import (
     AccessSummary,
-    AccessGroupSummary
+    AccessGroupSummary,
+    CardholderAccessGroupSummary,
 )
 
 from .alarm import (
     AlarmSourceSummary,
-    AlarmZoneSummary,
+    AlarmSummary,
 )
 
 from .card_type import (
@@ -20,12 +21,15 @@
 
 from .cardholder import (
     CardholderSummary,
+    CardholderCardSummary,
 )
 
 from .day_category import (
     DayCategorySummary,
 )
 
+from .division import DivisionSummary
+
 from .door import (
     DoorSummary,
 )
@@ -53,3 +57,7 @@
     VisitorManagementSummary,
     VisitorTypeSummary,
 )
+
+from .pdf import (
+    PdfSummary,
+)
diff --git a/gallagher/dto/summary/access_group.py b/gallagher/dto/summary/access_group.py
index 004c6e32..aa537874 100644
--- a/gallagher/dto/summary/access_group.py
+++ b/gallagher/dto/summary/access_group.py
@@ -3,7 +3,11 @@
 from ..utils import (
     AppBaseModel,
     IdentityMixin,
-    HrefMixin
+    HrefMixin,
+    OptionalHrefMixin,
+    TypeValuePair,
+    from_optional_datetime,
+    until_optional_datetime,
 )
 
 from ..ref import (
@@ -17,8 +21,8 @@ class AccessSummary(
     AppBaseModel,
     HrefMixin,
 ):
-    """ Access is zone paired with a schedule
-    """
+    """Access is zone paired with a schedule"""
+
     access_zone: AccessZoneRef
     schedule: ScheduleRef
 
@@ -26,15 +30,33 @@ class AccessSummary(
 class AccessGroupSummary(
     AppBaseModel,
 ):
-    """ AccessGroup Summary is what the API returns on searches
+    """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]
+    cardholders: OptionalHrefMixin
     server_display_name: Optional[str]
+
+
+
+class CardholderAccessGroupSummary(
+    AppBaseModel,
+    HrefMixin,
+):
+    """ An Access Group assigned to a cardholder.
+
+    Essentially when an access group is assigned to a cardholder
+    it inherits the properties and additionally a start and end date
+    """
+    access_group: AccessGroupRef
+    status: TypeValuePair
+
+    valid_from: from_optional_datetime = None # Appears as from in the API
+    valid_until: until_optional_datetime = None
diff --git a/gallagher/dto/summary/alarm.py b/gallagher/dto/summary/alarm.py
index 90226578..e78effa6 100644
--- a/gallagher/dto/summary/alarm.py
+++ b/gallagher/dto/summary/alarm.py
@@ -4,12 +4,11 @@
 from ..utils import (
     AppBaseModel,
     HrefMixin,
+    OptionalHrefMixin,
     IdentityMixin,
 )
 
-from .event import (
-    EventTypeSummary
-)
+from .event import EventTypeSummary
 
 
 class AlarmSourceSummary(
@@ -17,22 +16,26 @@ class AlarmSourceSummary(
     HrefMixin,
     IdentityMixin,
 ):
-    """ AlarmSource represents a device that has triggered an alarm
-    """
+    """AlarmSource represents a device that has triggered an alarm"""
+
     name: str
 
 
-class AlarmZoneSummary(
+class AlarmSummary(
     AppBaseModel,
     HrefMixin,
     IdentityMixin,
 ):
-    """ #TODO: Revise this if it shows up in other places
+    """AlarmSummary gives us unactioned events from the CC
+
+    While the detail and summary would typically differ, it seems that
+    the API endpoints return more detail in the summary endpoints.
+
+    Note: that we have a number of hrefs that we should follow to
+    HATEOAS compliance as per the documentation.
 
-    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
@@ -42,12 +45,27 @@ class AlarmZoneSummary(
     state: str
     active: bool
     division: HrefMixin
-    event: Optional[HrefMixin] = None
+    event: OptionalHrefMixin = None
     note_presets: list[str] = []
+
+    # The following URLS should be used to follow through
+    # on various actions that the system allows
     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
+    acknowledge: OptionalHrefMixin = None
+    acknowledge_with_comment: OptionalHrefMixin = None
+    process: OptionalHrefMixin = None
+    process_with_comment: OptionalHrefMixin = None
+    force_process: OptionalHrefMixin = None
+
+    def __rich_repr__(self):
+        return (
+            self.id,
+            f"{self.priority}",
+            self.time.strftime('%c'),
+            self.type,
+        )
+
+    def __str__(self):
+        return f"{self.id} {self.priority} {self.type}"
+
diff --git a/gallagher/dto/summary/card.py b/gallagher/dto/summary/card.py
new file mode 100644
index 00000000..f45e0488
--- /dev/null
+++ b/gallagher/dto/summary/card.py
@@ -0,0 +1,9 @@
+""" Summary models for Cards and annex information
+"""
+
+from typing import Optional
+
+from ..utils import (
+    AppBaseModel,
+    IdentityMixin,
+)
diff --git a/gallagher/dto/summary/card_type.py b/gallagher/dto/summary/card_type.py
index 67ef118c..8f1ed05a 100644
--- a/gallagher/dto/summary/card_type.py
+++ b/gallagher/dto/summary/card_type.py
@@ -5,10 +5,7 @@
     IdentityMixin,
 )
 
-
-class CardExpiryTypeSummary(
-    AppBaseModel
-):
+class CardExpiryTypeSummary(AppBaseModel):
     expiry_type: Optional[str] = None
 
 
@@ -16,11 +13,12 @@ class CardSummary(
     AppBaseModel,
     IdentityMixin,
 ):
-    """ Card summary as sent by the Event objects
+    """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
index ed041010..d65fad6b 100644
--- a/gallagher/dto/summary/cardholder.py
+++ b/gallagher/dto/summary/cardholder.py
@@ -1,11 +1,20 @@
 from typing import Optional
 
+from datetime import datetime
+
 from ..utils import (
     AppBaseModel,
     IdentityMixin,
     HrefMixin,
+    from_optional_datetime,
+    until_optional_datetime,
+)
+
+from ..ref import (
+    CardTypeRef,
 )
 
+from ..enum import CredentialsClass
 
 class CardholderSummary(
     AppBaseModel,
@@ -13,9 +22,9 @@ class CardholderSummary(
     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} 
+    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.
     """
@@ -27,12 +36,75 @@ class CardholderSummary(
     authorised: bool = False
 
     def __rich_repr__(self):
-        return [
+        return (
             self.id,
             self.first_name,
             self.last_name,
-            "yes" if self.authorised else "no"
-        ]
+            "yes" if self.authorised else "no",
+        )
 
     def __str__(self):
         return f"{self.id} {self.first_name} {self.last_name}"
+
+
+class CardholderCardStatusSummary(
+    AppBaseModel,
+):
+    """ Used by CardholderCardSummary to show the status of an assigned card
+    """
+    value: str
+    type: str
+
+
+class CardholderCardInvitationSummary(
+    AppBaseModel,
+    HrefMixin,
+):
+    """ Used by CardholderCardSummary to show status of an invitation
+
+    Invitations are sent by the Gallagher system to a cardholder via
+    email and then text messages for mobile credentials to activate
+    the app.
+    """
+    email: str
+    mobile: Optional[str] = None
+    single_factor_only: bool = False
+    status: str
+
+class CardholderCardSummary(
+    AppBaseModel,
+    HrefMixin,
+):
+    """ A card assigned to a cardholder, note that these are not the same as
+    the card objects themselves, they miss a few fields that the card object
+    has and are only used in the context of a cardholder's detail page.
+    
+    These children objects are referenced by their `href` as they aren't assigned
+    an identifier object in the system.
+    """
+    number: str
+    card_serial_number: Optional[str] = None
+    issue_level: Optional[int] = None
+
+    status: CardholderCardStatusSummary
+
+    type: CardTypeRef
+
+    invitation: Optional[CardholderCardInvitationSummary] = None
+
+    valid_from: from_optional_datetime = None # Appears as from in the API
+    valid_until: until_optional_datetime = None
+    credential_class: CredentialsClass
+    trace: bool = False
+    last_printed_or_encoded_time: Optional[datetime] = None
+    last_printed_or_encoded_issue_level: int = 0
+    pin: Optional[str] = None # Leading zeros are significant
+    visitor_contractor: bool = False
+    owned_by_site: bool = False
+    credential_id: str = "reserved" # TODO: Reserved for future use?
+    # TODO: Reserved for future use?
+    # Test instance returns an int when a mobile credential is assigned
+    # the docs still suggest this should be a string instead, leaving this
+    # here to check into the future
+    ble_facility_id:  str | int = "reserved" 
+
diff --git a/gallagher/dto/summary/day_category.py b/gallagher/dto/summary/day_category.py
index 7747d2c7..092e73df 100644
--- a/gallagher/dto/summary/day_category.py
+++ b/gallagher/dto/summary/day_category.py
@@ -3,20 +3,17 @@
 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
-)
+from ..utils import AppBaseModel, HrefMixin
 
 
 class DayCategorySummary(
     AppBaseModel,
     HrefMixin,
 ):
-    """ Represents a single entry from the response
-    """
+    """Represents a single entry from the response"""
 
     name: str
     description: Optional[str]
diff --git a/gallagher/dto/summary/division.py b/gallagher/dto/summary/division.py
index e69de29b..1df2c931 100644
--- a/gallagher/dto/summary/division.py
+++ b/gallagher/dto/summary/division.py
@@ -0,0 +1,34 @@
+""" Division Summary
+
+"""
+
+from typing import Optional
+
+from ..utils import (
+    AppBaseModel,
+    IdentityMixin,
+    HrefMixin,
+    OptionalHrefMixin,
+)
+
+
+class DivisionSummary(AppBaseModel, IdentityMixin, HrefMixin):
+    """Division Summary
+
+    Primarily separated for the cli to be able to distinguish between
+    sumamry and detail responses, this is also very useful for the CLI
+    """
+
+    name: str
+    description: Optional[str] = None
+    server_display_name: Optional[str] = None
+    parent: OptionalHrefMixin = None
+
+    def __rich_repr__(self):
+        return (
+            self.id,
+            self.name,
+            self.server_display_name \
+                if self.server_display_name \
+                else "unavailable",
+        )
diff --git a/gallagher/dto/summary/door.py b/gallagher/dto/summary/door.py
index 4037180e..e140cb2d 100644
--- a/gallagher/dto/summary/door.py
+++ b/gallagher/dto/summary/door.py
@@ -1,8 +1,4 @@
-from ..utils import (
-    AppBaseModel,
-    IdentityMixin,
-    HrefMixin
-)
+from ..utils import AppBaseModel, IdentityMixin, HrefMixin
 
 
 class DoorSummary(
@@ -10,7 +6,6 @@ class DoorSummary(
     IdentityMixin,
     HrefMixin,
 ):
-    """
+    """ """
 
-    """
     name: str
diff --git a/gallagher/dto/summary/event.py b/gallagher/dto/summary/event.py
index 775fec9d..e1b04ca3 100644
--- a/gallagher/dto/summary/event.py
+++ b/gallagher/dto/summary/event.py
@@ -5,13 +5,13 @@
     AppBaseModel,
     HrefMixin,
     IdentityMixin,
-    OptionalHref,
+    OptionalHrefMixin,
 )
 
 from ..ref import (
     AlarmRef,
     CardholderRef,
-    CardholderEventRef,
+    CardholderExtendedRef,
     DoorRef,
     AccessZoneRef,
     DivisionRef,
@@ -32,8 +32,8 @@ class EventTypeSummary(
     AppBaseModel,
     IdentityMixin,
 ):
-    """ An event type has identifiers and names
-    """
+    """An event type has identifiers and names"""
+
     name: str
 
 
@@ -41,11 +41,12 @@ class EventGroupSummary(
     AppBaseModel,
     IdentityMixin,
 ):
-    """ Event Groups are a collection of event types
+    """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]
 
@@ -55,8 +56,8 @@ class EventSummary(
     HrefMixin,
     IdentityMixin,
 ):
-    """ Summary of events that have occurred on the server
-    """
+    """Summary of events that have occurred on the server"""
+
     server_display_name: Optional[str] = None
     time: datetime
     message: Optional[str] = None
@@ -70,10 +71,10 @@ class EventSummary(
     type: Optional[EventTypeSummary] = None
     event_type: Optional[EventTypeSummary] = None
     division: Optional[DivisionRef] = None
-    cardholder: Optional[CardholderEventRef] = None
+    cardholder: Optional[CardholderExtendedRef] = None
     entry_access_zone: Optional[AccessZoneRef] = None
     exit_access_zone: Optional[AccessZoneRef] = None
     door: Optional[DoorRef] = None
-    access_group: OptionalHref = None
+    access_group: OptionalHrefMixin = None
     card: Optional[CardSummary] = None
     modified_item: Optional[ItemSummary] = None
diff --git a/gallagher/dto/summary/items.py b/gallagher/dto/summary/items.py
index 593390e1..6a6db83e 100644
--- a/gallagher/dto/summary/items.py
+++ b/gallagher/dto/summary/items.py
@@ -1,20 +1,17 @@
 from typing import Optional
 
-from ..utils import (
-    AppBaseModel,
-    IdentityMixin,
-    HrefMixin
-)
+from ..utils import AppBaseModel, IdentityMixin, HrefMixin
 
 
 class ItemTypeSummary(
     AppBaseModel,
 ):
-    """ Items Types only provide the name and id
+    """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
@@ -25,8 +22,9 @@ class ItemSummary(
     HrefMixin,
     IdentityMixin,
 ):
-    """ Summary of an Item which adds the notes and 
+    """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
index e69de29b..7d7cfae6 100644
--- a/gallagher/dto/summary/locker.py
+++ b/gallagher/dto/summary/locker.py
@@ -0,0 +1,19 @@
+""" Locker groups """
+
+from ..utils import (
+    AppBaseModel,
+    IdentityMixin,
+    HrefMixin,
+    OptionalHrefMixin,
+)
+
+
+class CardholderLockerSummary(
+    AppBaseModel,
+    IdentityMixin,
+    HrefMixin,
+):
+    """
+    """
+    pass
+
diff --git a/gallagher/dto/summary/pdf.py b/gallagher/dto/summary/pdf.py
index e69de29b..cd1f1781 100644
--- a/gallagher/dto/summary/pdf.py
+++ b/gallagher/dto/summary/pdf.py
@@ -0,0 +1,22 @@
+""" Personal Data Field Summary
+"""
+from ..enum import PdfType
+
+from ..utils import (
+    AppBaseModel,
+    HrefMixin,
+    IdentityMixin,
+)
+
+class PdfSummary(
+    AppBaseModel,
+    HrefMixin,
+    IdentityMixin,
+):
+    """ Personal Data Field Summary
+
+    Primarily used as the definition in the Cardholder response
+    """
+    
+    name: str
+    type: PdfType # See gallagher docs for enum
\ No newline at end of file
diff --git a/gallagher/dto/summary/salto.py b/gallagher/dto/summary/salto.py
index 4e204e62..bbfea7c7 100644
--- a/gallagher/dto/summary/salto.py
+++ b/gallagher/dto/summary/salto.py
@@ -12,9 +12,8 @@
 class SaltoAccessItemSummary(
     AppBaseModel,
 ):
-    """ A Summary of Salto items
+    """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
index eef9257e..c4554775 100644
--- a/gallagher/dto/summary/schedule.py
+++ b/gallagher/dto/summary/schedule.py
@@ -1,13 +1,7 @@
-from ..utils import (
-    AppBaseModel,
-    HrefMixin
-)
+from ..utils import AppBaseModel, HrefMixin
 
 
-class ScheduleSummary(
-    AppBaseModel,
-    HrefMixin
-):
-    """ Schedule is a time
-    """
+class ScheduleSummary(AppBaseModel, HrefMixin):
+    """Schedule is a time"""
+
     name: str
diff --git a/gallagher/dto/summary/visitor.py b/gallagher/dto/summary/visitor.py
index 215e8229..a1ad7aa2 100644
--- a/gallagher/dto/summary/visitor.py
+++ b/gallagher/dto/summary/visitor.py
@@ -1,38 +1,30 @@
-from ..utils import (
-    AppBaseModel,
-    IdentityMixin
-)
+from ..utils import AppBaseModel, IdentityMixin
 
-from ..ref import (
-    AccessGroupRef
-)
+from ..ref import AccessGroupRef
 
 from .access_group import (
     AccessGroupSummary,
 )
 
 
-class VisitorTypeSummary(
-    AppBaseModel,
-    IdentityMixin
-):
-    """ Visitor Types are a combination of:
+class VisitorTypeSummary(AppBaseModel, IdentityMixin):
+    """Visitor Types are a combination of:
     - Access Group
     - Host Access Groups
     - Visitor Access Groups
 
     these are represented in Divisions
     """
+
     access_group: AccessGroupRef
     host_access_groups: list[AccessGroupSummary]
     visitor_access_groups: list[AccessGroupSummary]
 
 
-class VisitorManagementSummary(
-    AppBaseModel
-):
-    """ This is the summary of the Visitor Management that appears
+class VisitorManagementSummary(AppBaseModel):
+    """This is the summary of the Visitor Management that appears
     in Divisions.
     """
+
     active: bool
     visitor_types: list[VisitorTypeSummary]
diff --git a/gallagher/dto/utils.py b/gallagher/dto/utils.py
index 7eac5472..2de1681d 100644
--- a/gallagher/dto/utils.py
+++ b/gallagher/dto/utils.py
@@ -1,29 +1,124 @@
 """ Data Transfer Object (DTO) utilities
 
+These are utilities that DTO refs, summaries and detail objects
+inherit from. It defines the basic parsing mechanism based on the
+configuration of each endpoint class.
+
+In addition we provide a based class for Responses to encapsulate
+behaviour outlined by the Gallagher API.
+
+Mixins are classes that provided an isolated behaviour for DTO
+classes to inherit from, for example Identifier, Hrefs, etc.
+
+There are some utility functions that are used internally, please
+ignore them unless you are developing the client itself.
 
 """
 
 from typing import (
     Optional,
 )
+from functools import cached_property
+from typing_extensions import Annotated
+
 from datetime import datetime
 
 from pydantic import (
     BaseModel,
     ConfigDict,
+    HttpUrl,
+    Field,
 )
 
+# Annotations for fields that are reserved words in Python
+from_optional_datetime = Annotated[
+    Optional[datetime],
+    Field(..., alias="from")
+]
 
-def to_lower_camel(name: str) -> str:
-    """
-    Converts a snake_case string to lowerCamelCase
+until_optional_datetime = Annotated[
+    Optional[datetime],
+    Field(..., alias="until")
+]
+
+
+# Helper functions for parsing
+def _to_lower_camel(name: str) -> str:
+    """Converts a snake_case string to lowerCamelCase
+
+    Not designed for use outside of the scope of this package
     """
     upper = "".join(word.capitalize() for word in name.split("_"))
     return upper[:1].lower() + upper[1:]
 
 
+# Ensure that the primitive wrappers such as Mixins appear
+# before the generic classes for parsing utilities
+
+
+class IdentityMixin(BaseModel):
+    """Identifier
+
+    This mixin is used to define the identifier field for all
+    responses from the Gallagher API.
+    """
+
+    id: str
+
+
+class HrefMixin(BaseModel):
+    """Href
+
+    This mixin is used to define the href field for all
+    responses from the Gallagher API.
+    """
+
+    href: HttpUrl
+
+class TypeValuePair(BaseModel):
+    """Type Value Pair
+
+    This class is used to represent a type value pair in the
+    Gallagher system. It is used in many places to represent
+    a key value pair.
+
+    eg this appears in CardholderDetail:
+
+    {
+        "type": "active",
+        "value": "Active"
+    }
+
+    """
+
+    type: str
+    value: str
+
+class OptionalHrefMixin(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[HttpUrl] = None
+
+
 class AppBaseModel(BaseModel):
-    """ Pydantic base model for applications
+    """Pydantic base model for applications
 
     This class is used to define the base model for all schema
     that we use in the Application, it configures pydantic to
@@ -36,63 +131,87 @@ class AppBaseModel(BaseModel):
     For a full set of options, see:
     https://pydantic-docs.helpmanual.io/usage/model_config/
     """
+
     model_config = ConfigDict(
         populate_by_name=True,
-        alias_generator=to_lower_camel,
+        alias_generator=_to_lower_camel,
+        from_attributes=True,
+        arbitrary_types_allowed=True,
     )
 
+    @classmethod
+    def _accumulated_annotations(cls) -> dict:
+        """Return a dictionary of all annotations
+
+        This method is used to return a dictionary of all the
+        annotations from itself and it's parent classes.
+
+        It is intended for use by the shillelagh extension
+        """
+        annotations = cls.__annotations__.copy() # TODO: should we  make copies?
+        for base in cls.__bases__:
+            if issubclass(base, BaseModel):
+                # Copy annotations from classes that are pydantic models
+                # they are the only things that form part of the response
+                annotations.update(base.__annotations__)
+        return annotations.items()
+
     # 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
+    _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
+        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()
+        self._good_known_since = datetime.now()
 
+    def __repr__(self) -> str:
+        """Return a string representation of the model
 
-class IdentityMixin(BaseModel):
-    """ Identifier 
+        This method is used to return the string representation of the
+        model, it's used for debugging purposes.
 
-    This mixin is used to define the identifier field for all
-    responses from the Gallagher API.
-    """
-    id: str
+        https://docs.python.org/3/reference/datamodel.html
+        """
+        return f"{self.__class__.__name__}({self.model_dump()})"
 
 
-class HrefMixin(BaseModel):
-    """ Href
+class AppBaseResponseModel(AppBaseModel):
+    """Response Model
+
+    Response classes should subclass this not AppBaseModel, so that
+    the framework can differentiate between Model classes and responses.
+
+    AppBaseResponseWithFollowModel sub classes from this, and adds
+    functionality for following next, updates or going back to the previous
+    set of results.
 
-    This mixin is used to define the href field for all
-    responses from the Gallagher API.
     """
-    href: str
 
+    pass
 
-class OptionalHref(BaseModel):
-    """ Optionally available Href
 
-    This mixin is used to define the href field for all
-    responses from the Gallagher API.
+class AppBaseResponseWithFollowModel(AppBaseResponseModel):
+    """Response with optional Follow links
 
-    Primarily used by the discovery endpoint, where the href
-    may be absent if the feature is not available.
+    Many responses from the Gallagher system return next, previous and update
+    links. This is typical when there are a large number of responses
+    where the absence of a next link denotes that the client has fetched
+    all pending objects.
 
-    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
+    Some endpoints return an `update` URL which can be followed to poll
+    for responses.
 
-    # 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
+
+    next: OptionalHrefMixin = None  # None means it's the end of responses
+    previous: OptionalHrefMixin = None  # None means first set of responses
+    updates: OptionalHrefMixin = None  # None means no updates to watch for
diff --git a/gallagher/enum.py b/gallagher/enum.py
index 0ebf54c3..c9b2e4b3 100644
--- a/gallagher/enum.py
+++ b/gallagher/enum.py
@@ -5,13 +5,12 @@
 CustomerSearch
 
 """
+
 from enum import Enum
 
 
 class CustomerSort(Enum):
-    """ Sort descriptors for the Customer object
-
-    """
+    """Sort descriptors for the Customer object"""
 
     ID: str = "id"
     ID_DESC: str = "-id"
diff --git a/gallagher/exception.py b/gallagher/exception.py
index 84efab4d..bc2052b4 100644
--- a/gallagher/exception.py
+++ b/gallagher/exception.py
@@ -8,11 +8,58 @@
 """
 
 
-class UnlicensedFeatureException(Exception):
-    """ Raised when a feature is not licensed
+class GCCBaseException(Exception):
+    """Base exception for all Gallagher exceptions
+
+    This is the base exception for all Gallagher exceptions.
+    """
+
+
+class UnlicensedFeatureException(GCCBaseException):
+    """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
+
+
+class NotFoundException(GCCBaseException):
+    """Raised if you tried to access an object that either does not
+    exists or you don't have permission to access it.
+    """
+
+
+class ComingSoonException(GCCBaseException):
+    """Raised if Gallagher has marked this feature to be coming soon
+
+    These are items such as mutating a division which is scheduled
+    but has not be released
+    """
+
+
+class DeadEndException(GCCBaseException):
+    """Raised if an API the path is a dead end
+
+    This is raised if the API path is not supported by the client
+    these related to next, previous or updates that certain
+    endpoints support.
+    """
+
+
+class AuthenticationError(GCCBaseException):
+    """Error authentication against the CC API
+
+    This is likely because the use has not provided an authentication
+    key or is not allowed to access this resources with this
+    particular key
+    """
+
+
+class PathFollowNotSupportedError(GCCBaseException):
+    """Raised if the path is not supported by the client
+
+    This is raised if the API path is not supported by the client
+    these related to next, previous or updates that certain
+    endpoints support.
+    """
diff --git a/gallagher/ext/__init__.py b/gallagher/ext/__init__.py
new file mode 100644
index 00000000..9aa447c9
--- /dev/null
+++ b/gallagher/ext/__init__.py
@@ -0,0 +1,14 @@
+""" Extensions package for building upon other frameworks
+
+This package provides extensions around framewokrs like shillelagh
+that allow us to extend functionality like a SQL interface to
+the package.
+
+
+It's recommdend that we list the package that ext provides for 
+developers to understand what is possible and in what context.
+
+- [shillelagh](https://youtu.be/85ztwhfyQ-k?feature=shared&t=14) 
+  how to pronounce
+
+"""
diff --git a/gallagher/ext/shillelagh/__init__.py b/gallagher/ext/shillelagh/__init__.py
new file mode 100644
index 00000000..75eafff6
--- /dev/null
+++ b/gallagher/ext/shillelagh/__init__.py
@@ -0,0 +1,253 @@
+""" Adapter and SQLAlchemy dialect for shillelagh
+
+The package provides extensions to provide a SQL interface for the
+Gallagher API.
+
+[shillelagh](https://github.com/betodealmeida/shillelagh)
+"""
+import os
+import asyncio
+import logging
+import urllib
+from datetime import datetime
+
+logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(message)s')
+# logging.basicConfig(level=logging.ERROR, format='%(asctime)s %(levelname)s %(message)s')
+
+from typing import (
+    Any,
+    Dict,
+    Iterator,
+    AsyncIterator,
+    List, 
+    Optional,
+    Tuple,
+)
+
+from shillelagh.adapters.base import Adapter
+from shillelagh.typing import (
+    RequestedOrder, 
+    Row,
+)
+
+from shillelagh.exceptions import (
+    ImpossibleFilterError,
+    InterfaceError,
+    InternalError,
+    ProgrammingError,
+    UnauthenticatedError,
+)
+
+from shillelagh.filters import (
+    Filter,
+    Impossible,
+    Operator,
+    Range,
+)
+
+from shillelagh.fields import (
+    Field,
+    Float,
+    Integer,
+    String,
+    Order,
+    Boolean,
+    DateTime,
+    Collection,
+    Blob,
+)
+
+# TODO: refactor this to generic based on SQL.md
+from gallagher import cc
+from gallagher.cc.alarms import __shillelagh__ \
+    as alarms_tables
+from gallagher.cc.cardholders import __shillelagh__ \
+    as cardholders_tables
+from gallagher.cc.status_overrides import __shillelagh__ \
+    as status_overrides_tables
+
+class CCAPIAdapter(Adapter):
+
+    # Use this to log messages to assist with shillelagh debugging
+    _logger = logging.getLogger(__name__)
+
+    # Concatenate all the tables into a single list for efficiency
+    # This is declared here because it's accessed by a static method
+    # because shillelagh's supports method is static
+    _all_tables = alarms_tables + cardholders_tables + \
+        status_overrides_tables
+
+    # Maps the python types to the shillelagh fields
+    # initialised here so we don't have to keep redefining it
+    _type_map = {
+        int: Integer,
+        str: String,
+        bool: Boolean,
+        bytes: Blob,
+        list: Collection,
+        datetime: DateTime,
+        float: Float,
+    }
+
+    # API endpoint being used to parse this URL
+    # TODO: see if this changes with every instantiation
+    _api_endpoint = None
+
+    # The adapter doesn't access the filesystem.
+    safe = True
+
+    # The adapter will receive a ``limit`` argument in the ``get_data``
+    # method, and will be responsible for limiting the number of rows returned.
+    supports_limit = True
+    supports_offset = True
+
+    # Check to see if we can do this using the partial column feature
+    supports_requested_columns = False
+
+    @staticmethod
+    def get_endpoint_urls() -> list[str]:
+        # TODO: we need to improve performance by cache this
+        # NOTE: this is a method because we need bootstrap to run first
+        return [
+            f"{table.__config__.endpoint.href}" for table \
+                in CCAPIAdapter._all_tables
+        ]
+
+
+    @staticmethod
+    def bootstrap_api_client():
+        """ Bootstrap the API client for the adapter 
+        
+        This checks for the availability of the GACC_API_KEY environment
+        and sets the variable in the API client
+        
+        Call this before any operations on the API client
+        """
+        if not 'GACC_API_KEY' in os.environ:
+            raise ValueError(
+                "GACC_API_KEY environment variable must be set"
+            )
+
+        api_key = os.environ.get('GACC_API_KEY')
+
+        if not api_key:
+            raise UnauthenticatedError(
+                "GACC_API_KEY environment variable must be set"
+            )
+        
+        cc.api_key = api_key
+
+        for table in CCAPIAdapter._all_tables:
+            # This should get us the url, and if not then we are in an invalid state
+            # Discover should only ever run for the first endpoint and all
+            # others should then deffer to the cached property
+            # 
+            # Running this here saves us from having to run it in other places
+            asyncio.run(table._discover())
+
+
+    @staticmethod
+    def supports(uri: str, fast: bool = True, **kwargs: Any) -> Optional[bool]:
+        """ Return the URL if it is a Gallagher Command Centre API URL
+
+        GACC requires us to respect the HATEOS principle, so we need to
+        run discovery before we can evaluate the URL, this means that we
+        require the API key to be present and initialise the client.
+
+        Because we share the cc object across multiple methods, it available
+        outside the scope of this method
+        """
+        CCAPIAdapter.bootstrap_api_client()
+
+        if not uri in CCAPIAdapter.get_endpoint_urls():
+            CCAPIAdapter._logger.debug(
+                f"{uri} not found in {CCAPIAdapter.get_endpoint_urls()}"
+            )
+            return False
+
+        # Parse the base url using urlparse for comparison        
+        base_parsed_url = urllib.parse.urlparse(cc.api_base)
+
+        # Parse the endpoint using urlparse
+        parsed_url = urllib.parse.urlparse(uri)
+
+        # Match the netloc property to be equal to the api_base
+        # and the scheme to be equal to the base_parsed_url
+        #
+        # Note that do not match the path as it wil be different
+        # and it should have passed the check of being in the endpoint_urls
+        if not parsed_url.netloc == base_parsed_url.netloc or\
+            not parsed_url.scheme == base_parsed_url.scheme:
+            return False
+        
+        return True
+        
+    @staticmethod
+    def parse_uri(uri: str) -> Tuple[str, str]:
+        """ 
+
+        Returns:
+            Tuple[str, str]: The URI and the API key
+        """
+        return (uri, os.environ.get('GACC_API_KEY'))
+
+    def __init__(self, uri: str, api_key: Optional[str], **kwargs: Any):
+
+        super().__init__()
+
+        self.uri = uri
+        # TODO: might be redundant due to moving this up the package level
+        cc.api_key = api_key
+
+        self._logger.debug(f"Finding suitable adapter for {self.uri}")
+
+        self._api_endpoint = next(
+            (table for table in self._all_tables if self.uri == f"{table.__config__.endpoint.href}"),
+            None
+        )
+
+        if self._api_endpoint:
+            self._logger.debug(f"Found helper class = {self._api_endpoint}")
+        else:
+            self._logger.debug("No suitable adapter found.")
+
+    def get_columns(self) -> Dict[str, Field]:
+        """Return a pyndatic DTO as shil compatible attribute config
+
+        Rules here are that we translate as many dictionary vars into
+        a __shillelagh__ compatible format.
+
+        If they are hrefs to other children then we select the id field for
+        each one of those objects
+        """
+
+        if not self._api_endpoint:
+            self._logger.debug("No suitable adapter found while get_columns.")
+            return {}
+
+        return {
+            key: self._type_map[value]() # shillelagh requires an instance
+            for key, value in \
+                self._api_endpoint.__config__.dto_retrieve._accumulated_annotations()
+            if not key.startswith("_") and value in self._type_map
+        }
+
+
+    def get_data(  # pylint: disable=too-many-locals
+        self,
+        bounds: Dict[str, Filter],
+        order: List[Tuple[str, RequestedOrder]],
+        limit: Optional[int] = None,
+        offset: Optional[int] = None,
+        **kwargs: Any,
+    ) -> Iterator[Row]:
+        
+        dto_list = asyncio.run(self._api_endpoint.list())
+
+        for row in dto_list.result_set:
+
+            yield {
+                'rowid': row.id, # Append this for shillelagh
+                **row.dict(), # Rest of the responses 
+            }
+
diff --git a/gallagher/ext/shillelagh/utils.py b/gallagher/ext/shillelagh/utils.py
new file mode 100644
index 00000000..a8fb4b37
--- /dev/null
+++ b/gallagher/ext/shillelagh/utils.py
@@ -0,0 +1,2 @@
+""" Utilities to help Shillelagh CLI commands
+"""
\ No newline at end of file
diff --git a/gallagher/tui/__init__.py b/gallagher/tui/__init__.py
index e650b746..a7878b6d 100644
--- a/gallagher/tui/__init__.py
+++ b/gallagher/tui/__init__.py
@@ -1,9 +1,10 @@
 """ Console
 """
 
-from textual.app import (
-    App,
-    ComposeResult
+from textual.app import App, ComposeResult
+
+from textual.screen import (
+    ModalScreen,
 )
 
 from textual.containers import (
@@ -20,15 +21,22 @@
 
 from .dashboard import Dashboard
 
+class ModalScreen(ModalScreen):
+
+    def compose(self) -> ComposeResult:
+        # Make a two column grid and occupy the second half
+        # with a placeholder
+        with Container():
+            # yield Container()
+            yield Placeholder("This is a modal screen")
 
 class GallagherConsole(App):
-    """ A console interface for the Gallagher Command Centre.
-    """
+    """A console interface for the Gallagher Command Centre."""
 
     CSS_PATH = "gallagher.tcss"
 
     BINDINGS = [
-        ("d", "toggle_dark", "Toggle dark mode"),
+        ("d", "toggle_dark", "Toggle dark mode"), 
         ("ctrl+q", "quit", "Quit")
     ]
 
@@ -39,22 +47,31 @@ class GallagherConsole(App):
     # def on_mount(self) -> None:
 
     def compose(self) -> ComposeResult:
-        """ Create child widgets for the app.
-        """
-        yield Header()
+        """Create child widgets for the app."""
+        yield Header(show_clock=True)
         yield Dashboard()
         yield Footer()
 
     def action_toggle_dark(self) -> None:
-        """ An action to toggle dark mode.
-        """
+        """An action to toggle dark mode."""
         self.dark = not self.dark
 
+    def key_m(self) -> None:
+        """Open the modal screen."""
+        self.push_screen(ModalScreen())
+
+
+# Move this out side of __main__ so that textual cli
+# and python can find them as instance variables
+#
+# see service.tom and you can run this via the terminal
+# textual-web --config serve.toml 
+# to get a web interface, follow textual-web for further
+# development and how we can use it provide a demo
+app = GallagherConsole()
 
 def main():
-    app = GallagherConsole()
     app.run()
 
-
 if __name__ == "__main__":
     main()
diff --git a/gallagher/tui/dashboard.py b/gallagher/tui/dashboard.py
index bde4eab4..795ec463 100644
--- a/gallagher/tui/dashboard.py
+++ b/gallagher/tui/dashboard.py
@@ -4,9 +4,7 @@
 this shows you some statistics, and health information.
 """
 
-from textual.app import (
-    ComposeResult
-)
+from textual.app import ComposeResult
 
 from textual.containers import (
     Container,
@@ -19,11 +17,18 @@
 )
 
 
+class VisitTimelineWidget(Container):
+
+    def compose(self) -> ComposeResult:
+        """Visit timeline widget"""
+        with Grid():
+            yield Placeholder("Visit Timeline, changes")
+
 class Dashboard(Container):
 
+    CSS_PATH = "gallagher.tcss"
+
     def compose(self) -> ComposeResult:
-        """ Create child widgets for the app.
-        """
+        """Dashboard for the command centre"""
         with Grid():
-            yield Placeholder("Dashboard")
-            yield Digits("3.141,592,653,5897", id="pi")
+            yield VisitTimelineWidget()
\ No newline at end of file
diff --git a/gallagher/tui/gallagher.tcss b/gallagher/tui/gallagher.tcss
index 7c5c2f0c..ba3db4d5 100644
--- a/gallagher/tui/gallagher.tcss
+++ b/gallagher/tui/gallagher.tcss
@@ -2,5 +2,7 @@ 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
index 747eea34..cee9d639 100644
--- a/gallagher/tui/overview.py
+++ b/gallagher/tui/overview.py
@@ -2,6 +2,7 @@
 
 
 """
+
 from textual.containers import (
     Container,
 )
diff --git a/poetry.lock b/poetry.lock
index a2552f97..38744903 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,88 +1,88 @@
-# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
 
 [[package]]
 name = "aiohttp"
-version = "3.9.4"
+version = "3.9.5"
 description = "Async http client/server framework (asyncio)"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "aiohttp-3.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:76d32588ef7e4a3f3adff1956a0ba96faabbdee58f2407c122dd45aa6e34f372"},
-    {file = "aiohttp-3.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:56181093c10dbc6ceb8a29dfeea1e815e1dfdc020169203d87fd8d37616f73f9"},
-    {file = "aiohttp-3.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7a5b676d3c65e88b3aca41816bf72831898fcd73f0cbb2680e9d88e819d1e4d"},
-    {file = "aiohttp-3.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1df528a85fb404899d4207a8d9934cfd6be626e30e5d3a5544a83dbae6d8a7e"},
-    {file = "aiohttp-3.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f595db1bceabd71c82e92df212dd9525a8a2c6947d39e3c994c4f27d2fe15b11"},
-    {file = "aiohttp-3.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c0b09d76e5a4caac3d27752027fbd43dc987b95f3748fad2b924a03fe8632ad"},
-    {file = "aiohttp-3.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:689eb4356649ec9535b3686200b231876fb4cab4aca54e3bece71d37f50c1d13"},
-    {file = "aiohttp-3.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3666cf4182efdb44d73602379a66f5fdfd5da0db5e4520f0ac0dcca644a3497"},
-    {file = "aiohttp-3.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b65b0f8747b013570eea2f75726046fa54fa8e0c5db60f3b98dd5d161052004a"},
-    {file = "aiohttp-3.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1885d2470955f70dfdd33a02e1749613c5a9c5ab855f6db38e0b9389453dce7"},
-    {file = "aiohttp-3.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0593822dcdb9483d41f12041ff7c90d4d1033ec0e880bcfaf102919b715f47f1"},
-    {file = "aiohttp-3.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:47f6eb74e1ecb5e19a78f4a4228aa24df7fbab3b62d4a625d3f41194a08bd54f"},
-    {file = "aiohttp-3.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c8b04a3dbd54de6ccb7604242fe3ad67f2f3ca558f2d33fe19d4b08d90701a89"},
-    {file = "aiohttp-3.9.4-cp310-cp310-win32.whl", hash = "sha256:8a78dfb198a328bfb38e4308ca8167028920fb747ddcf086ce706fbdd23b2926"},
-    {file = "aiohttp-3.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:e78da6b55275987cbc89141a1d8e75f5070e577c482dd48bd9123a76a96f0bbb"},
-    {file = "aiohttp-3.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c111b3c69060d2bafc446917534150fd049e7aedd6cbf21ba526a5a97b4402a5"},
-    {file = "aiohttp-3.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:efbdd51872cf170093998c87ccdf3cb5993add3559341a8e5708bcb311934c94"},
-    {file = "aiohttp-3.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7bfdb41dc6e85d8535b00d73947548a748e9534e8e4fddd2638109ff3fb081df"},
-    {file = "aiohttp-3.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bd9d334412961125e9f68d5b73c1d0ab9ea3f74a58a475e6b119f5293eee7ba"},
-    {file = "aiohttp-3.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35d78076736f4a668d57ade00c65d30a8ce28719d8a42471b2a06ccd1a2e3063"},
-    {file = "aiohttp-3.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:824dff4f9f4d0f59d0fa3577932ee9a20e09edec8a2f813e1d6b9f89ced8293f"},
-    {file = "aiohttp-3.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52b8b4e06fc15519019e128abedaeb56412b106ab88b3c452188ca47a25c4093"},
-    {file = "aiohttp-3.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eae569fb1e7559d4f3919965617bb39f9e753967fae55ce13454bec2d1c54f09"},
-    {file = "aiohttp-3.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:69b97aa5792428f321f72aeb2f118e56893371f27e0b7d05750bcad06fc42ca1"},
-    {file = "aiohttp-3.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4d79aad0ad4b980663316f26d9a492e8fab2af77c69c0f33780a56843ad2f89e"},
-    {file = "aiohttp-3.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:d6577140cd7db19e430661e4b2653680194ea8c22c994bc65b7a19d8ec834403"},
-    {file = "aiohttp-3.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:9860d455847cd98eb67897f5957b7cd69fbcb436dd3f06099230f16a66e66f79"},
-    {file = "aiohttp-3.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:69ff36d3f8f5652994e08bd22f093e11cfd0444cea310f92e01b45a4e46b624e"},
-    {file = "aiohttp-3.9.4-cp311-cp311-win32.whl", hash = "sha256:e27d3b5ed2c2013bce66ad67ee57cbf614288bda8cdf426c8d8fe548316f1b5f"},
-    {file = "aiohttp-3.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d6a67e26daa686a6fbdb600a9af8619c80a332556245fa8e86c747d226ab1a1e"},
-    {file = "aiohttp-3.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c5ff8ff44825736a4065d8544b43b43ee4c6dd1530f3a08e6c0578a813b0aa35"},
-    {file = "aiohttp-3.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d12a244627eba4e9dc52cbf924edef905ddd6cafc6513849b4876076a6f38b0e"},
-    {file = "aiohttp-3.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dcad56c8d8348e7e468899d2fb3b309b9bc59d94e6db08710555f7436156097f"},
-    {file = "aiohttp-3.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f7e69a7fd4b5ce419238388e55abd220336bd32212c673ceabc57ccf3d05b55"},
-    {file = "aiohttp-3.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4870cb049f10d7680c239b55428916d84158798eb8f353e74fa2c98980dcc0b"},
-    {file = "aiohttp-3.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2feaf1b7031ede1bc0880cec4b0776fd347259a723d625357bb4b82f62687b"},
-    {file = "aiohttp-3.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:939393e8c3f0a5bcd33ef7ace67680c318dc2ae406f15e381c0054dd658397de"},
-    {file = "aiohttp-3.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d2334e387b2adcc944680bebcf412743f2caf4eeebd550f67249c1c3696be04"},
-    {file = "aiohttp-3.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e0198ea897680e480845ec0ffc5a14e8b694e25b3f104f63676d55bf76a82f1a"},
-    {file = "aiohttp-3.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e40d2cd22914d67c84824045861a5bb0fb46586b15dfe4f046c7495bf08306b2"},
-    {file = "aiohttp-3.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:aba80e77c227f4234aa34a5ff2b6ff30c5d6a827a91d22ff6b999de9175d71bd"},
-    {file = "aiohttp-3.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:fb68dc73bc8ac322d2e392a59a9e396c4f35cb6fdbdd749e139d1d6c985f2527"},
-    {file = "aiohttp-3.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f3460a92638dce7e47062cf088d6e7663adb135e936cb117be88d5e6c48c9d53"},
-    {file = "aiohttp-3.9.4-cp312-cp312-win32.whl", hash = "sha256:32dc814ddbb254f6170bca198fe307920f6c1308a5492f049f7f63554b88ef36"},
-    {file = "aiohttp-3.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:63f41a909d182d2b78fe3abef557fcc14da50c7852f70ae3be60e83ff64edba5"},
-    {file = "aiohttp-3.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c3770365675f6be220032f6609a8fbad994d6dcf3ef7dbcf295c7ee70884c9af"},
-    {file = "aiohttp-3.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:305edae1dea368ce09bcb858cf5a63a064f3bff4767dec6fa60a0cc0e805a1d3"},
-    {file = "aiohttp-3.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6f121900131d116e4a93b55ab0d12ad72573f967b100e49086e496a9b24523ea"},
-    {file = "aiohttp-3.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b71e614c1ae35c3d62a293b19eface83d5e4d194e3eb2fabb10059d33e6e8cbf"},
-    {file = "aiohttp-3.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:419f009fa4cfde4d16a7fc070d64f36d70a8d35a90d71aa27670bba2be4fd039"},
-    {file = "aiohttp-3.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b39476ee69cfe64061fd77a73bf692c40021f8547cda617a3466530ef63f947"},
-    {file = "aiohttp-3.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b33f34c9c7decdb2ab99c74be6443942b730b56d9c5ee48fb7df2c86492f293c"},
-    {file = "aiohttp-3.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c78700130ce2dcebb1a8103202ae795be2fa8c9351d0dd22338fe3dac74847d9"},
-    {file = "aiohttp-3.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:268ba22d917655d1259af2d5659072b7dc11b4e1dc2cb9662fdd867d75afc6a4"},
-    {file = "aiohttp-3.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:17e7c051f53a0d2ebf33013a9cbf020bb4e098c4bc5bce6f7b0c962108d97eab"},
-    {file = "aiohttp-3.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:7be99f4abb008cb38e144f85f515598f4c2c8932bf11b65add0ff59c9c876d99"},
-    {file = "aiohttp-3.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:d58a54d6ff08d2547656356eea8572b224e6f9bbc0cf55fa9966bcaac4ddfb10"},
-    {file = "aiohttp-3.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7673a76772bda15d0d10d1aa881b7911d0580c980dbd16e59d7ba1422b2d83cd"},
-    {file = "aiohttp-3.9.4-cp38-cp38-win32.whl", hash = "sha256:e4370dda04dc8951012f30e1ce7956a0a226ac0714a7b6c389fb2f43f22a250e"},
-    {file = "aiohttp-3.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:eb30c4510a691bb87081192a394fb661860e75ca3896c01c6d186febe7c88530"},
-    {file = "aiohttp-3.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:84e90494db7df3be5e056f91412f9fa9e611fbe8ce4aaef70647297f5943b276"},
-    {file = "aiohttp-3.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7d4845f8501ab28ebfdbeab980a50a273b415cf69e96e4e674d43d86a464df9d"},
-    {file = "aiohttp-3.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:69046cd9a2a17245c4ce3c1f1a4ff8c70c7701ef222fce3d1d8435f09042bba1"},
-    {file = "aiohttp-3.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b73a06bafc8dcc508420db43b4dd5850e41e69de99009d0351c4f3007960019"},
-    {file = "aiohttp-3.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:418bb0038dfafeac923823c2e63226179976c76f981a2aaad0ad5d51f2229bca"},
-    {file = "aiohttp-3.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71a8f241456b6c2668374d5d28398f8e8cdae4cce568aaea54e0f39359cd928d"},
-    {file = "aiohttp-3.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:935c369bf8acc2dc26f6eeb5222768aa7c62917c3554f7215f2ead7386b33748"},
-    {file = "aiohttp-3.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74e4e48c8752d14ecfb36d2ebb3d76d614320570e14de0a3aa7a726ff150a03c"},
-    {file = "aiohttp-3.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:916b0417aeddf2c8c61291238ce25286f391a6acb6f28005dd9ce282bd6311b6"},
-    {file = "aiohttp-3.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9b6787b6d0b3518b2ee4cbeadd24a507756ee703adbac1ab6dc7c4434b8c572a"},
-    {file = "aiohttp-3.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:221204dbda5ef350e8db6287937621cf75e85778b296c9c52260b522231940ed"},
-    {file = "aiohttp-3.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:10afd99b8251022ddf81eaed1d90f5a988e349ee7d779eb429fb07b670751e8c"},
-    {file = "aiohttp-3.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2506d9f7a9b91033201be9ffe7d89c6a54150b0578803cce5cb84a943d075bc3"},
-    {file = "aiohttp-3.9.4-cp39-cp39-win32.whl", hash = "sha256:e571fdd9efd65e86c6af2f332e0e95dad259bfe6beb5d15b3c3eca3a6eb5d87b"},
-    {file = "aiohttp-3.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:7d29dd5319d20aa3b7749719ac9685fbd926f71ac8c77b2477272725f882072d"},
-    {file = "aiohttp-3.9.4.tar.gz", hash = "sha256:6ff71ede6d9a5a58cfb7b6fffc83ab5d4a63138276c771ac91ceaaddf5459644"},
+    {file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fcde4c397f673fdec23e6b05ebf8d4751314fa7c24f93334bf1f1364c1c69ac7"},
+    {file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d6b3f1fabe465e819aed2c421a6743d8debbde79b6a8600739300630a01bf2c"},
+    {file = "aiohttp-3.9.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ae79c1bc12c34082d92bf9422764f799aee4746fd7a392db46b7fd357d4a17a"},
+    {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d3ebb9e1316ec74277d19c5f482f98cc65a73ccd5430540d6d11682cd857430"},
+    {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84dabd95154f43a2ea80deffec9cb44d2e301e38a0c9d331cc4aa0166fe28ae3"},
+    {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a02fbeca6f63cb1f0475c799679057fc9268b77075ab7cf3f1c600e81dd46b"},
+    {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c26959ca7b75ff768e2776d8055bf9582a6267e24556bb7f7bd29e677932be72"},
+    {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:714d4e5231fed4ba2762ed489b4aec07b2b9953cf4ee31e9871caac895a839c0"},
+    {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7a6a8354f1b62e15d48e04350f13e726fa08b62c3d7b8401c0a1314f02e3558"},
+    {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c413016880e03e69d166efb5a1a95d40f83d5a3a648d16486592c49ffb76d0db"},
+    {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ff84aeb864e0fac81f676be9f4685f0527b660f1efdc40dcede3c251ef1e867f"},
+    {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ad7f2919d7dac062f24d6f5fe95d401597fbb015a25771f85e692d043c9d7832"},
+    {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:702e2c7c187c1a498a4e2b03155d52658fdd6fda882d3d7fbb891a5cf108bb10"},
+    {file = "aiohttp-3.9.5-cp310-cp310-win32.whl", hash = "sha256:67c3119f5ddc7261d47163ed86d760ddf0e625cd6246b4ed852e82159617b5fb"},
+    {file = "aiohttp-3.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:471f0ef53ccedec9995287f02caf0c068732f026455f07db3f01a46e49d76bbb"},
+    {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0ae53e33ee7476dd3d1132f932eeb39bf6125083820049d06edcdca4381f342"},
+    {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c088c4d70d21f8ca5c0b8b5403fe84a7bc8e024161febdd4ef04575ef35d474d"},
+    {file = "aiohttp-3.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:639d0042b7670222f33b0028de6b4e2fad6451462ce7df2af8aee37dcac55424"},
+    {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f26383adb94da5e7fb388d441bf09c61e5e35f455a3217bfd790c6b6bc64b2ee"},
+    {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66331d00fb28dc90aa606d9a54304af76b335ae204d1836f65797d6fe27f1ca2"},
+    {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ff550491f5492ab5ed3533e76b8567f4b37bd2995e780a1f46bca2024223233"},
+    {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f22eb3a6c1080d862befa0a89c380b4dafce29dc6cd56083f630073d102eb595"},
+    {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a81b1143d42b66ffc40a441379387076243ef7b51019204fd3ec36b9f69e77d6"},
+    {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f64fd07515dad67f24b6ea4a66ae2876c01031de91c93075b8093f07c0a2d93d"},
+    {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:93e22add827447d2e26d67c9ac0161756007f152fdc5210277d00a85f6c92323"},
+    {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:55b39c8684a46e56ef8c8d24faf02de4a2b2ac60d26cee93bc595651ff545de9"},
+    {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4715a9b778f4293b9f8ae7a0a7cef9829f02ff8d6277a39d7f40565c737d3771"},
+    {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:afc52b8d969eff14e069a710057d15ab9ac17cd4b6753042c407dcea0e40bf75"},
+    {file = "aiohttp-3.9.5-cp311-cp311-win32.whl", hash = "sha256:b3df71da99c98534be076196791adca8819761f0bf6e08e07fd7da25127150d6"},
+    {file = "aiohttp-3.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:88e311d98cc0bf45b62fc46c66753a83445f5ab20038bcc1b8a1cc05666f428a"},
+    {file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c7a4b7a6cf5b6eb11e109a9755fd4fda7d57395f8c575e166d363b9fc3ec4678"},
+    {file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0a158704edf0abcac8ac371fbb54044f3270bdbc93e254a82b6c82be1ef08f3c"},
+    {file = "aiohttp-3.9.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d153f652a687a8e95ad367a86a61e8d53d528b0530ef382ec5aaf533140ed00f"},
+    {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82a6a97d9771cb48ae16979c3a3a9a18b600a8505b1115cfe354dfb2054468b4"},
+    {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60cdbd56f4cad9f69c35eaac0fbbdf1f77b0ff9456cebd4902f3dd1cf096464c"},
+    {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8676e8fd73141ded15ea586de0b7cda1542960a7b9ad89b2b06428e97125d4fa"},
+    {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da00da442a0e31f1c69d26d224e1efd3a1ca5bcbf210978a2ca7426dfcae9f58"},
+    {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18f634d540dd099c262e9f887c8bbacc959847cfe5da7a0e2e1cf3f14dbf2daf"},
+    {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:320e8618eda64e19d11bdb3bd04ccc0a816c17eaecb7e4945d01deee2a22f95f"},
+    {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:2faa61a904b83142747fc6a6d7ad8fccff898c849123030f8e75d5d967fd4a81"},
+    {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:8c64a6dc3fe5db7b1b4d2b5cb84c4f677768bdc340611eca673afb7cf416ef5a"},
+    {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:393c7aba2b55559ef7ab791c94b44f7482a07bf7640d17b341b79081f5e5cd1a"},
+    {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c671dc117c2c21a1ca10c116cfcd6e3e44da7fcde37bf83b2be485ab377b25da"},
+    {file = "aiohttp-3.9.5-cp312-cp312-win32.whl", hash = "sha256:5a7ee16aab26e76add4afc45e8f8206c95d1d75540f1039b84a03c3b3800dd59"},
+    {file = "aiohttp-3.9.5-cp312-cp312-win_amd64.whl", hash = "sha256:5ca51eadbd67045396bc92a4345d1790b7301c14d1848feaac1d6a6c9289e888"},
+    {file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:694d828b5c41255e54bc2dddb51a9f5150b4eefa9886e38b52605a05d96566e8"},
+    {file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0605cc2c0088fcaae79f01c913a38611ad09ba68ff482402d3410bf59039bfb8"},
+    {file = "aiohttp-3.9.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4558e5012ee03d2638c681e156461d37b7a113fe13970d438d95d10173d25f78"},
+    {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbc053ac75ccc63dc3a3cc547b98c7258ec35a215a92bd9f983e0aac95d3d5b"},
+    {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4109adee842b90671f1b689901b948f347325045c15f46b39797ae1bf17019de"},
+    {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6ea1a5b409a85477fd8e5ee6ad8f0e40bf2844c270955e09360418cfd09abac"},
+    {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3c2890ca8c59ee683fd09adf32321a40fe1cf164e3387799efb2acebf090c11"},
+    {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3916c8692dbd9d55c523374a3b8213e628424d19116ac4308e434dbf6d95bbdd"},
+    {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8d1964eb7617907c792ca00b341b5ec3e01ae8c280825deadbbd678447b127e1"},
+    {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5ab8e1f6bee051a4bf6195e38a5c13e5e161cb7bad83d8854524798bd9fcd6e"},
+    {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:52c27110f3862a1afbcb2af4281fc9fdc40327fa286c4625dfee247c3ba90156"},
+    {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7f64cbd44443e80094309875d4f9c71d0401e966d191c3d469cde4642bc2e031"},
+    {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8b4f72fbb66279624bfe83fd5eb6aea0022dad8eec62b71e7bf63ee1caadeafe"},
+    {file = "aiohttp-3.9.5-cp38-cp38-win32.whl", hash = "sha256:6380c039ec52866c06d69b5c7aad5478b24ed11696f0e72f6b807cfb261453da"},
+    {file = "aiohttp-3.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:da22dab31d7180f8c3ac7c7635f3bcd53808f374f6aa333fe0b0b9e14b01f91a"},
+    {file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1732102949ff6087589408d76cd6dea656b93c896b011ecafff418c9661dc4ed"},
+    {file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6021d296318cb6f9414b48e6a439a7f5d1f665464da507e8ff640848ee2a58a"},
+    {file = "aiohttp-3.9.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:239f975589a944eeb1bad26b8b140a59a3a320067fb3cd10b75c3092405a1372"},
+    {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b7b30258348082826d274504fbc7c849959f1989d86c29bc355107accec6cfb"},
+    {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2adf5c87ff6d8b277814a28a535b59e20bfea40a101db6b3bdca7e9926bc24"},
+    {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9a3d838441bebcf5cf442700e3963f58b5c33f015341f9ea86dcd7d503c07e2"},
+    {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e3a1ae66e3d0c17cf65c08968a5ee3180c5a95920ec2731f53343fac9bad106"},
+    {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c69e77370cce2d6df5d12b4e12bdcca60c47ba13d1cbbc8645dd005a20b738b"},
+    {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf56238f4bbf49dab8c2dc2e6b1b68502b1e88d335bea59b3f5b9f4c001475"},
+    {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d1469f228cd9ffddd396d9948b8c9cd8022b6d1bf1e40c6f25b0fb90b4f893ed"},
+    {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:45731330e754f5811c314901cebdf19dd776a44b31927fa4b4dbecab9e457b0c"},
+    {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:3fcb4046d2904378e3aeea1df51f697b0467f2aac55d232c87ba162709478c46"},
+    {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8cf142aa6c1a751fcb364158fd710b8a9be874b81889c2bd13aa8893197455e2"},
+    {file = "aiohttp-3.9.5-cp39-cp39-win32.whl", hash = "sha256:7b179eea70833c8dee51ec42f3b4097bd6370892fa93f510f76762105568cf09"},
+    {file = "aiohttp-3.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:38d80498e2e169bc61418ff36170e0aad0cd268da8b38a17c4cf29d254a8b3f1"},
+    {file = "aiohttp-3.9.5.tar.gz", hash = "sha256:edea7d15772ceeb29db4aff55e482d4bcfb6ae160ce144f2682de02f6d693551"},
 ]
 
 [package.dependencies]
@@ -111,13 +111,13 @@ frozenlist = ">=1.1.0"
 
 [[package]]
 name = "alembic"
-version = "1.13.1"
+version = "1.13.2"
 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"},
+    {file = "alembic-1.13.2-py3-none-any.whl", hash = "sha256:6b8733129a6224a9a711e17c99b08462dbf7cc9670ba8f2e2ae9af860ceb1953"},
+    {file = "alembic-1.13.2.tar.gz", hash = "sha256:1ff0ae32975f4fd96028c39ed9bb3c867fe3af956bd7bb37343b54c9fe7445ef"},
 ]
 
 [package.dependencies]
@@ -130,24 +130,24 @@ tz = ["backports.zoneinfo"]
 
 [[package]]
 name = "annotated-types"
-version = "0.6.0"
+version = "0.7.0"
 description = "Reusable constraint types to use with typing.Annotated"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"},
-    {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"},
+    {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
+    {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
 ]
 
 [[package]]
 name = "anyio"
-version = "4.3.0"
+version = "4.4.0"
 description = "High level compatibility layer for multiple asynchronous event loop implementations"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"},
-    {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"},
+    {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"},
+    {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"},
 ]
 
 [package.dependencies]
@@ -159,15 +159,71 @@ doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphin
 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 = "apsw"
+version = "3.46.0.1"
+description = "Another Python SQLite Wrapper"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "apsw-3.46.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376252c7549f655cb5247a4f35bca32e02201e7b06e168d757be079343ca3e51"},
+    {file = "apsw-3.46.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:77c58fc2bb63cc89dd109aa260eddb0e21c7ee018ae5a63305b67e8c2503879d"},
+    {file = "apsw-3.46.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e765a23679fe41e5466839288afdf70b874af06862d4f893c986a6aa6605caf4"},
+    {file = "apsw-3.46.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d74cf3e9a8d2df9fedc84d88b602e2a4d5ea6e6eb206e1a98d1bbc7120fa2cd2"},
+    {file = "apsw-3.46.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:af24db1431f07f8a52347bc5eb67cd2c8abff3c4ba9119516920bd337ccaaea5"},
+    {file = "apsw-3.46.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c647d8adf270329fda0a47f6b3ddbbd37e11c54e61e7f37238b543fe859692"},
+    {file = "apsw-3.46.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1d2c1e53ed2525c3ed97abfba83a685adc289cc8939230f771db2365c8193b2c"},
+    {file = "apsw-3.46.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b28b8b6f83dfe1721a08913542660c56afefb29c29c2c8a4e6c798e070c776f0"},
+    {file = "apsw-3.46.0.1-cp310-cp310-win32.whl", hash = "sha256:ef67d20a617e9d3b560a4d40278beffac432222df2d2b3e411231a839a6c9b8b"},
+    {file = "apsw-3.46.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:88fd8f33a4fc33cb2dd2fafd195763234525b15b3743a39f40fb26c6594f0d40"},
+    {file = "apsw-3.46.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f6c752b27aef5ae6c3435bbb8d008aaf4b651667ac30f6e27663d48bb9e77248"},
+    {file = "apsw-3.46.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:603ea29d13daf5aabd79886c35128e91ad78d372942c6f60478e2f9327c8f36d"},
+    {file = "apsw-3.46.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d7c15e02765013fcd316e3014ee2684ac5ea41b1edee14b49af33de3f57c429"},
+    {file = "apsw-3.46.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50ae2f38ed5285cc472be2ccff7235e69354e55b0b679e95b53839e61cdc669a"},
+    {file = "apsw-3.46.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:697424946533c8a869e619994895f79e9f7edfbe53fa7985ee43447cde508a7b"},
+    {file = "apsw-3.46.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c123e9c20fcb43720c8e3fbaa8e2d0c3cfbd7b90925d4ff58c6caa660d619a83"},
+    {file = "apsw-3.46.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:96f9c2d3c47ed0a8517422168f02c75061a566a7a49f9751d77be08a49fb78c5"},
+    {file = "apsw-3.46.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dd6b96c795a2feea57db1e2bfb32349dfea12db9e7682d80028c395c78bb6038"},
+    {file = "apsw-3.46.0.1-cp311-cp311-win32.whl", hash = "sha256:894eac8b6eadd892a203d026d557b1e1f4033b2a0d3c5ad36ded58d31957e732"},
+    {file = "apsw-3.46.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:92f5c8ddf11ba4203bacd8fcba3d5d4bc79142a67b405046d7f9db85fe2aba41"},
+    {file = "apsw-3.46.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9c68414281a08a94b8b5c311da59d038bf99217fea5436f9527a0655a4409496"},
+    {file = "apsw-3.46.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50434559a7f2c06ffe48ba8f07d8b330c47674e651b0f32449fa23bef168e516"},
+    {file = "apsw-3.46.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:354e2f3ad37d8ca3afdeac7dcaf5cae9340cf70a819ccf86d6300114b970ff61"},
+    {file = "apsw-3.46.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:347d039d7f6ebe05111516465057007a86aa60ced1122f98f5e2a14bb40febeb"},
+    {file = "apsw-3.46.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd382205968bede44d074742c8e99efdb10138fdab1c584f4fb810b6543bed16"},
+    {file = "apsw-3.46.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6c5c2115e5b45d5ad7c0e6f19a0ee571d256f0d12937182625ce7028ddb73eae"},
+    {file = "apsw-3.46.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:12fe2fca3525431dbb459a4a6aa3bde11d65f47665c76a56b5fced850a7a421e"},
+    {file = "apsw-3.46.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:469366689ec6cbf526260d0aa241090b876e55cbf7615a3c6e79715cb789bfdd"},
+    {file = "apsw-3.46.0.1-cp312-cp312-win32.whl", hash = "sha256:7ae47a7de0ca9eeee462580a1d1b7e30097ebfac4bd8d69f3b049d11095ca72f"},
+    {file = "apsw-3.46.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:347774ee0388ee14cdf54849b2983a9959740457c36a213d3b63e828af3d4c3a"},
+    {file = "apsw-3.46.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:64f82d7529387980c427b209a9e556ca238f1f76f838ab823e739e95a2b23984"},
+    {file = "apsw-3.46.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8a461f767d829db7ddf8297ab0b4aacbc880186f33f110ac15a6541c30843824"},
+    {file = "apsw-3.46.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf62c7f9758a8d61a0d9fcd6ca9a6823319be41700cf0ffda427234d754d00c9"},
+    {file = "apsw-3.46.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92c196ccd45c0d51849e2dee4b7b53010e054f4b4720e786d9e9d5fbd1466505"},
+    {file = "apsw-3.46.0.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:e98a1c48a9376a50377f94edd645f18b7edc5e50714754627ecf51a516ec88b3"},
+    {file = "apsw-3.46.0.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:544e71870b1337fd00420092b6bb51478609d07cd58aca22da9c4f054d949280"},
+    {file = "apsw-3.46.0.1-cp38-cp38-win32.whl", hash = "sha256:6c9a9ae7b0cdf8d9f84853799b75d9f16d5770448300c755e5f71dd3cc3bf605"},
+    {file = "apsw-3.46.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:bec83c29578135d4f9473b05c1ec3ad5c17c15f38dd9efb3e87dbfa0c9372e19"},
+    {file = "apsw-3.46.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6a1d636c4ef230b86d2a26cffb626732fd3fd300f9781396bd24283e1ef393e6"},
+    {file = "apsw-3.46.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8085c0536b93a0fd5f1158568e85a4e3d33dfaea066ac403c449b0bd53fe0470"},
+    {file = "apsw-3.46.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9517fb5e4f7d5c9acfa338e0c5475d7fbf1582a76092fc3c4876e8577a352dd2"},
+    {file = "apsw-3.46.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0538051b1db2a7fdfe3c9184abd2b242c5020ecc62c3ba277f39164e3bfb4ac"},
+    {file = "apsw-3.46.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9b95d67b553ce672a560e60df584a0197f063096c5a54814de2c49d5399f2571"},
+    {file = "apsw-3.46.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d4541eb2c0bd78d833545a25aea00d98338ea3537aa70328cbbaea48d811aa17"},
+    {file = "apsw-3.46.0.1-cp39-cp39-win32.whl", hash = "sha256:259d0555a8077ece645fe2aea745938018e4d816a1eee4e0a661078bfc9c2461"},
+    {file = "apsw-3.46.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:ea0a32cbcfc3a57f7b0fa4000a1b6a813c1080a1fbefd56d0b90f468742fdd02"},
+    {file = "apsw-3.46.0.1.tar.gz", hash = "sha256:a36d1e80180d1dc9c079f766dead8204dfeb1c80707676f9b75c1705800fe7d0"},
+]
+
 [[package]]
 name = "asyncer"
-version = "0.0.3"
+version = "0.0.7"
 description = "Asyncer, async and await, focused on developer experience."
 optional = false
-python-versions = ">=3.8,<4.0"
+python-versions = ">=3.8"
 files = [
-    {file = "asyncer-0.0.3-py3-none-any.whl", hash = "sha256:803398c31c23337285b97aa50e7594ac92fa99672a9e26e596e8dd92ed03fe1e"},
-    {file = "asyncer-0.0.3.tar.gz", hash = "sha256:4734ede2bbe9c45caf8d81573af7a98d1bf866e3b92aa174b3f17b191cc51726"},
+    {file = "asyncer-0.0.7-py3-none-any.whl", hash = "sha256:f0d579d4f67c4ead52ede3a45c854f462cae569058a8a6a68a4ebccac1c335d8"},
+    {file = "asyncer-0.0.7.tar.gz", hash = "sha256:d5e563fb0f56eb87b97257984703658a4f5bbdb52ff851b3e8ed864cc200b1d2"},
 ]
 
 [package.dependencies]
@@ -192,15 +248,195 @@ 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 = "babel"
+version = "2.15.0"
+description = "Internationalization utilities"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb"},
+    {file = "babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413"},
+]
+
+[package.extras]
+dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"]
+
+[[package]]
+name = "black"
+version = "24.4.2"
+description = "The uncompromising code formatter."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"},
+    {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"},
+    {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"},
+    {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"},
+    {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"},
+    {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"},
+    {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"},
+    {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"},
+    {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"},
+    {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"},
+    {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"},
+    {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"},
+    {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"},
+    {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"},
+    {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"},
+    {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"},
+    {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"},
+    {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"},
+    {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"},
+    {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"},
+    {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"},
+    {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"},
+]
+
+[package.dependencies]
+click = ">=8.0.0"
+mypy-extensions = ">=0.4.3"
+packaging = ">=22.0"
+pathspec = ">=0.9.0"
+platformdirs = ">=2"
+
+[package.extras]
+colorama = ["colorama (>=0.4.3)"]
+d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"]
+jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
+uvloop = ["uvloop (>=0.15.2)"]
+
+[[package]]
+name = "cattrs"
+version = "23.2.3"
+description = "Composable complex class support for attrs and dataclasses."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "cattrs-23.2.3-py3-none-any.whl", hash = "sha256:0341994d94971052e9ee70662542699a3162ea1e0c62f7ce1b4a57f563685108"},
+    {file = "cattrs-23.2.3.tar.gz", hash = "sha256:a934090d95abaa9e911dac357e3a8699e0b4b14f8529bcc7d2b1ad9d51672b9f"},
+]
+
+[package.dependencies]
+attrs = ">=23.1.0"
+
+[package.extras]
+bson = ["pymongo (>=4.4.0)"]
+cbor2 = ["cbor2 (>=5.4.6)"]
+msgpack = ["msgpack (>=1.0.5)"]
+orjson = ["orjson (>=3.9.2)"]
+pyyaml = ["pyyaml (>=6.0)"]
+tomlkit = ["tomlkit (>=0.11.8)"]
+ujson = ["ujson (>=5.7.0)"]
+
 [[package]]
 name = "certifi"
-version = "2024.2.2"
+version = "2024.6.2"
 description = "Python package for providing Mozilla's CA Bundle."
 optional = false
 python-versions = ">=3.6"
 files = [
-    {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"},
-    {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"},
+    {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"},
+    {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"},
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.3.2"
+description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
+optional = false
+python-versions = ">=3.7.0"
+files = [
+    {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"},
+    {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
 ]
 
 [[package]]
@@ -230,68 +466,84 @@ files = [
 
 [[package]]
 name = "coverage"
-version = "7.4.3"
+version = "7.5.4"
 description = "Code coverage measurement for Python"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {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"},
+    {file = "coverage-7.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6cfb5a4f556bb51aba274588200a46e4dd6b505fb1a5f8c5ae408222eb416f99"},
+    {file = "coverage-7.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2174e7c23e0a454ffe12267a10732c273243b4f2d50d07544a91198f05c48f47"},
+    {file = "coverage-7.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2214ee920787d85db1b6a0bd9da5f8503ccc8fcd5814d90796c2f2493a2f4d2e"},
+    {file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1137f46adb28e3813dec8c01fefadcb8c614f33576f672962e323b5128d9a68d"},
+    {file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b385d49609f8e9efc885790a5a0e89f2e3ae042cdf12958b6034cc442de428d3"},
+    {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4a474f799456e0eb46d78ab07303286a84a3140e9700b9e154cfebc8f527016"},
+    {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5cd64adedf3be66f8ccee418473c2916492d53cbafbfcff851cbec5a8454b136"},
+    {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e564c2cf45d2f44a9da56f4e3a26b2236504a496eb4cb0ca7221cd4cc7a9aca9"},
+    {file = "coverage-7.5.4-cp310-cp310-win32.whl", hash = "sha256:7076b4b3a5f6d2b5d7f1185fde25b1e54eb66e647a1dfef0e2c2bfaf9b4c88c8"},
+    {file = "coverage-7.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:018a12985185038a5b2bcafab04ab833a9a0f2c59995b3cec07e10074c78635f"},
+    {file = "coverage-7.5.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db14f552ac38f10758ad14dd7b983dbab424e731588d300c7db25b6f89e335b5"},
+    {file = "coverage-7.5.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3257fdd8e574805f27bb5342b77bc65578e98cbc004a92232106344053f319ba"},
+    {file = "coverage-7.5.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a6612c99081d8d6134005b1354191e103ec9705d7ba2754e848211ac8cacc6b"},
+    {file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d45d3cbd94159c468b9b8c5a556e3f6b81a8d1af2a92b77320e887c3e7a5d080"},
+    {file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed550e7442f278af76d9d65af48069f1fb84c9f745ae249c1a183c1e9d1b025c"},
+    {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a892be37ca35eb5019ec85402c3371b0f7cda5ab5056023a7f13da0961e60da"},
+    {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8192794d120167e2a64721d88dbd688584675e86e15d0569599257566dec9bf0"},
+    {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:820bc841faa502e727a48311948e0461132a9c8baa42f6b2b84a29ced24cc078"},
+    {file = "coverage-7.5.4-cp311-cp311-win32.whl", hash = "sha256:6aae5cce399a0f065da65c7bb1e8abd5c7a3043da9dceb429ebe1b289bc07806"},
+    {file = "coverage-7.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:d2e344d6adc8ef81c5a233d3a57b3c7d5181f40e79e05e1c143da143ccb6377d"},
+    {file = "coverage-7.5.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:54317c2b806354cbb2dc7ac27e2b93f97096912cc16b18289c5d4e44fc663233"},
+    {file = "coverage-7.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:042183de01f8b6d531e10c197f7f0315a61e8d805ab29c5f7b51a01d62782747"},
+    {file = "coverage-7.5.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6bb74ed465d5fb204b2ec41d79bcd28afccf817de721e8a807d5141c3426638"},
+    {file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3d45ff86efb129c599a3b287ae2e44c1e281ae0f9a9bad0edc202179bcc3a2e"},
+    {file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5013ed890dc917cef2c9f765c4c6a8ae9df983cd60dbb635df8ed9f4ebc9f555"},
+    {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1014fbf665fef86cdfd6cb5b7371496ce35e4d2a00cda501cf9f5b9e6fced69f"},
+    {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3684bc2ff328f935981847082ba4fdc950d58906a40eafa93510d1b54c08a66c"},
+    {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:581ea96f92bf71a5ec0974001f900db495488434a6928a2ca7f01eee20c23805"},
+    {file = "coverage-7.5.4-cp312-cp312-win32.whl", hash = "sha256:73ca8fbc5bc622e54627314c1a6f1dfdd8db69788f3443e752c215f29fa87a0b"},
+    {file = "coverage-7.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:cef4649ec906ea7ea5e9e796e68b987f83fa9a718514fe147f538cfeda76d7a7"},
+    {file = "coverage-7.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdd31315fc20868c194130de9ee6bfd99755cc9565edff98ecc12585b90be882"},
+    {file = "coverage-7.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:02ff6e898197cc1e9fa375581382b72498eb2e6d5fc0b53f03e496cfee3fac6d"},
+    {file = "coverage-7.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d05c16cf4b4c2fc880cb12ba4c9b526e9e5d5bb1d81313d4d732a5b9fe2b9d53"},
+    {file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5986ee7ea0795a4095ac4d113cbb3448601efca7f158ec7f7087a6c705304e4"},
+    {file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5df54843b88901fdc2f598ac06737f03d71168fd1175728054c8f5a2739ac3e4"},
+    {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ab73b35e8d109bffbda9a3e91c64e29fe26e03e49addf5b43d85fc426dde11f9"},
+    {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:aea072a941b033813f5e4814541fc265a5c12ed9720daef11ca516aeacd3bd7f"},
+    {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:16852febd96acd953b0d55fc842ce2dac1710f26729b31c80b940b9afcd9896f"},
+    {file = "coverage-7.5.4-cp38-cp38-win32.whl", hash = "sha256:8f894208794b164e6bd4bba61fc98bf6b06be4d390cf2daacfa6eca0a6d2bb4f"},
+    {file = "coverage-7.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:e2afe743289273209c992075a5a4913e8d007d569a406ffed0bd080ea02b0633"},
+    {file = "coverage-7.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b95c3a8cb0463ba9f77383d0fa8c9194cf91f64445a63fc26fb2327e1e1eb088"},
+    {file = "coverage-7.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d7564cc09dd91b5a6001754a5b3c6ecc4aba6323baf33a12bd751036c998be4"},
+    {file = "coverage-7.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44da56a2589b684813f86d07597fdf8a9c6ce77f58976727329272f5a01f99f7"},
+    {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e16f3d6b491c48c5ae726308e6ab1e18ee830b4cdd6913f2d7f77354b33f91c8"},
+    {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbc5958cb471e5a5af41b0ddaea96a37e74ed289535e8deca404811f6cb0bc3d"},
+    {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a04e990a2a41740b02d6182b498ee9796cf60eefe40cf859b016650147908029"},
+    {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ddbd2f9713a79e8e7242d7c51f1929611e991d855f414ca9996c20e44a895f7c"},
+    {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b1ccf5e728ccf83acd313c89f07c22d70d6c375a9c6f339233dcf792094bcbf7"},
+    {file = "coverage-7.5.4-cp39-cp39-win32.whl", hash = "sha256:56b4eafa21c6c175b3ede004ca12c653a88b6f922494b023aeb1e836df953ace"},
+    {file = "coverage-7.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:65e528e2e921ba8fd67d9055e6b9f9e34b21ebd6768ae1c1723f4ea6ace1234d"},
+    {file = "coverage-7.5.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:79b356f3dd5b26f3ad23b35c75dbdaf1f9e2450b6bcefc6d0825ea0aa3f86ca5"},
+    {file = "coverage-7.5.4.tar.gz", hash = "sha256:a44963520b069e12789d0faea4e9fdb1e410cdc4aab89d94f7f55cbb7fef0353"},
 ]
 
 [package.extras]
 toml = ["tomli"]
 
+[[package]]
+name = "flake8"
+version = "7.1.0"
+description = "the modular source code checker: pep8 pyflakes and co"
+optional = false
+python-versions = ">=3.8.1"
+files = [
+    {file = "flake8-7.1.0-py2.py3-none-any.whl", hash = "sha256:2e416edcc62471a64cea09353f4e7bdba32aeb079b6e360554c659a122b1bc6a"},
+    {file = "flake8-7.1.0.tar.gz", hash = "sha256:48a07b626b55236e0fb4784ee69a465fbf59d79eec1f5b4785c3d3bc57d17aa5"},
+]
+
+[package.dependencies]
+mccabe = ">=0.7.0,<0.8.0"
+pycodestyle = ">=2.12.0,<2.13.0"
+pyflakes = ">=3.2.0,<3.3.0"
+
 [[package]]
 name = "frozenlist"
 version = "1.4.1"
@@ -378,6 +630,23 @@ files = [
     {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"},
 ]
 
+[[package]]
+name = "ghp-import"
+version = "2.1.0"
+description = "Copy your docs directly to the gh-pages branch."
+optional = false
+python-versions = "*"
+files = [
+    {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"},
+    {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"},
+]
+
+[package.dependencies]
+python-dateutil = ">=2.8.1"
+
+[package.extras]
+dev = ["flake8", "markdown", "twine", "wheel"]
+
 [[package]]
 name = "greenlet"
 version = "3.0.3"
@@ -462,13 +731,13 @@ files = [
 
 [[package]]
 name = "httpcore"
-version = "1.0.2"
+version = "1.0.5"
 description = "A minimal low-level HTTP client."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"},
-    {file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"},
+    {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"},
+    {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"},
 ]
 
 [package.dependencies]
@@ -479,7 +748,7 @@ h11 = ">=0.13,<0.15"
 asyncio = ["anyio (>=4.0,<5.0)"]
 http2 = ["h2 (>=3,<5)"]
 socks = ["socksio (==1.*)"]
-trio = ["trio (>=0.22.0,<0.23.0)"]
+trio = ["trio (>=0.22.0,<0.26.0)"]
 
 [[package]]
 name = "httpx"
@@ -527,15 +796,32 @@ files = [
     {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
 ]
 
+[[package]]
+name = "jinja2"
+version = "3.1.4"
+description = "A very fast and expressive template engine."
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"},
+    {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"},
+]
+
+[package.dependencies]
+MarkupSafe = ">=2.0"
+
+[package.extras]
+i18n = ["Babel (>=2.7)"]
+
 [[package]]
 name = "linkify-it-py"
-version = "2.0.2"
+version = "2.0.3"
 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"},
+    {file = "linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048"},
+    {file = "linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79"},
 ]
 
 [package.dependencies]
@@ -549,13 +835,13 @@ test = ["coverage", "pytest", "pytest-cov"]
 
 [[package]]
 name = "mako"
-version = "1.3.1"
+version = "1.3.5"
 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"},
+    {file = "Mako-1.3.5-py3-none-any.whl", hash = "sha256:260f1dbc3a519453a9c856dedfe4beb4e50bd5a26d96386cb6c80856556bb91a"},
+    {file = "Mako-1.3.5.tar.gz", hash = "sha256:48dbc20568c1d276a2698b36d968fa76161bf127194907ea6fc594fa81f943bc"},
 ]
 
 [package.dependencies]
@@ -566,6 +852,21 @@ babel = ["Babel"]
 lingua = ["lingua"]
 testing = ["pytest"]
 
+[[package]]
+name = "markdown"
+version = "3.6"
+description = "Python implementation of John Gruber's Markdown."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "Markdown-3.6-py3-none-any.whl", hash = "sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f"},
+    {file = "Markdown-3.6.tar.gz", hash = "sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224"},
+]
+
+[package.extras]
+docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"]
+testing = ["coverage", "pyyaml"]
+
 [[package]]
 name = "markdown-it-py"
 version = "3.0.0"
@@ -594,82 +895,93 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
 
 [[package]]
 name = "markupsafe"
-version = "2.1.4"
+version = "2.1.5"
 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"},
+    {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"},
+    {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"},
+    {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"},
+    {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"},
+    {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"},
+    {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"},
+    {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"},
+    {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"},
+    {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"},
+    {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"},
+    {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"},
+    {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"},
+    {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"},
+    {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"},
+    {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"},
+    {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"},
+    {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"},
+    {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"},
+    {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"},
+    {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"},
+    {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"},
+    {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"},
+    {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"},
+    {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"},
+    {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"},
+    {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"},
+    {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"},
+    {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"},
+    {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"},
+    {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"},
+    {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"},
+    {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"},
+    {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"},
+    {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"},
+    {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"},
+    {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"},
+    {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"},
+    {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"},
+    {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"},
+    {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"},
+    {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"},
+    {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"},
+    {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"},
+    {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"},
+    {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"},
+    {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"},
+    {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"},
+    {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"},
+    {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"},
+    {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"},
+    {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"},
+    {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"},
+    {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"},
+    {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"},
+    {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"},
+    {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"},
+    {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"},
+    {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"},
+    {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"},
+    {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"},
+]
+
+[[package]]
+name = "mccabe"
+version = "0.7.0"
+description = "McCabe checker, plugin for flake8"
+optional = false
+python-versions = ">=3.6"
+files = [
+    {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
+    {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
 ]
 
 [[package]]
 name = "mdit-py-plugins"
-version = "0.4.0"
+version = "0.4.1"
 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"},
+    {file = "mdit_py_plugins-0.4.1-py3-none-any.whl", hash = "sha256:1020dfe4e6bfc2c79fb49ae4e3f5b297f5ccd20f010187acc52af2921e27dc6a"},
+    {file = "mdit_py_plugins-0.4.1.tar.gz", hash = "sha256:834b8ac23d1cd60cec703646ffd22ae97b7955a6d596eb1d304be1e251ae499c"},
 ]
 
 [package.dependencies]
@@ -691,152 +1003,291 @@ files = [
     {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
 ]
 
+[[package]]
+name = "mergedeep"
+version = "1.3.4"
+description = "A deep merge function for 🐍."
+optional = false
+python-versions = ">=3.6"
+files = [
+    {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"},
+    {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"},
+]
+
+[[package]]
+name = "mkdocs"
+version = "1.6.0"
+description = "Project documentation with Markdown."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "mkdocs-1.6.0-py3-none-any.whl", hash = "sha256:1eb5cb7676b7d89323e62b56235010216319217d4af5ddc543a91beb8d125ea7"},
+    {file = "mkdocs-1.6.0.tar.gz", hash = "sha256:a73f735824ef83a4f3bcb7a231dcab23f5a838f88b7efc54a0eef5fbdbc3c512"},
+]
+
+[package.dependencies]
+click = ">=7.0"
+colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""}
+ghp-import = ">=1.0"
+jinja2 = ">=2.11.1"
+markdown = ">=3.3.6"
+markupsafe = ">=2.0.1"
+mergedeep = ">=1.3.4"
+mkdocs-get-deps = ">=0.2.0"
+packaging = ">=20.5"
+pathspec = ">=0.11.1"
+pyyaml = ">=5.1"
+pyyaml-env-tag = ">=0.1"
+watchdog = ">=2.0"
+
+[package.extras]
+i18n = ["babel (>=2.9.0)"]
+min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.4)", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"]
+
+[[package]]
+name = "mkdocs-get-deps"
+version = "0.2.0"
+description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"},
+    {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"},
+]
+
+[package.dependencies]
+mergedeep = ">=1.3.4"
+platformdirs = ">=2.2.0"
+pyyaml = ">=5.1"
+
+[[package]]
+name = "mkdocs-material"
+version = "9.5.27"
+description = "Documentation that simply works"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "mkdocs_material-9.5.27-py3-none-any.whl", hash = "sha256:af8cc263fafa98bb79e9e15a8c966204abf15164987569bd1175fd66a7705182"},
+    {file = "mkdocs_material-9.5.27.tar.gz", hash = "sha256:a7d4a35f6d4a62b0c43a0cfe7e987da0980c13587b5bc3c26e690ad494427ec0"},
+]
+
+[package.dependencies]
+babel = ">=2.10,<3.0"
+colorama = ">=0.4,<1.0"
+jinja2 = ">=3.0,<4.0"
+markdown = ">=3.2,<4.0"
+mkdocs = ">=1.6,<2.0"
+mkdocs-material-extensions = ">=1.3,<2.0"
+paginate = ">=0.5,<1.0"
+pygments = ">=2.16,<3.0"
+pymdown-extensions = ">=10.2,<11.0"
+regex = ">=2022.4"
+requests = ">=2.26,<3.0"
+
+[package.extras]
+git = ["mkdocs-git-committers-plugin-2 (>=1.1,<2.0)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"]
+imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=10.2,<11.0)"]
+recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"]
+
+[[package]]
+name = "mkdocs-material-extensions"
+version = "1.3.1"
+description = "Extension pack for Python Markdown and MkDocs Material."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"},
+    {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"},
+]
+
+[[package]]
+name = "mkdocs-typer"
+version = "0.0.3"
+description = "An MkDocs extension to generate documentation for Typer command line applications"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "mkdocs_typer-0.0.3-py3-none-any.whl", hash = "sha256:b2a9a44da590a7100114fde4de9123fedfea692d229379984db20ee3b3f12d7c"},
+    {file = "mkdocs_typer-0.0.3.tar.gz", hash = "sha256:4dd37f024190a82aaf0f6c984faafb15167d34eab7e29a6a85e61362423a4eb7"},
+]
+
+[package.dependencies]
+markdown = "==3.*"
+typer = "==0.*"
+
 [[package]]
 name = "msgpack"
-version = "1.0.7"
+version = "1.0.8"
 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"},
+    {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868"},
+    {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c"},
+    {file = "msgpack-1.0.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659"},
+    {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2"},
+    {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982"},
+    {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa"},
+    {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128"},
+    {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d"},
+    {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653"},
+    {file = "msgpack-1.0.8-cp310-cp310-win32.whl", hash = "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693"},
+    {file = "msgpack-1.0.8-cp310-cp310-win_amd64.whl", hash = "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a"},
+    {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836"},
+    {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad"},
+    {file = "msgpack-1.0.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b"},
+    {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba"},
+    {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85"},
+    {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950"},
+    {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a"},
+    {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b"},
+    {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce"},
+    {file = "msgpack-1.0.8-cp311-cp311-win32.whl", hash = "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305"},
+    {file = "msgpack-1.0.8-cp311-cp311-win_amd64.whl", hash = "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e"},
+    {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee"},
+    {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b"},
+    {file = "msgpack-1.0.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8"},
+    {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3"},
+    {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc"},
+    {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58"},
+    {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f"},
+    {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04"},
+    {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543"},
+    {file = "msgpack-1.0.8-cp312-cp312-win32.whl", hash = "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c"},
+    {file = "msgpack-1.0.8-cp312-cp312-win_amd64.whl", hash = "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd"},
+    {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40"},
+    {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151"},
+    {file = "msgpack-1.0.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24"},
+    {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d"},
+    {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db"},
+    {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77"},
+    {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13"},
+    {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2"},
+    {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a"},
+    {file = "msgpack-1.0.8-cp38-cp38-win32.whl", hash = "sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c"},
+    {file = "msgpack-1.0.8-cp38-cp38-win_amd64.whl", hash = "sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480"},
+    {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a"},
+    {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596"},
+    {file = "msgpack-1.0.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d"},
+    {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f"},
+    {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228"},
+    {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18"},
+    {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8"},
+    {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746"},
+    {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"},
+    {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"},
+    {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"},
+    {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"},
 ]
 
 [[package]]
 name = "multidict"
-version = "6.0.4"
+version = "6.0.5"
 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"},
+    {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"},
+    {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"},
+    {file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"},
+    {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"},
+    {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"},
+    {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"},
+    {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"},
+    {file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"},
+    {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"},
+    {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"},
+    {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"},
+    {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"},
+    {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"},
+    {file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"},
+    {file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"},
+    {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"},
+    {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"},
+    {file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"},
+    {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"},
+    {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"},
+    {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"},
+    {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"},
+    {file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"},
+    {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"},
+    {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"},
+    {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"},
+    {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"},
+    {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"},
+    {file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"},
+    {file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"},
+    {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"},
+    {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"},
+    {file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"},
+    {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"},
+    {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"},
+    {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"},
+    {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"},
+    {file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"},
+    {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"},
+    {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"},
+    {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"},
+    {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"},
+    {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"},
+    {file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"},
+    {file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"},
+    {file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"},
+    {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"},
+    {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"},
+    {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"},
+    {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"},
+    {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"},
+    {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"},
+    {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"},
+    {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"},
+    {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"},
+    {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"},
+    {file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"},
+    {file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"},
+    {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"},
+    {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"},
+    {file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"},
+    {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"},
+    {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"},
+    {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"},
+    {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"},
+    {file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"},
+    {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"},
+    {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"},
+    {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"},
+    {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"},
+    {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"},
+    {file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"},
+    {file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"},
+    {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"},
+    {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"},
+    {file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"},
+    {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"},
+    {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"},
+    {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"},
+    {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"},
+    {file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"},
+    {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"},
+    {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"},
+    {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"},
+    {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"},
+    {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"},
+    {file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"},
+    {file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"},
+    {file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"},
+    {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"},
+]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.0.0"
+description = "Type system extensions for programs checked with the mypy type checker."
+optional = false
+python-versions = ">=3.5"
+files = [
+    {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
+    {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
 ]
 
 [[package]]
@@ -850,15 +1301,138 @@ files = [
     {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
 ]
 
+[[package]]
+name = "paginate"
+version = "0.5.6"
+description = "Divides large result sets into pages for easier browsing"
+optional = false
+python-versions = "*"
+files = [
+    {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"},
+]
+
+[[package]]
+name = "pathspec"
+version = "0.12.1"
+description = "Utility library for gitignore style pattern matching of file paths."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
+    {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
+]
+
+[[package]]
+name = "pillow"
+version = "10.3.0"
+description = "Python Imaging Library (Fork)"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "pillow-10.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45"},
+    {file = "pillow-10.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c"},
+    {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf"},
+    {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599"},
+    {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475"},
+    {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf"},
+    {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3"},
+    {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5"},
+    {file = "pillow-10.3.0-cp310-cp310-win32.whl", hash = "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2"},
+    {file = "pillow-10.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f"},
+    {file = "pillow-10.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b"},
+    {file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"},
+    {file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"},
+    {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"},
+    {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"},
+    {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"},
+    {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"},
+    {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"},
+    {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"},
+    {file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"},
+    {file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"},
+    {file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"},
+    {file = "pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84"},
+    {file = "pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19"},
+    {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338"},
+    {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1"},
+    {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462"},
+    {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"},
+    {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef"},
+    {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3"},
+    {file = "pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d"},
+    {file = "pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b"},
+    {file = "pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a"},
+    {file = "pillow-10.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b"},
+    {file = "pillow-10.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2"},
+    {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa"},
+    {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383"},
+    {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d"},
+    {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd"},
+    {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d"},
+    {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3"},
+    {file = "pillow-10.3.0-cp38-cp38-win32.whl", hash = "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b"},
+    {file = "pillow-10.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999"},
+    {file = "pillow-10.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936"},
+    {file = "pillow-10.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002"},
+    {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60"},
+    {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375"},
+    {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57"},
+    {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8"},
+    {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9"},
+    {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb"},
+    {file = "pillow-10.3.0-cp39-cp39-win32.whl", hash = "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572"},
+    {file = "pillow-10.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb"},
+    {file = "pillow-10.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f"},
+    {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355"},
+    {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9"},
+    {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2"},
+    {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463"},
+    {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced"},
+    {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3"},
+    {file = "pillow-10.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170"},
+    {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32"},
+    {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828"},
+    {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f"},
+    {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015"},
+    {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5"},
+    {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a"},
+    {file = "pillow-10.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591"},
+    {file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"},
+]
+
+[package.extras]
+docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"]
+fpx = ["olefile"]
+mic = ["olefile"]
+tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
+typing = ["typing-extensions"]
+xmp = ["defusedxml"]
+
+[[package]]
+name = "platformdirs"
+version = "4.2.2"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"},
+    {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"},
+]
+
+[package.extras]
+docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"]
+type = ["mypy (>=1.8)"]
+
 [[package]]
 name = "pluggy"
-version = "1.4.0"
+version = "1.5.0"
 description = "plugin and hook calling mechanisms for python"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"},
-    {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"},
+    {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
+    {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
 ]
 
 [package.extras]
@@ -876,20 +1450,31 @@ files = [
     {file = "pprintpp-0.4.0.tar.gz", hash = "sha256:ea826108e2c7f49dc6d66c752973c3fc9749142a798d6b254e1e301cfdbc6403"},
 ]
 
+[[package]]
+name = "pycodestyle"
+version = "2.12.0"
+description = "Python style guide checker"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "pycodestyle-2.12.0-py2.py3-none-any.whl", hash = "sha256:949a39f6b86c3e1515ba1787c2022131d165a8ad271b11370a8819aa070269e4"},
+    {file = "pycodestyle-2.12.0.tar.gz", hash = "sha256:442f950141b4f43df752dd303511ffded3a04c2b6fb7f65980574f0c31e6e79c"},
+]
+
 [[package]]
 name = "pydantic"
-version = "2.6.4"
+version = "2.7.4"
 description = "Data validation using Python type hints"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "pydantic-2.6.4-py3-none-any.whl", hash = "sha256:cc46fce86607580867bdc3361ad462bab9c222ef042d3da86f2fb333e1d916c5"},
-    {file = "pydantic-2.6.4.tar.gz", hash = "sha256:b1704e0847db01817624a6b86766967f552dd9dbf3afba4004409f908dcc84e6"},
+    {file = "pydantic-2.7.4-py3-none-any.whl", hash = "sha256:ee8538d41ccb9c0a9ad3e0e5f07bf15ed8015b481ced539a1759d8cc89ae90d0"},
+    {file = "pydantic-2.7.4.tar.gz", hash = "sha256:0c84efd9548d545f63ac0060c1e4d39bb9b14db8b3c0652338aecc07b5adec52"},
 ]
 
 [package.dependencies]
 annotated-types = ">=0.4.0"
-pydantic-core = "2.16.3"
+pydantic-core = "2.18.4"
 typing-extensions = ">=4.6.1"
 
 [package.extras]
@@ -897,139 +1482,167 @@ email = ["email-validator (>=2.0.0)"]
 
 [[package]]
 name = "pydantic-core"
-version = "2.16.3"
-description = ""
+version = "2.18.4"
+description = "Core functionality for Pydantic validation and serialization"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {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"},
+    {file = "pydantic_core-2.18.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:f76d0ad001edd426b92233d45c746fd08f467d56100fd8f30e9ace4b005266e4"},
+    {file = "pydantic_core-2.18.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:59ff3e89f4eaf14050c8022011862df275b552caef8082e37b542b066ce1ff26"},
+    {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a55b5b16c839df1070bc113c1f7f94a0af4433fcfa1b41799ce7606e5c79ce0a"},
+    {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4d0dcc59664fcb8974b356fe0a18a672d6d7cf9f54746c05f43275fc48636851"},
+    {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8951eee36c57cd128f779e641e21eb40bc5073eb28b2d23f33eb0ef14ffb3f5d"},
+    {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4701b19f7e3a06ea655513f7938de6f108123bf7c86bbebb1196eb9bd35cf724"},
+    {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e00a3f196329e08e43d99b79b286d60ce46bed10f2280d25a1718399457e06be"},
+    {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:97736815b9cc893b2b7f663628e63f436018b75f44854c8027040e05230eeddb"},
+    {file = "pydantic_core-2.18.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6891a2ae0e8692679c07728819b6e2b822fb30ca7445f67bbf6509b25a96332c"},
+    {file = "pydantic_core-2.18.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bc4ff9805858bd54d1a20efff925ccd89c9d2e7cf4986144b30802bf78091c3e"},
+    {file = "pydantic_core-2.18.4-cp310-none-win32.whl", hash = "sha256:1b4de2e51bbcb61fdebd0ab86ef28062704f62c82bbf4addc4e37fa4b00b7cbc"},
+    {file = "pydantic_core-2.18.4-cp310-none-win_amd64.whl", hash = "sha256:6a750aec7bf431517a9fd78cb93c97b9b0c496090fee84a47a0d23668976b4b0"},
+    {file = "pydantic_core-2.18.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:942ba11e7dfb66dc70f9ae66b33452f51ac7bb90676da39a7345e99ffb55402d"},
+    {file = "pydantic_core-2.18.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b2ebef0e0b4454320274f5e83a41844c63438fdc874ea40a8b5b4ecb7693f1c4"},
+    {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a642295cd0c8df1b86fc3dced1d067874c353a188dc8e0f744626d49e9aa51c4"},
+    {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f09baa656c904807e832cf9cce799c6460c450c4ad80803517032da0cd062e2"},
+    {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98906207f29bc2c459ff64fa007afd10a8c8ac080f7e4d5beff4c97086a3dabd"},
+    {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19894b95aacfa98e7cb093cd7881a0c76f55731efad31073db4521e2b6ff5b7d"},
+    {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fbbdc827fe5e42e4d196c746b890b3d72876bdbf160b0eafe9f0334525119c8"},
+    {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f85d05aa0918283cf29a30b547b4df2fbb56b45b135f9e35b6807cb28bc47951"},
+    {file = "pydantic_core-2.18.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e85637bc8fe81ddb73fda9e56bab24560bdddfa98aa64f87aaa4e4b6730c23d2"},
+    {file = "pydantic_core-2.18.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2f5966897e5461f818e136b8451d0551a2e77259eb0f73a837027b47dc95dab9"},
+    {file = "pydantic_core-2.18.4-cp311-none-win32.whl", hash = "sha256:44c7486a4228413c317952e9d89598bcdfb06399735e49e0f8df643e1ccd0558"},
+    {file = "pydantic_core-2.18.4-cp311-none-win_amd64.whl", hash = "sha256:8a7164fe2005d03c64fd3b85649891cd4953a8de53107940bf272500ba8a788b"},
+    {file = "pydantic_core-2.18.4-cp311-none-win_arm64.whl", hash = "sha256:4e99bc050fe65c450344421017f98298a97cefc18c53bb2f7b3531eb39bc7805"},
+    {file = "pydantic_core-2.18.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6f5c4d41b2771c730ea1c34e458e781b18cc668d194958e0112455fff4e402b2"},
+    {file = "pydantic_core-2.18.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2fdf2156aa3d017fddf8aea5adfba9f777db1d6022d392b682d2a8329e087cef"},
+    {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4748321b5078216070b151d5271ef3e7cc905ab170bbfd27d5c83ee3ec436695"},
+    {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847a35c4d58721c5dc3dba599878ebbdfd96784f3fb8bb2c356e123bdcd73f34"},
+    {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c40d4eaad41f78e3bbda31b89edc46a3f3dc6e171bf0ecf097ff7a0ffff7cb1"},
+    {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:21a5e440dbe315ab9825fcd459b8814bb92b27c974cbc23c3e8baa2b76890077"},
+    {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01dd777215e2aa86dfd664daed5957704b769e726626393438f9c87690ce78c3"},
+    {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4b06beb3b3f1479d32befd1f3079cc47b34fa2da62457cdf6c963393340b56e9"},
+    {file = "pydantic_core-2.18.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:564d7922e4b13a16b98772441879fcdcbe82ff50daa622d681dd682175ea918c"},
+    {file = "pydantic_core-2.18.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0eb2a4f660fcd8e2b1c90ad566db2b98d7f3f4717c64fe0a83e0adb39766d5b8"},
+    {file = "pydantic_core-2.18.4-cp312-none-win32.whl", hash = "sha256:8b8bab4c97248095ae0c4455b5a1cd1cdd96e4e4769306ab19dda135ea4cdb07"},
+    {file = "pydantic_core-2.18.4-cp312-none-win_amd64.whl", hash = "sha256:14601cdb733d741b8958224030e2bfe21a4a881fb3dd6fbb21f071cabd48fa0a"},
+    {file = "pydantic_core-2.18.4-cp312-none-win_arm64.whl", hash = "sha256:c1322d7dd74713dcc157a2b7898a564ab091ca6c58302d5c7b4c07296e3fd00f"},
+    {file = "pydantic_core-2.18.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:823be1deb01793da05ecb0484d6c9e20baebb39bd42b5d72636ae9cf8350dbd2"},
+    {file = "pydantic_core-2.18.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ebef0dd9bf9b812bf75bda96743f2a6c5734a02092ae7f721c048d156d5fabae"},
+    {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae1d6df168efb88d7d522664693607b80b4080be6750c913eefb77e34c12c71a"},
+    {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f9899c94762343f2cc2fc64c13e7cae4c3cc65cdfc87dd810a31654c9b7358cc"},
+    {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99457f184ad90235cfe8461c4d70ab7dd2680e28821c29eca00252ba90308c78"},
+    {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18f469a3d2a2fdafe99296a87e8a4c37748b5080a26b806a707f25a902c040a8"},
+    {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7cdf28938ac6b8b49ae5e92f2735056a7ba99c9b110a474473fd71185c1af5d"},
+    {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:938cb21650855054dc54dfd9120a851c974f95450f00683399006aa6e8abb057"},
+    {file = "pydantic_core-2.18.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:44cd83ab6a51da80fb5adbd9560e26018e2ac7826f9626bc06ca3dc074cd198b"},
+    {file = "pydantic_core-2.18.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:972658f4a72d02b8abfa2581d92d59f59897d2e9f7e708fdabe922f9087773af"},
+    {file = "pydantic_core-2.18.4-cp38-none-win32.whl", hash = "sha256:1d886dc848e60cb7666f771e406acae54ab279b9f1e4143babc9c2258213daa2"},
+    {file = "pydantic_core-2.18.4-cp38-none-win_amd64.whl", hash = "sha256:bb4462bd43c2460774914b8525f79b00f8f407c945d50881568f294c1d9b4443"},
+    {file = "pydantic_core-2.18.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:44a688331d4a4e2129140a8118479443bd6f1905231138971372fcde37e43528"},
+    {file = "pydantic_core-2.18.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a2fdd81edd64342c85ac7cf2753ccae0b79bf2dfa063785503cb85a7d3593223"},
+    {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86110d7e1907ab36691f80b33eb2da87d780f4739ae773e5fc83fb272f88825f"},
+    {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46387e38bd641b3ee5ce247563b60c5ca098da9c56c75c157a05eaa0933ed154"},
+    {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:123c3cec203e3f5ac7b000bd82235f1a3eced8665b63d18be751f115588fea30"},
+    {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dc1803ac5c32ec324c5261c7209e8f8ce88e83254c4e1aebdc8b0a39f9ddb443"},
+    {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53db086f9f6ab2b4061958d9c276d1dbe3690e8dd727d6abf2321d6cce37fa94"},
+    {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abc267fa9837245cc28ea6929f19fa335f3dc330a35d2e45509b6566dc18be23"},
+    {file = "pydantic_core-2.18.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a0d829524aaefdebccb869eed855e2d04c21d2d7479b6cada7ace5448416597b"},
+    {file = "pydantic_core-2.18.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:509daade3b8649f80d4e5ff21aa5673e4ebe58590b25fe42fac5f0f52c6f034a"},
+    {file = "pydantic_core-2.18.4-cp39-none-win32.whl", hash = "sha256:ca26a1e73c48cfc54c4a76ff78df3727b9d9f4ccc8dbee4ae3f73306a591676d"},
+    {file = "pydantic_core-2.18.4-cp39-none-win_amd64.whl", hash = "sha256:c67598100338d5d985db1b3d21f3619ef392e185e71b8d52bceacc4a7771ea7e"},
+    {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:574d92eac874f7f4db0ca653514d823a0d22e2354359d0759e3f6a406db5d55d"},
+    {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1f4d26ceb5eb9eed4af91bebeae4b06c3fb28966ca3a8fb765208cf6b51102ab"},
+    {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77450e6d20016ec41f43ca4a6c63e9fdde03f0ae3fe90e7c27bdbeaece8b1ed4"},
+    {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d323a01da91851a4f17bf592faf46149c9169d68430b3146dcba2bb5e5719abc"},
+    {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43d447dd2ae072a0065389092a231283f62d960030ecd27565672bd40746c507"},
+    {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:578e24f761f3b425834f297b9935e1ce2e30f51400964ce4801002435a1b41ef"},
+    {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:81b5efb2f126454586d0f40c4d834010979cb80785173d1586df845a632e4e6d"},
+    {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ab86ce7c8f9bea87b9d12c7f0af71102acbf5ecbc66c17796cff45dae54ef9a5"},
+    {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:90afc12421df2b1b4dcc975f814e21bc1754640d502a2fbcc6d41e77af5ec312"},
+    {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:51991a89639a912c17bef4b45c87bd83593aee0437d8102556af4885811d59f5"},
+    {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:293afe532740370aba8c060882f7d26cfd00c94cae32fd2e212a3a6e3b7bc15e"},
+    {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b48ece5bde2e768197a2d0f6e925f9d7e3e826f0ad2271120f8144a9db18d5c8"},
+    {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eae237477a873ab46e8dd748e515c72c0c804fb380fbe6c85533c7de51f23a8f"},
+    {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:834b5230b5dfc0c1ec37b2fda433b271cbbc0e507560b5d1588e2cc1148cf1ce"},
+    {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e858ac0a25074ba4bce653f9b5d0a85b7456eaddadc0ce82d3878c22489fa4ee"},
+    {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2fd41f6eff4c20778d717af1cc50eca52f5afe7805ee530a4fbd0bae284f16e9"},
+    {file = "pydantic_core-2.18.4.tar.gz", hash = "sha256:ec3beeada09ff865c344ff3bc2f427f5e6c26401cc6113d77e372c3fdac73864"},
 ]
 
 [package.dependencies]
 typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
 
+[[package]]
+name = "pyflakes"
+version = "3.2.0"
+description = "passive checker of Python programs"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"},
+    {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"},
+]
+
 [[package]]
 name = "pygments"
-version = "2.17.2"
+version = "2.18.0"
 description = "Pygments is a syntax highlighting package written in Python."
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"},
-    {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"},
+    {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"},
+    {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"},
 ]
 
 [package.extras]
-plugins = ["importlib-metadata"]
 windows-terminal = ["colorama (>=0.4.6)"]
 
+[[package]]
+name = "pymdown-extensions"
+version = "10.8.1"
+description = "Extension pack for Python Markdown."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "pymdown_extensions-10.8.1-py3-none-any.whl", hash = "sha256:f938326115884f48c6059c67377c46cf631c733ef3629b6eed1349989d1b30cb"},
+    {file = "pymdown_extensions-10.8.1.tar.gz", hash = "sha256:3ab1db5c9e21728dabf75192d71471f8e50f216627e9a1fa9535ecb0231b9940"},
+]
+
+[package.dependencies]
+markdown = ">=3.6"
+pyyaml = "*"
+
+[package.extras]
+extra = ["pygments (>=2.12)"]
+
 [[package]]
 name = "pytest"
-version = "8.1.1"
+version = "8.2.2"
 description = "pytest: simple powerful testing with Python"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"},
-    {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"},
+    {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"},
+    {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"},
 ]
 
 [package.dependencies]
 colorama = {version = "*", markers = "sys_platform == \"win32\""}
 iniconfig = "*"
 packaging = "*"
-pluggy = ">=1.4,<2.0"
+pluggy = ">=1.5,<2.0"
 
 [package.extras]
-testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
+dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
 
 [[package]]
 name = "pytest-asyncio"
-version = "0.23.6"
+version = "0.23.7"
 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"},
+    {file = "pytest_asyncio-0.23.7-py3-none-any.whl", hash = "sha256:009b48127fbe44518a547bddd25611551b0e43ccdbf1e67d12479f569832c20b"},
+    {file = "pytest_asyncio-0.23.7.tar.gz", hash = "sha256:5f5c72948f4c49e7db4f29f2521d4031f1c27f86e57b046126654083d4770268"},
 ]
 
 [package.dependencies]
@@ -1056,13 +1669,13 @@ rich = ">=8.0.0"
 
 [[package]]
 name = "pytest-cov"
-version = "4.1.0"
+version = "5.0.0"
 description = "Pytest plugin for measuring coverage."
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"},
-    {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"},
+    {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"},
+    {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"},
 ]
 
 [package.dependencies]
@@ -1070,17 +1683,31 @@ coverage = {version = ">=5.2.1", extras = ["toml"]}
 pytest = ">=4.6"
 
 [package.extras]
-testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
+testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"]
+
+[[package]]
+name = "pytest-dependency"
+version = "0.6.0"
+description = "Manage dependencies of tests"
+optional = false
+python-versions = ">=3.4"
+files = [
+    {file = "pytest-dependency-0.6.0.tar.gz", hash = "sha256:934b0e6a39d95995062c193f7eaeed8a8ffa06ff1bcef4b62b0dc74a708bacc1"},
+]
+
+[package.dependencies]
+pytest = ">=3.7.0"
+setuptools = "*"
 
 [[package]]
 name = "pytest-order"
-version = "1.2.0"
+version = "1.2.1"
 description = "pytest plugin to run your tests in a specific order"
 optional = false
 python-versions = ">=3.6"
 files = [
-    {file = "pytest-order-1.2.0.tar.gz", hash = "sha256:944f86b6d441aa7b1da80f801c6ab65b84bbeba472d0a7a12eb43ba26650101a"},
-    {file = "pytest_order-1.2.0-py3-none-any.whl", hash = "sha256:9d65c3b6dc6d6ee984d6ae2c6c4aa4f1331e5b915116219075c888c8bcbb93b8"},
+    {file = "pytest-order-1.2.1.tar.gz", hash = "sha256:4451bd8821ba4fa2109455a2fcc882af60ef8e53e09d244d67674be08f56eac3"},
+    {file = "pytest_order-1.2.1-py3-none-any.whl", hash = "sha256:c3082fc73f9ddcf13e4a22dda9bbcc2f39865bf537438a1d50fa241e028dd743"},
 ]
 
 [package.dependencies]
@@ -1101,6 +1728,222 @@ files = [
 pytest = ">=3.0"
 "tap.py" = ">=3.0,<4.0"
 
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+description = "Extensions to the standard Python datetime module"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+files = [
+    {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
+    {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
+]
+
+[package.dependencies]
+six = ">=1.5"
+
+[[package]]
+name = "pyyaml"
+version = "6.0.1"
+description = "YAML parser and emitter for Python"
+optional = false
+python-versions = ">=3.6"
+files = [
+    {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
+    {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
+    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
+    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
+    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
+    {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
+    {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
+    {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
+    {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"},
+    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
+    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
+    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
+    {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
+    {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"},
+    {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"},
+    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
+    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
+    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
+    {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
+    {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
+    {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
+    {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"},
+    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
+    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
+    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
+    {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
+    {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
+    {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
+]
+
+[[package]]
+name = "pyyaml-env-tag"
+version = "0.1"
+description = "A custom YAML tag for referencing environment variables in YAML files. "
+optional = false
+python-versions = ">=3.6"
+files = [
+    {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"},
+    {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"},
+]
+
+[package.dependencies]
+pyyaml = "*"
+
+[[package]]
+name = "regex"
+version = "2024.5.15"
+description = "Alternative regular expression module, to replace re."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "regex-2024.5.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a81e3cfbae20378d75185171587cbf756015ccb14840702944f014e0d93ea09f"},
+    {file = "regex-2024.5.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7b59138b219ffa8979013be7bc85bb60c6f7b7575df3d56dc1e403a438c7a3f6"},
+    {file = "regex-2024.5.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0bd000c6e266927cb7a1bc39d55be95c4b4f65c5be53e659537537e019232b1"},
+    {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5eaa7ddaf517aa095fa8da0b5015c44d03da83f5bd49c87961e3c997daed0de7"},
+    {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba68168daedb2c0bab7fd7e00ced5ba90aebf91024dea3c88ad5063c2a562cca"},
+    {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e8d717bca3a6e2064fc3a08df5cbe366369f4b052dcd21b7416e6d71620dca1"},
+    {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1337b7dbef9b2f71121cdbf1e97e40de33ff114801263b275aafd75303bd62b5"},
+    {file = "regex-2024.5.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9ebd0a36102fcad2f03696e8af4ae682793a5d30b46c647eaf280d6cfb32796"},
+    {file = "regex-2024.5.15-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9efa1a32ad3a3ea112224897cdaeb6aa00381627f567179c0314f7b65d354c62"},
+    {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1595f2d10dff3d805e054ebdc41c124753631b6a471b976963c7b28543cf13b0"},
+    {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b802512f3e1f480f41ab5f2cfc0e2f761f08a1f41092d6718868082fc0d27143"},
+    {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a0981022dccabca811e8171f913de05720590c915b033b7e601f35ce4ea7019f"},
+    {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:19068a6a79cf99a19ccefa44610491e9ca02c2be3305c7760d3831d38a467a6f"},
+    {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1b5269484f6126eee5e687785e83c6b60aad7663dafe842b34691157e5083e53"},
+    {file = "regex-2024.5.15-cp310-cp310-win32.whl", hash = "sha256:ada150c5adfa8fbcbf321c30c751dc67d2f12f15bd183ffe4ec7cde351d945b3"},
+    {file = "regex-2024.5.15-cp310-cp310-win_amd64.whl", hash = "sha256:ac394ff680fc46b97487941f5e6ae49a9f30ea41c6c6804832063f14b2a5a145"},
+    {file = "regex-2024.5.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f5b1dff3ad008dccf18e652283f5e5339d70bf8ba7c98bf848ac33db10f7bc7a"},
+    {file = "regex-2024.5.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c6a2b494a76983df8e3d3feea9b9ffdd558b247e60b92f877f93a1ff43d26656"},
+    {file = "regex-2024.5.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a32b96f15c8ab2e7d27655969a23895eb799de3665fa94349f3b2fbfd547236f"},
+    {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10002e86e6068d9e1c91eae8295ef690f02f913c57db120b58fdd35a6bb1af35"},
+    {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec54d5afa89c19c6dd8541a133be51ee1017a38b412b1321ccb8d6ddbeb4cf7d"},
+    {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:10e4ce0dca9ae7a66e6089bb29355d4432caed736acae36fef0fdd7879f0b0cb"},
+    {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e507ff1e74373c4d3038195fdd2af30d297b4f0950eeda6f515ae3d84a1770f"},
+    {file = "regex-2024.5.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1f059a4d795e646e1c37665b9d06062c62d0e8cc3c511fe01315973a6542e40"},
+    {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0721931ad5fe0dda45d07f9820b90b2148ccdd8e45bb9e9b42a146cb4f695649"},
+    {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:833616ddc75ad595dee848ad984d067f2f31be645d603e4d158bba656bbf516c"},
+    {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:287eb7f54fc81546346207c533ad3c2c51a8d61075127d7f6d79aaf96cdee890"},
+    {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:19dfb1c504781a136a80ecd1fff9f16dddf5bb43cec6871778c8a907a085bb3d"},
+    {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:119af6e56dce35e8dfb5222573b50c89e5508d94d55713c75126b753f834de68"},
+    {file = "regex-2024.5.15-cp311-cp311-win32.whl", hash = "sha256:1c1c174d6ec38d6c8a7504087358ce9213d4332f6293a94fbf5249992ba54efa"},
+    {file = "regex-2024.5.15-cp311-cp311-win_amd64.whl", hash = "sha256:9e717956dcfd656f5055cc70996ee2cc82ac5149517fc8e1b60261b907740201"},
+    {file = "regex-2024.5.15-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:632b01153e5248c134007209b5c6348a544ce96c46005d8456de1d552455b014"},
+    {file = "regex-2024.5.15-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e64198f6b856d48192bf921421fdd8ad8eb35e179086e99e99f711957ffedd6e"},
+    {file = "regex-2024.5.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68811ab14087b2f6e0fc0c2bae9ad689ea3584cad6917fc57be6a48bbd012c49"},
+    {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8ec0c2fea1e886a19c3bee0cd19d862b3aa75dcdfb42ebe8ed30708df64687a"},
+    {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0c0c0003c10f54a591d220997dd27d953cd9ccc1a7294b40a4be5312be8797b"},
+    {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2431b9e263af1953c55abbd3e2efca67ca80a3de8a0437cb58e2421f8184717a"},
+    {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a605586358893b483976cffc1723fb0f83e526e8f14c6e6614e75919d9862cf"},
+    {file = "regex-2024.5.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391d7f7f1e409d192dba8bcd42d3e4cf9e598f3979cdaed6ab11288da88cb9f2"},
+    {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9ff11639a8d98969c863d4617595eb5425fd12f7c5ef6621a4b74b71ed8726d5"},
+    {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4eee78a04e6c67e8391edd4dad3279828dd66ac4b79570ec998e2155d2e59fd5"},
+    {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8fe45aa3f4aa57faabbc9cb46a93363edd6197cbc43523daea044e9ff2fea83e"},
+    {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d0a3d8d6acf0c78a1fff0e210d224b821081330b8524e3e2bc5a68ef6ab5803d"},
+    {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c486b4106066d502495b3025a0a7251bf37ea9540433940a23419461ab9f2a80"},
+    {file = "regex-2024.5.15-cp312-cp312-win32.whl", hash = "sha256:c49e15eac7c149f3670b3e27f1f28a2c1ddeccd3a2812cba953e01be2ab9b5fe"},
+    {file = "regex-2024.5.15-cp312-cp312-win_amd64.whl", hash = "sha256:673b5a6da4557b975c6c90198588181029c60793835ce02f497ea817ff647cb2"},
+    {file = "regex-2024.5.15-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:87e2a9c29e672fc65523fb47a90d429b70ef72b901b4e4b1bd42387caf0d6835"},
+    {file = "regex-2024.5.15-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c3bea0ba8b73b71b37ac833a7f3fd53825924165da6a924aec78c13032f20850"},
+    {file = "regex-2024.5.15-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bfc4f82cabe54f1e7f206fd3d30fda143f84a63fe7d64a81558d6e5f2e5aaba9"},
+    {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5bb9425fe881d578aeca0b2b4b3d314ec88738706f66f219c194d67179337cb"},
+    {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64c65783e96e563103d641760664125e91bd85d8e49566ee560ded4da0d3e704"},
+    {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf2430df4148b08fb4324b848672514b1385ae3807651f3567871f130a728cc3"},
+    {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5397de3219a8b08ae9540c48f602996aa6b0b65d5a61683e233af8605c42b0f2"},
+    {file = "regex-2024.5.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:455705d34b4154a80ead722f4f185b04c4237e8e8e33f265cd0798d0e44825fa"},
+    {file = "regex-2024.5.15-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b2b6f1b3bb6f640c1a92be3bbfbcb18657b125b99ecf141fb3310b5282c7d4ed"},
+    {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3ad070b823ca5890cab606c940522d05d3d22395d432f4aaaf9d5b1653e47ced"},
+    {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5b5467acbfc153847d5adb21e21e29847bcb5870e65c94c9206d20eb4e99a384"},
+    {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e6662686aeb633ad65be2a42b4cb00178b3fbf7b91878f9446075c404ada552f"},
+    {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:2b4c884767504c0e2401babe8b5b7aea9148680d2e157fa28f01529d1f7fcf67"},
+    {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3cd7874d57f13bf70078f1ff02b8b0aa48d5b9ed25fc48547516c6aba36f5741"},
+    {file = "regex-2024.5.15-cp38-cp38-win32.whl", hash = "sha256:e4682f5ba31f475d58884045c1a97a860a007d44938c4c0895f41d64481edbc9"},
+    {file = "regex-2024.5.15-cp38-cp38-win_amd64.whl", hash = "sha256:d99ceffa25ac45d150e30bd9ed14ec6039f2aad0ffa6bb87a5936f5782fc1569"},
+    {file = "regex-2024.5.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:13cdaf31bed30a1e1c2453ef6015aa0983e1366fad2667657dbcac7b02f67133"},
+    {file = "regex-2024.5.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cac27dcaa821ca271855a32188aa61d12decb6fe45ffe3e722401fe61e323cd1"},
+    {file = "regex-2024.5.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7dbe2467273b875ea2de38ded4eba86cbcbc9a1a6d0aa11dcf7bd2e67859c435"},
+    {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f18a9a3513a99c4bef0e3efd4c4a5b11228b48aa80743be822b71e132ae4f5"},
+    {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d347a741ea871c2e278fde6c48f85136c96b8659b632fb57a7d1ce1872547600"},
+    {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1878b8301ed011704aea4c806a3cadbd76f84dece1ec09cc9e4dc934cfa5d4da"},
+    {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4babf07ad476aaf7830d77000874d7611704a7fcf68c9c2ad151f5d94ae4bfc4"},
+    {file = "regex-2024.5.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35cb514e137cb3488bce23352af3e12fb0dbedd1ee6e60da053c69fb1b29cc6c"},
+    {file = "regex-2024.5.15-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cdd09d47c0b2efee9378679f8510ee6955d329424c659ab3c5e3a6edea696294"},
+    {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:72d7a99cd6b8f958e85fc6ca5b37c4303294954eac1376535b03c2a43eb72629"},
+    {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a094801d379ab20c2135529948cb84d417a2169b9bdceda2a36f5f10977ebc16"},
+    {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c0c18345010870e58238790a6779a1219b4d97bd2e77e1140e8ee5d14df071aa"},
+    {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:16093f563098448ff6b1fa68170e4acbef94e6b6a4e25e10eae8598bb1694b5d"},
+    {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e38a7d4e8f633a33b4c7350fbd8bad3b70bf81439ac67ac38916c4a86b465456"},
+    {file = "regex-2024.5.15-cp39-cp39-win32.whl", hash = "sha256:71a455a3c584a88f654b64feccc1e25876066c4f5ef26cd6dd711308aa538694"},
+    {file = "regex-2024.5.15-cp39-cp39-win_amd64.whl", hash = "sha256:cab12877a9bdafde5500206d1020a584355a97884dfd388af3699e9137bf7388"},
+    {file = "regex-2024.5.15.tar.gz", hash = "sha256:d3ee02d9e5f482cc8309134a91eeaacbdd2261ba111b0fef3748eeb4913e6a2c"},
+]
+
+[[package]]
+name = "requests"
+version = "2.32.3"
+description = "Python HTTP for Humans."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
+    {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
+]
+
+[package.dependencies]
+certifi = ">=2017.4.17"
+charset-normalizer = ">=2,<4"
+idna = ">=2.5,<4"
+urllib3 = ">=1.21.1,<3"
+
+[package.extras]
+socks = ["PySocks (>=1.5.6,!=1.5.7)"]
+use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
+
+[[package]]
+name = "requests-cache"
+version = "1.2.1"
+description = "A persistent cache for python requests"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "requests_cache-1.2.1-py3-none-any.whl", hash = "sha256:1285151cddf5331067baa82598afe2d47c7495a1334bfe7a7d329b43e9fd3603"},
+    {file = "requests_cache-1.2.1.tar.gz", hash = "sha256:68abc986fdc5b8d0911318fbb5f7c80eebcd4d01bfacc6685ecf8876052511d1"},
+]
+
+[package.dependencies]
+attrs = ">=21.2"
+cattrs = ">=22.2"
+platformdirs = ">=2.5"
+requests = ">=2.22"
+url-normalize = ">=1.4"
+urllib3 = ">=1.25.5"
+
+[package.extras]
+all = ["boto3 (>=1.15)", "botocore (>=1.18)", "itsdangerous (>=2.0)", "pymongo (>=3)", "pyyaml (>=6.0.1)", "redis (>=3)", "ujson (>=5.4)"]
+bson = ["bson (>=0.5)"]
+docs = ["furo (>=2023.3,<2024.0)", "linkify-it-py (>=2.0,<3.0)", "myst-parser (>=1.0,<2.0)", "sphinx (>=5.0.2,<6.0.0)", "sphinx-autodoc-typehints (>=1.19)", "sphinx-automodapi (>=0.14)", "sphinx-copybutton (>=0.5)", "sphinx-design (>=0.2)", "sphinx-notfound-page (>=0.8)", "sphinxcontrib-apidoc (>=0.3)", "sphinxext-opengraph (>=0.9)"]
+dynamodb = ["boto3 (>=1.15)", "botocore (>=1.18)"]
+json = ["ujson (>=5.4)"]
+mongodb = ["pymongo (>=3)"]
+redis = ["redis (>=3)"]
+security = ["itsdangerous (>=2.0)"]
+yaml = ["pyyaml (>=6.0.1)"]
+
 [[package]]
 name = "rich"
 version = "13.7.1"
@@ -1119,77 +1962,165 @@ pygments = ">=2.13.0,<3.0.0"
 [package.extras]
 jupyter = ["ipywidgets (>=7.5.1,<9)"]
 
+[[package]]
+name = "rich-pixels"
+version = "3.0.1"
+description = "A Rich-compatible library for writing pixel images and ASCII art to the terminal."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "rich_pixels-3.0.1-py3-none-any.whl", hash = "sha256:e82c5aa0d00885609675494f16e1ef814c68fa795634f1d6917cae9159b755e1"},
+    {file = "rich_pixels-3.0.1.tar.gz", hash = "sha256:4a81977d45437ce5009cdcaf70af80256c3bdfab870e87ab802c577ba4133235"},
+]
+
+[package.dependencies]
+pillow = ">=10.0.0"
+rich = ">=12.0.0"
+
+[[package]]
+name = "setuptools"
+version = "70.1.1"
+description = "Easily download, build, install, upgrade, and uninstall Python packages"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "setuptools-70.1.1-py3-none-any.whl", hash = "sha256:a58a8fde0541dab0419750bcc521fbdf8585f6e5cb41909df3a472ef7b81ca95"},
+    {file = "setuptools-70.1.1.tar.gz", hash = "sha256:937a48c7cdb7a21eb53cd7f9b59e525503aa8abaf3584c730dc5f7a5bec3a650"},
+]
+
+[package.extras]
+docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
+testing = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
+
+[[package]]
+name = "shellingham"
+version = "1.5.4"
+description = "Tool to Detect Surrounding Shell"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"},
+    {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"},
+]
+
+[[package]]
+name = "shillelagh"
+version = "1.2.19"
+description = "Making it easy to query APIs via SQL"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "shillelagh-1.2.19-py2.py3-none-any.whl", hash = "sha256:69765ad4f53ab3335c3400e9821ff087cd0fdab860556111c554b2ddae0ac7c6"},
+    {file = "shillelagh-1.2.19.tar.gz", hash = "sha256:3218ddbe9aabf9248eb3171e7a6a677222c475797c59bca6c57b066a506db569"},
+]
+
+[package.dependencies]
+apsw = ">=3.9.2"
+greenlet = ">=2.0.2"
+packaging = "*"
+python-dateutil = ">=2.8.1"
+requests = ">=2.31.0"
+requests-cache = ">=0.7.1"
+sqlalchemy = ">=1.3"
+typing-extensions = ">=3.7.4.3"
+
+[package.extras]
+all = ["PyYAML (>=5.4)", "appdirs (>=1.4.4)", "beautifulsoup4 (>=4.11.1)", "boto3 (>=1.24.28)", "defusedxml (>=0.7.1)", "google-auth (>=1.23.0)", "holidays (>=0.23)", "html5lib (>=1.1)", "pandas (>=1.2.2)", "prison (>=0.2.1)", "prompt-toolkit (>=3)", "psutil (>=5.8.0)", "pygments (>=2.8)", "python-jsonpath (>=0.10.3)", "tabulate (>=0.8.9)", "yarl (>=1.8.1)"]
+console = ["PyYAML (>=5.4)", "appdirs (>=1.4.4)", "prompt-toolkit (>=3)", "pygments (>=2.8)", "tabulate (>=0.8.9)"]
+docs = ["sphinx (>=4.0.1)"]
+genericjsonapi = ["prison (>=0.2.1)", "python-jsonpath (>=0.10.3)", "yarl (>=1.8.1)"]
+genericxmlapi = ["defusedxml (>=0.7.1)", "prison (>=0.2.1)", "yarl (>=1.8.1)"]
+githubapi = ["python-jsonpath (>=0.10.3)"]
+gsheetsapi = ["google-auth (>=1.23.0)", "pyopenssl (>=24.0.0)"]
+holidaysmemory = ["holidays (>=0.23)"]
+htmltableapi = ["beautifulsoup4 (>=4.11.1)", "html5lib (>=1.1)", "pandas (>=1.2.2)"]
+pandasmemory = ["pandas (>=1.2.2)"]
+s3selectapi = ["boto3 (>=1.24.28)"]
+systemapi = ["psutil (>=5.8.0)"]
+testing = ["PyYAML (>=5.4)", "appdirs (>=1.4.4)", "beautifulsoup4 (>=4.11.1)", "boto3 (>=1.24.28)", "codespell (>=2.1.0)", "defusedxml (>=0.7.1)", "dill (>=0.3.6)", "freezegun (>=1.1.0)", "google-auth (>=1.23.0)", "holidays (>=0.23)", "html5lib (>=1.1)", "pandas (>=1.2.2)", "pip-tools (>=6.4.0)", "pre-commit (>=2.13.0)", "prison (>=0.2.1)", "prompt-toolkit (>=3)", "psutil (>=5.8.0)", "pyfakefs (>=4.3.3)", "pygments (>=2.8)", "pylint (>=2.16.2)", "pytest (>=7.2.0)", "pytest-cov (>=2.11.1)", "pytest-integration (==0.2.2)", "pytest-mock (>=3.5.1)", "python-jsonpath (>=0.10.3)", "requests-mock (>=1.8.0)", "tabulate (>=0.8.9)", "yarl (>=1.8.1)"]
+
+[[package]]
+name = "six"
+version = "1.16.0"
+description = "Python 2 and 3 compatibility utilities"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+files = [
+    {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
+    {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
+]
+
 [[package]]
 name = "sniffio"
-version = "1.3.0"
+version = "1.3.1"
 description = "Sniff out which async library your code is running under"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"},
-    {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
+    {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
+    {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
 ]
 
 [[package]]
 name = "sqlalchemy"
-version = "2.0.28"
+version = "2.0.31"
 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"},
+    {file = "SQLAlchemy-2.0.31-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f2a213c1b699d3f5768a7272de720387ae0122f1becf0901ed6eaa1abd1baf6c"},
+    {file = "SQLAlchemy-2.0.31-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9fea3d0884e82d1e33226935dac990b967bef21315cbcc894605db3441347443"},
+    {file = "SQLAlchemy-2.0.31-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ad7f221d8a69d32d197e5968d798217a4feebe30144986af71ada8c548e9fa"},
+    {file = "SQLAlchemy-2.0.31-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2bee229715b6366f86a95d497c347c22ddffa2c7c96143b59a2aa5cc9eebbc"},
+    {file = "SQLAlchemy-2.0.31-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cd5b94d4819c0c89280b7c6109c7b788a576084bf0a480ae17c227b0bc41e109"},
+    {file = "SQLAlchemy-2.0.31-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:750900a471d39a7eeba57580b11983030517a1f512c2cb287d5ad0fcf3aebd58"},
+    {file = "SQLAlchemy-2.0.31-cp310-cp310-win32.whl", hash = "sha256:7bd112be780928c7f493c1a192cd8c5fc2a2a7b52b790bc5a84203fb4381c6be"},
+    {file = "SQLAlchemy-2.0.31-cp310-cp310-win_amd64.whl", hash = "sha256:5a48ac4d359f058474fadc2115f78a5cdac9988d4f99eae44917f36aa1476327"},
+    {file = "SQLAlchemy-2.0.31-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f68470edd70c3ac3b6cd5c2a22a8daf18415203ca1b036aaeb9b0fb6f54e8298"},
+    {file = "SQLAlchemy-2.0.31-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e2c38c2a4c5c634fe6c3c58a789712719fa1bf9b9d6ff5ebfce9a9e5b89c1ca"},
+    {file = "SQLAlchemy-2.0.31-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd15026f77420eb2b324dcb93551ad9c5f22fab2c150c286ef1dc1160f110203"},
+    {file = "SQLAlchemy-2.0.31-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2196208432deebdfe3b22185d46b08f00ac9d7b01284e168c212919891289396"},
+    {file = "SQLAlchemy-2.0.31-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:352b2770097f41bff6029b280c0e03b217c2dcaddc40726f8f53ed58d8a85da4"},
+    {file = "SQLAlchemy-2.0.31-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:56d51ae825d20d604583f82c9527d285e9e6d14f9a5516463d9705dab20c3740"},
+    {file = "SQLAlchemy-2.0.31-cp311-cp311-win32.whl", hash = "sha256:6e2622844551945db81c26a02f27d94145b561f9d4b0c39ce7bfd2fda5776dac"},
+    {file = "SQLAlchemy-2.0.31-cp311-cp311-win_amd64.whl", hash = "sha256:ccaf1b0c90435b6e430f5dd30a5aede4764942a695552eb3a4ab74ed63c5b8d3"},
+    {file = "SQLAlchemy-2.0.31-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3b74570d99126992d4b0f91fb87c586a574a5872651185de8297c6f90055ae42"},
+    {file = "SQLAlchemy-2.0.31-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f77c4f042ad493cb8595e2f503c7a4fe44cd7bd59c7582fd6d78d7e7b8ec52c"},
+    {file = "SQLAlchemy-2.0.31-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd1591329333daf94467e699e11015d9c944f44c94d2091f4ac493ced0119449"},
+    {file = "SQLAlchemy-2.0.31-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74afabeeff415e35525bf7a4ecdab015f00e06456166a2eba7590e49f8db940e"},
+    {file = "SQLAlchemy-2.0.31-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b9c01990d9015df2c6f818aa8f4297d42ee71c9502026bb074e713d496e26b67"},
+    {file = "SQLAlchemy-2.0.31-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:66f63278db425838b3c2b1c596654b31939427016ba030e951b292e32b99553e"},
+    {file = "SQLAlchemy-2.0.31-cp312-cp312-win32.whl", hash = "sha256:0b0f658414ee4e4b8cbcd4a9bb0fd743c5eeb81fc858ca517217a8013d282c96"},
+    {file = "SQLAlchemy-2.0.31-cp312-cp312-win_amd64.whl", hash = "sha256:fa4b1af3e619b5b0b435e333f3967612db06351217c58bfb50cee5f003db2a5a"},
+    {file = "SQLAlchemy-2.0.31-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f43e93057cf52a227eda401251c72b6fbe4756f35fa6bfebb5d73b86881e59b0"},
+    {file = "SQLAlchemy-2.0.31-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d337bf94052856d1b330d5fcad44582a30c532a2463776e1651bd3294ee7e58b"},
+    {file = "SQLAlchemy-2.0.31-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c06fb43a51ccdff3b4006aafee9fcf15f63f23c580675f7734245ceb6b6a9e05"},
+    {file = "SQLAlchemy-2.0.31-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:b6e22630e89f0e8c12332b2b4c282cb01cf4da0d26795b7eae16702a608e7ca1"},
+    {file = "SQLAlchemy-2.0.31-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:79a40771363c5e9f3a77f0e28b3302801db08040928146e6808b5b7a40749c88"},
+    {file = "SQLAlchemy-2.0.31-cp37-cp37m-win32.whl", hash = "sha256:501ff052229cb79dd4c49c402f6cb03b5a40ae4771efc8bb2bfac9f6c3d3508f"},
+    {file = "SQLAlchemy-2.0.31-cp37-cp37m-win_amd64.whl", hash = "sha256:597fec37c382a5442ffd471f66ce12d07d91b281fd474289356b1a0041bdf31d"},
+    {file = "SQLAlchemy-2.0.31-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:dc6d69f8829712a4fd799d2ac8d79bdeff651c2301b081fd5d3fe697bd5b4ab9"},
+    {file = "SQLAlchemy-2.0.31-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:23b9fbb2f5dd9e630db70fbe47d963c7779e9c81830869bd7d137c2dc1ad05fb"},
+    {file = "SQLAlchemy-2.0.31-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a21c97efcbb9f255d5c12a96ae14da873233597dfd00a3a0c4ce5b3e5e79704"},
+    {file = "SQLAlchemy-2.0.31-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26a6a9837589c42b16693cf7bf836f5d42218f44d198f9343dd71d3164ceeeac"},
+    {file = "SQLAlchemy-2.0.31-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc251477eae03c20fae8db9c1c23ea2ebc47331bcd73927cdcaecd02af98d3c3"},
+    {file = "SQLAlchemy-2.0.31-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:2fd17e3bb8058359fa61248c52c7b09a97cf3c820e54207a50af529876451808"},
+    {file = "SQLAlchemy-2.0.31-cp38-cp38-win32.whl", hash = "sha256:c76c81c52e1e08f12f4b6a07af2b96b9b15ea67ccdd40ae17019f1c373faa227"},
+    {file = "SQLAlchemy-2.0.31-cp38-cp38-win_amd64.whl", hash = "sha256:4b600e9a212ed59355813becbcf282cfda5c93678e15c25a0ef896b354423238"},
+    {file = "SQLAlchemy-2.0.31-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b6cf796d9fcc9b37011d3f9936189b3c8074a02a4ed0c0fbbc126772c31a6d4"},
+    {file = "SQLAlchemy-2.0.31-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:78fe11dbe37d92667c2c6e74379f75746dc947ee505555a0197cfba9a6d4f1a4"},
+    {file = "SQLAlchemy-2.0.31-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fc47dc6185a83c8100b37acda27658fe4dbd33b7d5e7324111f6521008ab4fe"},
+    {file = "SQLAlchemy-2.0.31-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a41514c1a779e2aa9a19f67aaadeb5cbddf0b2b508843fcd7bafdf4c6864005"},
+    {file = "SQLAlchemy-2.0.31-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:afb6dde6c11ea4525318e279cd93c8734b795ac8bb5dda0eedd9ebaca7fa23f1"},
+    {file = "SQLAlchemy-2.0.31-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3f9faef422cfbb8fd53716cd14ba95e2ef655400235c3dfad1b5f467ba179c8c"},
+    {file = "SQLAlchemy-2.0.31-cp39-cp39-win32.whl", hash = "sha256:fc6b14e8602f59c6ba893980bea96571dd0ed83d8ebb9c4479d9ed5425d562e9"},
+    {file = "SQLAlchemy-2.0.31-cp39-cp39-win_amd64.whl", hash = "sha256:3cb8a66b167b033ec72c3812ffc8441d4e9f5f78f5e31e54dcd4c90a4ca5bebc"},
+    {file = "SQLAlchemy-2.0.31-py3-none-any.whl", hash = "sha256:69f3e3c08867a8e4856e92d7afb618b95cdee18e0bc1647b77599722c9a28911"},
+    {file = "SQLAlchemy-2.0.31.tar.gz", hash = "sha256:b607489dd4a54de56984a0c7656247504bd5523d9d0ba799aef59d4add009484"},
 ]
 
 [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\""}
+greenlet = {version = "!=0.4.17", markers = "python_version < \"3.13\" and (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]
@@ -1233,13 +2164,13 @@ yaml = ["PyYAML (>=5.1)", "more-itertools"]
 
 [[package]]
 name = "textual"
-version = "0.53.1"
+version = "0.69.0"
 description = "Modern Text User Interface framework"
 optional = false
-python-versions = ">=3.8,<4.0"
+python-versions = "<4.0,>=3.8"
 files = [
-    {file = "textual-0.53.1-py3-none-any.whl", hash = "sha256:32201aa9d334ed064d5e670f15fe3d7f19c736ca54cecb054a5b995691104434"},
-    {file = "textual-0.53.1.tar.gz", hash = "sha256:23ba673be7974819ded35ea88d28df7117987e53d58f15b2cc890ac2ecf56401"},
+    {file = "textual-0.69.0-py3-none-any.whl", hash = "sha256:51d19a5eaeb00e6dabc7b1ef3d8de558dc78e55a3b273c8cf8273b3db574a651"},
+    {file = "textual-0.69.0.tar.gz", hash = "sha256:73736996f468f20efdc8854482db02bf8799586430a39bc0a7c97f91043081b0"},
 ]
 
 [package.dependencies]
@@ -1248,7 +2179,7 @@ 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)"]
+syntax = ["tree-sitter (>=0.20.1,<0.21.0)", "tree-sitter-languages (==1.10.2)"]
 
 [[package]]
 name = "textual-dev"
@@ -1268,52 +2199,143 @@ msgpack = ">=1.0.3"
 textual = ">=0.36.0"
 typing-extensions = ">=4.4.0,<5.0.0"
 
+[[package]]
+name = "tqdm"
+version = "4.66.4"
+description = "Fast, Extensible Progress Meter"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "tqdm-4.66.4-py3-none-any.whl", hash = "sha256:b75ca56b413b030bc3f00af51fd2c1a1a5eac6a0c1cca83cbb37a5c52abce644"},
+    {file = "tqdm-4.66.4.tar.gz", hash = "sha256:e4d936c9de8727928f3be6079590e97d9abfe8d39a590be678eb5919ffc186bb"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[package.extras]
+dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"]
+notebook = ["ipywidgets (>=6)"]
+slack = ["slack-sdk"]
+telegram = ["requests"]
+
 [[package]]
 name = "typer"
-version = "0.9.0"
+version = "0.12.3"
 description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
 files = [
-    {file = "typer-0.9.0-py3-none-any.whl", hash = "sha256:5d96d986a21493606a358cae4461bd8cdf83cbf33a5aa950ae629ca3b51467ee"},
-    {file = "typer-0.9.0.tar.gz", hash = "sha256:50922fd79aea2f4751a8e0408ff10d2662bd0c8bbfa84755a699f3bada2978b2"},
+    {file = "typer-0.12.3-py3-none-any.whl", hash = "sha256:070d7ca53f785acbccba8e7d28b08dcd88f79f1fbda035ade0aecec71ca5c914"},
+    {file = "typer-0.12.3.tar.gz", hash = "sha256:49e73131481d804288ef62598d97a1ceef3058905aa536a1134f90891ba35482"},
 ]
 
 [package.dependencies]
-click = ">=7.1.1,<9.0.0"
+click = ">=8.0.0"
+rich = ">=10.11.0"
+shellingham = ">=1.3.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.10.0"
+version = "4.12.2"
 description = "Backported and Experimental Type Hints for Python 3.8+"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"},
-    {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"},
+    {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
+    {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
 ]
 
 [[package]]
 name = "uc-micro-py"
-version = "1.0.2"
+version = "1.0.3"
 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"},
+    {file = "uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a"},
+    {file = "uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5"},
 ]
 
 [package.extras]
 test = ["coverage", "pytest", "pytest-cov"]
 
+[[package]]
+name = "url-normalize"
+version = "1.4.3"
+description = "URL normalization for Python"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
+files = [
+    {file = "url-normalize-1.4.3.tar.gz", hash = "sha256:d23d3a070ac52a67b83a1c59a0e68f8608d1cd538783b401bc9de2c0fac999b2"},
+    {file = "url_normalize-1.4.3-py2.py3-none-any.whl", hash = "sha256:ec3c301f04e5bb676d333a7fa162fa977ad2ca04b7e652bfc9fac4e405728eed"},
+]
+
+[package.dependencies]
+six = "*"
+
+[[package]]
+name = "urllib3"
+version = "2.2.2"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"},
+    {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"},
+]
+
+[package.extras]
+brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
+h2 = ["h2 (>=4,<5)"]
+socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
+zstd = ["zstandard (>=0.18.0)"]
+
+[[package]]
+name = "watchdog"
+version = "4.0.1"
+description = "Filesystem events monitoring"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:da2dfdaa8006eb6a71051795856bedd97e5b03e57da96f98e375682c48850645"},
+    {file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e93f451f2dfa433d97765ca2634628b789b49ba8b504fdde5837cdcf25fdb53b"},
+    {file = "watchdog-4.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ef0107bbb6a55f5be727cfc2ef945d5676b97bffb8425650dadbb184be9f9a2b"},
+    {file = "watchdog-4.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:17e32f147d8bf9657e0922c0940bcde863b894cd871dbb694beb6704cfbd2fb5"},
+    {file = "watchdog-4.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03e70d2df2258fb6cb0e95bbdbe06c16e608af94a3ffbd2b90c3f1e83eb10767"},
+    {file = "watchdog-4.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:123587af84260c991dc5f62a6e7ef3d1c57dfddc99faacee508c71d287248459"},
+    {file = "watchdog-4.0.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:093b23e6906a8b97051191a4a0c73a77ecc958121d42346274c6af6520dec175"},
+    {file = "watchdog-4.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:611be3904f9843f0529c35a3ff3fd617449463cb4b73b1633950b3d97fa4bfb7"},
+    {file = "watchdog-4.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:62c613ad689ddcb11707f030e722fa929f322ef7e4f18f5335d2b73c61a85c28"},
+    {file = "watchdog-4.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d4925e4bf7b9bddd1c3de13c9b8a2cdb89a468f640e66fbfabaf735bd85b3e35"},
+    {file = "watchdog-4.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cad0bbd66cd59fc474b4a4376bc5ac3fc698723510cbb64091c2a793b18654db"},
+    {file = "watchdog-4.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a3c2c317a8fb53e5b3d25790553796105501a235343f5d2bf23bb8649c2c8709"},
+    {file = "watchdog-4.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c9904904b6564d4ee8a1ed820db76185a3c96e05560c776c79a6ce5ab71888ba"},
+    {file = "watchdog-4.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:667f3c579e813fcbad1b784db7a1aaa96524bed53437e119f6a2f5de4db04235"},
+    {file = "watchdog-4.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d10a681c9a1d5a77e75c48a3b8e1a9f2ae2928eda463e8d33660437705659682"},
+    {file = "watchdog-4.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0144c0ea9997b92615af1d94afc0c217e07ce2c14912c7b1a5731776329fcfc7"},
+    {file = "watchdog-4.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:998d2be6976a0ee3a81fb8e2777900c28641fb5bfbd0c84717d89bca0addcdc5"},
+    {file = "watchdog-4.0.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e7921319fe4430b11278d924ef66d4daa469fafb1da679a2e48c935fa27af193"},
+    {file = "watchdog-4.0.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:f0de0f284248ab40188f23380b03b59126d1479cd59940f2a34f8852db710625"},
+    {file = "watchdog-4.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bca36be5707e81b9e6ce3208d92d95540d4ca244c006b61511753583c81c70dd"},
+    {file = "watchdog-4.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ab998f567ebdf6b1da7dc1e5accfaa7c6992244629c0fdaef062f43249bd8dee"},
+    {file = "watchdog-4.0.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:dddba7ca1c807045323b6af4ff80f5ddc4d654c8bce8317dde1bd96b128ed253"},
+    {file = "watchdog-4.0.1-py3-none-manylinux2014_armv7l.whl", hash = "sha256:4513ec234c68b14d4161440e07f995f231be21a09329051e67a2118a7a612d2d"},
+    {file = "watchdog-4.0.1-py3-none-manylinux2014_i686.whl", hash = "sha256:4107ac5ab936a63952dea2a46a734a23230aa2f6f9db1291bf171dac3ebd53c6"},
+    {file = "watchdog-4.0.1-py3-none-manylinux2014_ppc64.whl", hash = "sha256:6e8c70d2cd745daec2a08734d9f63092b793ad97612470a0ee4cbb8f5f705c57"},
+    {file = "watchdog-4.0.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:f27279d060e2ab24c0aa98363ff906d2386aa6c4dc2f1a374655d4e02a6c5e5e"},
+    {file = "watchdog-4.0.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:f8affdf3c0f0466e69f5b3917cdd042f89c8c63aebdb9f7c078996f607cdb0f5"},
+    {file = "watchdog-4.0.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ac7041b385f04c047fcc2951dc001671dee1b7e0615cde772e84b01fbf68ee84"},
+    {file = "watchdog-4.0.1-py3-none-win32.whl", hash = "sha256:206afc3d964f9a233e6ad34618ec60b9837d0582b500b63687e34011e15bb429"},
+    {file = "watchdog-4.0.1-py3-none-win_amd64.whl", hash = "sha256:7577b3c43e5909623149f76b099ac49a1a01ca4e167d1785c76eb52fa585745a"},
+    {file = "watchdog-4.0.1-py3-none-win_ia64.whl", hash = "sha256:d7b9f5f3299e8dd230880b6c55504a1f69cf1e4316275d1b215ebdd8187ec88d"},
+    {file = "watchdog-4.0.1.tar.gz", hash = "sha256:eebaacf674fa25511e8867028d281e602ee6500045b57f43b08778082f7f8b44"},
+]
+
+[package.extras]
+watchmedo = ["PyYAML (>=3.10)"]
+
 [[package]]
 name = "yarl"
 version = "1.9.4"
@@ -1419,9 +2441,9 @@ multidict = ">=4.0"
 
 [extras]
 cli = []
-console = []
+tui = []
 
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.11"
-content-hash = "b911f6948b103878ca4ef58b2d802a02d305f7e9821fca4807c4991ba3ff83f7"
+content-hash = "fc7f06c32fd05dc7580ff9dfd6c26a9d17d831f2cf6f4310818c3715a8dddcbd"
diff --git a/pyproject.toml b/pyproject.toml
index 534e12a6..30f8bdf6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,13 +1,13 @@
 [tool.poetry]
 name = "gallagher"
-version = "0.1.0-alpha.2"
-description = "Python idiomatic client and tools for Gallagher Command Centre API"
+version = "0.1.0-alpha.3"
+description = "Python tools for Gallagher Security Centre"
 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"]
+keywords = ["gallagher", "rest", "api", "tui", "cli"]
 classifiers = [
     "License :: OSI Approved :: MIT License",
     "Topic :: Software Development :: Build Tools",
@@ -15,6 +15,10 @@ classifiers = [
     "Operating System :: OS Independent",
     "Framework :: Pydantic",
     "Framework :: Pydantic :: 2",
+    "Framework :: Typer",
+    "Framework :: Textual",
+    "Framework :: Rich",
+    "Framework :: Shillelagh",
 ]
 
 [tool.poetry.urls]
@@ -22,14 +26,14 @@ classifiers = [
 
 [tool.poetry.extras]
 cli = ["asyncer", "typer", "rich"]
-console = ["textual"]
+tui = ["textual"]
 
 [tool.poetry.dependencies]
 python = "^3.11"
 httpx = "^0.27.0"
-pydantic = "^2.6.4"
-typing-extensions = "^4.10.0"
-annotated-types = "^0.6.0"
+pydantic = "^2.7.1"
+typing-extensions = "^4.11.0"
+annotated-types = "^0.7.0"
 certifi = "^2024.2.2"
 idna = "^3.7"
 packaging = "^23.2"
@@ -38,30 +42,40 @@ anyio = "^4.3.0"
 aiohttp = "^3.9.4"
 
 [tool.poetry.group.dev.dependencies]
-pytest-cov = "^4.1.0"
-coverage = "^7.4.0"
-pytest = "^8.1.1"
-pytest-order = "^1.2.0"
+pytest-cov = "^5.0.0"
+coverage = "^7.5.1"
+pytest = "^8.2.1"
+pytest-order = "^1.2.1"
 pytest-tap = "^3.4"
 pytest-asyncio = "^0.23.6"
 pytest-clarity = "^1.0.1"
 textual-dev = "^1.4.0"
+mkdocs-typer = "^0.0.3"
+mkdocs = "^1.6.0"
+pytest-dependency = "^0.6.0"
+flake8 = "^7.0.0"
+black = "^24.4.2"
+tqdm = "^4.66.4"
 
 
+mkdocs-material = "^9.5.23"
+pygments = "^2.18.0"
 [tool.poetry.group.cli.dependencies]
-asyncer = "^0.0.3"
-typer = "^0.9.0"
+asyncer = "^0.0.7"
+typer = "^0.12.3"
 rich = "^13.7.1"
 
 
 [tool.poetry.group.tui.dependencies]
-textual = "^0.53.1"
+textual = "^0.69.0"
 textual-dev = "^1.5.1"
 
 
+rich-pixels = "^3.0.1"
 [tool.poetry.group.sync.dependencies]
-sqlalchemy = "^2.0.28"
+sqlalchemy = "^2.0.30"
 alembic = "^1.13.1"
+shillelagh = "^1.2.19"
 
 
 [tool.poetry.group.test.dependencies]
@@ -69,9 +83,13 @@ coverage = "^7.4.3"
 pytest-asyncio = "^0.23.5.post1"
 
 [tool.poetry.scripts]
-gal = "gallagher.cli:app"
-gcon = "gallagher.tui:main"
+gala = "gallagher.cli:app"
+gtui = "gallagher.tui:main"
 
 [build-system]
 requires = ["poetry-core"]
 build-backend = "poetry.core.masonry.api"
+
+
+[tool.poetry.plugins."shillelagh.adapter"]
+gacc = "gallagher.ext.shillelagh:CCAPIAdapter"
diff --git a/serve.toml b/serve.toml
new file mode 100644
index 00000000..39bb8c23
--- /dev/null
+++ b/serve.toml
@@ -0,0 +1,2 @@
+[app.Gallagher]
+command = "textual run gallagher.tui"


Test patch:
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index 602ae405..72ce4b5e 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -1,4 +1,4 @@
-name: Testing
+name: ci
 on:
   push:
     branches:
@@ -15,11 +15,11 @@ jobs:
       - name: Checkout
         uses: actions/checkout@v4
       - name: Setup Python
-        uses: actions/setup-python@v4
+        uses: actions/setup-python@v5
         with:
           python-version-file: pyproject.toml
       - name: Install Task
-        uses: arduino/setup-task@v1
+        uses: arduino/setup-task@v2
         with:
           version: 3.x
           repo-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/tests/__init__.py b/tests/__init__.py
index 243b2176..1bd568f2 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -15,25 +15,28 @@
 
 
 def setup_module(module):
-    """ The Gallagher API client requires a test key, this is 
+    """The Gallagher API client requires a test key, this is
     set in the environment variable GACC_API_KEY.
 
     For obvious security reasons never store or version a key
     in the source.
     """
     import os
+
     api_key = os.environ.get("GACC_API_KEY")
 
     from gallagher import cc
+
     cc.api_key = api_key
 
 
 def teardown_module(module):
-    """ When we are done running the tests return the API client
+    """When we are done running the tests return the API client
     to it's default state.
 
     This ensures that anything that runs past this point is not
     accessing the server
     """
     from gallagher import cc
+
     cc.api_key = None
diff --git a/tests/test_alarm.py b/tests/test_alarm.py
index b304123b..31a11960 100644
--- a/tests/test_alarm.py
+++ b/tests/test_alarm.py
@@ -3,20 +3,93 @@
 
 """
 
+from datetime import datetime
 
-async def test_alarms_list():
-    """ Get a list of item types and iterates through it
+import pytest
+
+from gallagher.dto.detail import (
+    AlarmDetail,
+)
+
+from gallagher.dto.response import (
+    AlarmSummaryResponse,
+)
+
+from gallagher.cc.alarms import (
+    Alarms,
+)
+
+# Fixture for the comment, this will create a timestamp
+# based string so we can compare this is a comment that
+# was created for this test
+
+
+@pytest.fixture
+async def alarm_comment():
+    return f"Test suite alarm comment {datetime.now()}"
+
+
+@pytest.fixture
+async def alarm_summary() -> AlarmSummaryResponse:
+    """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
+    return response
+
+
+async def test_alarms_list(alarm_summary: AlarmSummaryResponse):
+    """Get a list of item types and iterates through it
+    these are a summary response
+
+    """
+    assert type(alarm_summary) is AlarmSummaryResponse
+    assert type(alarm_summary.alarms) is list
+    assert len(alarm_summary.alarms) > 0
+
+
+async def test_alarms_detail(alarm_summary: AlarmSummaryResponse):
+    """Get details of all the alarms
+
+    This will get the details of all the alarms and then
+    compare the summary with the detail to make sure that
+    the data is consistent.
+    """
+
+    for alarm_summary in alarm_summary.alarms:
+        # Get the detail of the alarm for comparison
+        alarm_detail_response = await Alarms.retrieve(alarm_summary.id)
+        assert type(alarm_detail_response) is AlarmDetail
+        assert alarm_detail_response.id == alarm_summary.id
+
+
+async def test_alarms_post_comment(
+    alarm_summary: AlarmSummaryResponse, alarm_comment: str
+):
+    """Posts a commend to an alarm
+
+    This will make a new comment and then expect the next test
+    to figure out that we have a new comment on the first
+    """
+    for alarm_summary in alarm_summary.alarms:
+        # Get the detail of the alarm for comparison
+        await Alarms.comment(
+            alarm_summary,
+            alarm_comment,
+        )
+
+
+async def test_alarms_comment_exists(
+    alarm_summary: AlarmSummaryResponse, alarm_comment: str
+):
+    for alarm_summary in alarm_summary.alarms:
+
+        # Get alarm detail to check the comment
+        alarm_detail_response = await Alarms.retrieve(alarm_summary.id)
+
+        for history in alarm_detail_response.history:
+            # Find the comment in the history
+            if history.comment == alarm_comment and history.action == "comment":
+                assert True
+                return
diff --git a/tests/test_card_types.py b/tests/test_card_types.py
index 0325b5c5..dc901180 100644
--- a/tests/test_card_types.py
+++ b/tests/test_card_types.py
@@ -1,17 +1,31 @@
-"""
+""" Card types tests
 
 """
 
+import pytest
+
+from gallagher.dto.response import CardTypeResponse
+
+from gallagher.cc.cardholders.card_type import CardType
+
+
+@pytest.fixture
+async def card_types() -> CardTypeResponse:
+    """Makes a single call to the card type list
 
-async def test_get_card_types():
-    from gallagher.cc.cardholders.card_type import (
-        CardType
-    )
-    from gallagher.dto.response import (
-        CardTypeResponse
-    )
+    This is passed as a fixture to all other calls around
+    on this test to save network round trips.
+
+    :return: CardTypeResponse
+    """
 
     response = await CardType.list()
-    assert type(response) is CardTypeResponse
-    assert type(response.results) is list
-    assert len(response.results) > 0
+    return response
+
+
+async def test_get_card_types(card_types: CardTypeResponse):
+    """Test if test card type"""
+
+    assert type(card_types) is CardTypeResponse
+    assert type(card_types.results) is list
+    assert len(card_types.results) > 0
diff --git a/tests/test_cardholder.py b/tests/test_cardholder.py
index f515e6ad..78e77b3f 100644
--- a/tests/test_cardholder.py
+++ b/tests/test_cardholder.py
@@ -2,41 +2,57 @@
 
 """
 
+import pytest
 
-async def test_cardholder_list():
+from gallagher.dto.detail import (
+    CardholderDetail,
+)
 
-    from gallagher.cc.cardholders.cardholders import (
-        Cardholder
-    )
-    from gallagher.dto.response import (
-        CardholderSummaryResponse
-    )
+from gallagher.dto.response import CardholderSummaryResponse
+
+from gallagher.cc.cardholders import Cardholder
 
-    response = await Cardholder.list()
-    assert type(response) is CardholderSummaryResponse
-    assert type(response.results) is list
-    assert len(response.results) > 0
 
+@pytest.fixture
+async def cardholder_summary() -> CardholderSummaryResponse:
+    """Makes a single call to the cardholder list
 
-async def test_cardholder_detail():
+    This is passed as a fixture to all other calls around
+    on this test to save network round trips.
 
-    from gallagher.cc.cardholders.cardholders import (
-        Cardholder
-    )
-    from gallagher.dto.detail import (
-        CardholderDetail,
-    )
-    from gallagher.dto.response import (
-        CardholderSummaryResponse,
-    )
+    :return: CardholderSummaryResponse
+    """
 
     response = await Cardholder.list()
-    assert type(response) is CardholderSummaryResponse
+    return response
+
+
+async def test_cardholder_list(cardholder_summary: CardholderSummaryResponse):
+    """Test for the cardholder list"""
+    assert type(cardholder_summary) is CardholderSummaryResponse
+    assert type(cardholder_summary.results) is list
+    assert len(cardholder_summary.results) > 0
+
+
+async def test_cardholder_detail(cardholder_summary: CardholderSummaryResponse):
+    """For each cardholder in the list, get the detail and compare"""
 
-    for cardholder_summary in response.results:
+    for cardholder_summary in cardholder_summary.results:
         # Get the detail of the cardholder for comparison
-        cardholder_detail_response = await Cardholder.retrieve(
-            cardholder_summary.id
-        )
+        cardholder_detail_response = await Cardholder.retrieve(cardholder_summary.id)
         assert type(cardholder_detail_response) is CardholderDetail
-        assert (cardholder_detail_response.id == cardholder_summary.id)
+        assert cardholder_detail_response.id == cardholder_summary.id
+
+        # For each personal_data_definition attempt to access it via 
+        # the wrapper and compare values
+        for pdf in cardholder_detail_response.personal_data_definitions:
+            # see if it's access via the shortcut
+            pdf_attr_name = pdf.name[1:].replace(' ', '_').lower()
+            # first test to see there is an accessible value
+            assert getattr(cardholder_detail_response.pdf, pdf_attr_name) \
+                is not None
+            # then compare the values, this should work because
+            # the model_validator would have assigned this via reference
+            assert getattr(cardholder_detail_response.pdf, pdf_attr_name) \
+                == pdf.contents
+                
diff --git a/tests/test_day_category.py b/tests/test_day_category.py
index 813e625c..d5292d92 100644
--- a/tests/test_day_category.py
+++ b/tests/test_day_category.py
@@ -2,17 +2,30 @@
 
 """
 
+import pytest
 
-async def test_day_category():
+from gallagher.dto.response import DayCategoryResponse
 
-    from gallagher.cc.alarms.day_category import (
-        DayCategory
-    )
-    from gallagher.dto.response import (
-        DayCategoryResponse
-    )
+from gallagher.cc.alarms.day_category import DayCategory
+
+
+@pytest.fixture
+async def day_category() -> DayCategoryResponse:
+    """Makes a single call to the day category list
+
+    This is passed as a fixture to all other calls around
+    on this test to save network round trips.
+
+    :return: DayCategoryResponse
+    """
 
     response = await DayCategory.list()
-    assert type(response) is DayCategoryResponse
-    assert type(response.results) is list
-    assert len(response.results) > 0
+    return response
+
+
+async def test_day_category(day_category: DayCategoryResponse):
+    """Test listing a day category"""
+
+    assert type(day_category) is DayCategoryResponse
+    assert type(day_category.results) is list
+    assert len(day_category.results) > 0
diff --git a/tests/test_discover.py b/tests/test_discover.py
index 29932a01..28c736e5 100644
--- a/tests/test_discover.py
+++ b/tests/test_discover.py
@@ -2,18 +2,46 @@
 
 """
 
+import pytest
+import httpx
 
-async def test_discover():
-    """
+from gallagher import cc
+from gallagher.cc import core
+
+from gallagher.dto.detail import (
+    FeaturesDetail,
+)
+from gallagher.dto.response import (
+    DiscoveryResponse,
+)
+
+
+@pytest.fixture
+async def discover_response() -> DiscoveryResponse:
+    """Makes sure that the API is discoverable as per HATEOAS
+
+    This is not an endpoint like the others, because it is a discovery
+    protocol and must be run before any other items are executed.
 
+    :return: DiscoveryResponse
     """
-    from gallagher.cc.core import APIEndpoint
 
-    from gallagher.dto.detail import (
-        FeaturesDetail,
-    )
-    from gallagher.dto.response import (
-        DiscoveryResponse,
-    )
+    async with httpx.AsyncClient() as _httpx_async:
+        response = await _httpx_async.get(
+            cc.api_base,
+            headers=core._get_authorization_headers(),
+        )
+
+        await _httpx_async.aclose()
+
+        parsed_obj = DiscoveryResponse.model_validate(response.json())
+
+        return parsed_obj
+
 
-    assert 1 == 1
+async def test_discover_response(discover_response: DiscoveryResponse):
+    """Process to see if the discovery method is legit"""
+    assert type(discover_response) is DiscoveryResponse
+    assert type(discover_response.features) is FeaturesDetail
+    assert type(discover_response.version) is str
+    assert len(discover_response.version) > 0
diff --git a/tests/test_divisions.py b/tests/test_divisions.py
index 1aeb9954..8cf6d422 100644
--- a/tests/test_divisions.py
+++ b/tests/test_divisions.py
@@ -1,34 +1,62 @@
-async def test_division_list():
-    from gallagher.cc.alarms.divisions import (
-        Division
-    )
-    from gallagher.dto.response import (
-        DivisionDetailResponse
-    )
+""" Test Division endpoints
 
-    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,
-    )
+"""
+
+import pytest
+
+from gallagher.dto.detail import (
+    DivisionDetail,
+)
+
+from gallagher.dto.response import DivisionSummaryResponse
+
+from gallagher.cc.alarms.divisions import Division
+
+
+@pytest.fixture
+async def division_summary() -> DivisionSummaryResponse:
+    """Makes a single call to the division list
+
+    This is passed as a fixture to all other calls around
+    on this test to save network round trips.
+
+    :return: DivisionSummaryResponse
+    """
 
     response = await Division.list()
-    assert type(response) is DivisionDetailResponse
-    assert type(response.results) is list
+    return response
+
 
-    for division_summary in response.results:
+async def test_division_list(division_summary: DivisionSummaryResponse):
+    """Test listing a division
+
+    This will trigger the list operation in the Division endpoints
+
+    https://gallaghersecurity.github.io/cc-rest-docs/ref/events.html
+    """
+
+    assert type(division_summary) is DivisionSummaryResponse
+    assert type(division_summary.results) is list
+    assert len(division_summary.results) > 0
+
+
+async def test_division_detail(division_summary: DivisionSummaryResponse):
+    """Test getting the details of a division
+
+    This will trigger the list endpoint and then run the detail
+    endpoint for each one of the items in the list and compare the id
+    of the items in the list to the ones in the detail
+
+    https://gallaghersecurity.github.io/cc-rest-docs/ref/events.html
+    """
+
+    for division_summary in division_summary.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
+
+
+async def test_remove_division():
+    """Test removing a division"""
+    pass
diff --git a/tests/test_events.py b/tests/test_events.py
index 3a04cd7f..40d5ceff 100644
--- a/tests/test_events.py
+++ b/tests/test_events.py
@@ -1,26 +1,39 @@
+""" Events
 
+"""
 
-async def test_event_types():
+import pytest
 
-    from gallagher.cc.alarms.events import (
-        EventType
-    )
-    from gallagher.dto.response import (
-        EventTypeResponse,
-    )
+from gallagher.dto.response import (
+    EventTypeResponse,
+    EventSummaryResponse,
+)
 
-    response = await EventType.list()
-    assert type(response) is EventTypeResponse
+from gallagher.cc.alarms.events import (
+    EventType,
+    Event,
+)
 
 
-async def test_event_summary():
+@pytest.fixture
+async def event_types() -> EventTypeResponse:
+    """Event Types list"""
+    response = await EventType.list()
+    return response
 
-    from gallagher.cc.alarms.events import (
-        Event
-    )
-    from gallagher.dto.response import (
-        EventSummaryResponse,
-    )
 
+@pytest.fixture
+async def event_summary() -> EventSummaryResponse:
+    """Event Summary list"""
     response = await Event.list()
-    assert type(response) is EventSummaryResponse
+    return response
+
+
+async def test_event_types(event_types: EventTypeResponse):
+    """Event Type listings"""
+    assert type(event_types) is EventTypeResponse
+
+
+async def test_event_summary(event_summary: EventSummaryResponse):
+    """Event Summary listing"""
+    assert type(event_summary) is EventSummaryResponse
diff --git a/tests/test_items.py b/tests/test_items.py
index 2003931e..f826dca2 100644
--- a/tests/test_items.py
+++ b/tests/test_items.py
@@ -3,62 +3,89 @@
 
 """
 
+import pytest
 
-async def test_items_types_list():
-    """ Get a list of item types and iterates through it
+from gallagher.dto.summary import (
+    ItemSummary,
+)
+
+from gallagher.dto.response import (
+    ItemTypesResponse,
+    ItemsSummaryResponse,
+)
+
+from gallagher.cc.alarms.items import Item
+
+
+@pytest.fixture
+async def items_types() -> ItemTypesResponse:
+    """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.response import (
-        ItemTypesResponse
-    )
+    from gallagher.cc.alarms.items import ItemsTypes
 
     response = await ItemsTypes.list()
-    assert type(response) is ItemTypesResponse
-    assert type(response.item_types) is list
-    assert len(response.item_types) > 0
+    return response
 
 
-async def test_items_list():
-    """ Get a list of items and this should feed into fetching
+@pytest.fixture
+async def items_summary() -> ItemsSummaryResponse:
+    """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.response import (
-        ItemsSummaryResponse
-    )
-
     response = await Item.list()
-    assert type(response) is ItemsSummaryResponse
-    assert type(response.results) is list
-    assert len(response.results) > 0
+    return response
+
 
+async def test_items_types_list(items_types: ItemTypesResponse):
+    """Get a list of item types and iterates through it
+    these are a summary response
+
+    """
 
-async def test_item_detail():
-    """ Get each item in the list and make sure it's a valid item
+    assert type(items_types) is ItemTypesResponse
+    assert type(items_types.item_types) is list
+    assert len(items_types.item_types) > 0
+
+
+async def test_items_list(items_summary: ItemsSummaryResponse):
+    """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.summary import (
-        ItemSummary,
-    )
-    from gallagher.dto.response import (
-        ItemsSummaryResponse,
-    )
-
-    response: ItemsSummaryResponse = await Item.list()
-    assert type(response) is ItemsSummaryResponse
-
-    for item_summary in response.results:
+
+    assert type(items_summary) is ItemsSummaryResponse
+    assert type(items_summary.results) is list
+    assert len(items_summary.results) > 0
+
+
+async def test_items_follow_next(items_summary: ItemsSummaryResponse):
+    """Get a list of items and this should feed into fetching
+    each one of these on it's own.
+
+    """
+
+    assert type(items_summary) is ItemsSummaryResponse
+    assert type(items_summary.results) is list
+    assert len(items_summary.results) > 0
+
+    follow_item_summary = items_summary
+
+    while follow_item_summary.next:
+        # Get the next page if there is one
+        # assign it to the summary and keep it going
+        follow_item_summary = await Item.next(follow_item_summary)
+        assert type(follow_item_summary) is ItemsSummaryResponse
+        assert type(follow_item_summary.results) is list
+        assert len(follow_item_summary.results) > 0
+
+
+async def test_item_detail(items_summary: ItemsSummaryResponse):
+    """Get each item in the list and make sure it's a valid item"""
+    for item_summary in items_summary.results:
         # Get the detail of the item
         item_detail_response = await Item.retrieve(item_summary.id)
         assert type(item_detail_response) is ItemSummary
-        assert (item_detail_response.id == item_summary.id)
+        assert item_detail_response.id == item_summary.id
diff --git a/tests/test_pdf.py b/tests/test_pdf.py
index f9bf0b31..fa96faed 100644
--- a/tests/test_pdf.py
+++ b/tests/test_pdf.py
@@ -1,13 +1,43 @@
-async def test_pdf_list():
+""" Personal Data Fields tests
 
-    from gallagher.cc.cardholders.cardholders import (
-        PdfDefinition
-    )
-    from gallagher.dto.response import (
-        PdfResponse
-    )
+Personal Data Fields are dynamic fields that can be added on a cardholder.
+These are per site and are local to the customer.
+
+See #1 to track how this was developed and tested.
+"""
+
+import pytest
+
+from gallagher.dto.response import PdfResponse
+from gallagher.dto.detail import PdfDetail
+
+from gallagher.cc.cardholders import PdfDefinition
+
+
+@pytest.fixture
+async def pdf_definition() -> PdfResponse:
+    """Makes a single call to the pdf list
+
+    This is passed as a fixture to all other calls around
+    on this test to save network round trips.
+
+    :return: PdfResponse
+    """
 
     response = await PdfDefinition.list()
-    assert type(response) is PdfResponse
-    assert type(response.results) is list
-    assert len(response.results) > 0
+    return response
+
+
+async def test_pdf_list(pdf_definition: PdfResponse):
+
+    assert type(pdf_definition) is PdfResponse
+    assert type(pdf_definition.results) is list
+    assert len(pdf_definition.results) > 0
+
+
+async def test_pdf_detail(pdf_definition: PdfResponse):
+
+    for pdf in pdf_definition.results:
+        detail = await PdfDefinition.retrieve(pdf.id)
+
+        assert type(detail) is PdfDetail
diff --git a/tests/test_schedule.py b/tests/test_schedule.py
index ac92cb80..5098ee38 100644
--- a/tests/test_schedule.py
+++ b/tests/test_schedule.py
@@ -1,19 +1,34 @@
+""" Schedule lists
 """
 
+import pytest
+
+from gallagher.dto.response import (
+    ScheduleSummaryResponse,
+)
+
+from gallagher.cc.alarms.schedule import Schedule
 
-"""
 
+@pytest.fixture
+async def schedule_summary_response() -> ScheduleSummaryResponse:
+    """Makes a single call to the schedule list
 
-async def test_schedules_list():
+    This is passed as a fixture to all other calls around
+    on this test to save network round trips.
 
-    from gallagher.cc.alarms.schedule import (
-        Schedule
-    )
-    from gallagher.dto.response import (
-        ScheduleSummaryResponse,
-    )
+    :return: ScheduleSummaryResponse
+    """
 
     response = await Schedule.list()
-    assert type(response) is ScheduleSummaryResponse
-    assert type(response.results) is list
-    assert len(response.results) > 0
+    return response
+
+
+async def test_schedules_list(schedule_summary_response: ScheduleSummaryResponse):
+    """Tests the schedule list
+
+    :param schedule_summary_response: ScheduleSummaryResponse
+    """
+    assert type(schedule_summary_response) is ScheduleSummaryResponse
+    assert type(schedule_summary_response.results) is list
+    assert len(schedule_summary_response.results) > 0
diff --git a/tests/test_version.py b/tests/test_version.py
index 77e8770f..66df9470 100644
--- a/tests/test_version.py
+++ b/tests/test_version.py
@@ -1,10 +1,8 @@
 """ Tests to see that are running the tests against the right version
-
-
 """
 
 from gallagher import __version__
 
 
 async def test_version():
-    assert __version__ == '0.1.0-alpha.3'
+    assert __version__ == "0.1.0-alpha.3"