diff --git a/.env b/.env index 81ac5e67..b2900a7c 100644 --- a/.env +++ b/.env @@ -1,6 +1,18 @@ # Default configuration values. These are overwritten by docker-compose.yml environment variables -VALENTINA_LOG_FILE = "/valentina/valentina.log" -VALENTINA_LOG_LEVEL_AWS = "ERROR" # Log level for AWS S3 client -VALENTINA_LOG_LEVEL_HTTP = "INFO" # Log level for discord HTTP, gateway, webhook, client events -VALENTINA_LOG_LEVEL = "INFO" # Overall log level for the bot -VALENTINA_LOG_LEVEL_PYMONGO = "ERROR" # Log level for pymongo +VALENTINA_LOG_FILE=/valentina.log +VALENTINA_LOG_LEVEL_AWS=ERROR +VALENTINA_LOG_LEVEL_HTTP=INFO +VALENTINA_LOG_LEVEL=INFO +VALENTINA_LOG_LEVEL_PYMONGO=ERROR + +VALENTINA_WEBUI_ENABLE=true +VALENTINA_WEBUI_PORT=8088 + +# Set to 0.0.0.0 in Docker +VALENTINA_WEBUI_HOST=127.0.0.1 + +# Used for Discord oauth callback url +VALENTINA_WEBUI_BASE_URL=http://127.0.0.1:8088 +VALENTINA_WEBUI_LOG_LEVEL=DEBUG +VALENTINA_WEBUI_DEBUG=true +VALENTINA_REDIS_ADDR=127.0.0.1:6379 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2da58e34..baa669b8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ default_stages: [commit, manual] fail_fast: true repos: - repo: "https://github.com/commitizen-tools/commitizen" - rev: v3.27.0 + rev: v3.29.0 hooks: - id: commitizen # - id: commitizen-branch @@ -59,17 +59,23 @@ repos: entry: yamllint --strict --config-file .yamllint.yml - repo: "https://github.com/charliermarsh/ruff-pre-commit" - rev: "v0.5.1" + rev: "v0.5.7" hooks: - id: ruff exclude: tests/ - id: ruff-format - repo: "https://github.com/crate-ci/typos" - rev: typos-v0.10.26 + rev: v1.23.6 hooks: - id: typos + - repo: "https://github.com/djlint/djLint" + rev: v1.34.1 + hooks: + - id: djlint + args: ["--configuration", "pyproject.toml"] + - repo: local hooks: # This calls a custom pre-commit script. diff --git a/.typos.toml b/.typos.toml index 91e2502d..66c1e558 100644 --- a/.typos.toml +++ b/.typos.toml @@ -6,4 +6,4 @@ # nd = "nd" # Used in the context of '2nd' [files] - extend-exclude = ["*_cache", ".venv"] + extend-exclude = ["*_cache", ".env", ".env.secrets", ".venv", "static"] diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..f548f375 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "files.associations": { + "*.html": "jinja-html", + "*.html.j2": "jinja-html" + } +} diff --git a/README.md b/README.md index f8316900..e7b6b8e2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,24 @@ # Valentina -A discord bot to manage roll playing sessions for a highly customized version of Vampire the Masquerade. Major differences from the published game are: +A Discord bot and optional web service to manage roll playing sessions for a highly customized version of the White Wolf series of TTRPGs. This project is not affiliated with White Wolf or [Paradox Interactive](https://www.paradoxinteractive.com/). + +## Topline Features + +- Character creation and management +- Campaign management +- Dice rolling +- Storyteller tools +- Other niceties such as: + - Optional Web UI + - Github integration + - Image uploads + - Statistic tracking + - And more! + +## Ruleset Overview + +Major differences from the games published by White Wolf are: 1. Dice are rolled as a single pool of D10s with a set difficulty. The number of success determines the outcome of the roll. @@ -16,54 +33,74 @@ To play with traditional rules I strongly recommend you use [Inconnu Bot](https: **For more information on the features and functionality, see the [User Guide](user_guide.md)** -## Install and run +# Install and run -### Prerequisites +## Prerequisites Before running Valentina, the following must be configured or installed. +
+Discord Bot + - Docker and Docker Compose - A valid Discord Bot token. Instructions for this can be found on [Discord's Developer Portal](https://discord.com/developers/docs/getting-started) -- To use image uploads, an AWS S3 Bucket must be configured with appropriate permissions. _(Instructions on how to do this are out of scope for this document)_ - - - Public must be able to read objects from the bucket - - An IAM role must be created with read/write/list access and the credentials added to the environment variables. - - ```json - { - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "GetBucketLocation", - "Effect": "Allow", - "Action": ["s3:GetBucketLocation"], - "Resource": ["arn:aws:s3:::Bucket-Name"] - }, - { - "Sid": "ListObjectsInBucket", - "Effect": "Allow", - "Action": ["s3:ListBucket"], - "Resource": ["arn:aws:s3:::Bucket-Name"] - }, - { - "Sid": "AllObjectActions", - "Effect": "Allow", - "Action": "s3:*Object", - "Resource": ["arn:aws:s3:::Bucket-Name/*"] - } - ] - } - ``` - -### Run the bot + +
+ +
+Web UI (Optional) + +- A Redis instance for caching. This can be run locally or in a cloud service. +- Discord OAuth credentials for the bot. Instructions for this can be found on [Discord's Developer Portal](https://discord.com/developers/docs/topics/oauth2) +- Ability to run the Docker container on a public IP address or domain name. This is outside the scope of this document. +
+ +
+Image Uploads (Optional) +To allow image uploads, an AWS S3 bucket must be configured with appropriate permissions. Instructions for this can be found on the [AWS Documentation](https://docs.aws.amazon.com/AmazonS3/latest/userguide/creating-bucket.html) + +- Public must be able to read objects from the bucket +- An IAM role must be created with read/write/list access and the credentials added to the environment variables. + + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "GetBucketLocation", + "Effect": "Allow", + "Action": ["s3:GetBucketLocation"], + "Resource": ["arn:aws:s3:::Bucket-Name"] + }, + { + "Sid": "ListObjectsInBucket", + "Effect": "Allow", + "Action": ["s3:ListBucket"], + "Resource": ["arn:aws:s3:::Bucket-Name"] + }, + { + "Sid": "AllObjectActions", + "Effect": "Allow", + "Action": "s3:*Object", + "Resource": ["arn:aws:s3:::Bucket-Name/*"] + } + ] + } + ``` + +
+ +## Run the bot 1. Copy the `docker-compose.yml` file to a directory on your machine 2. Edit the `docker-compose.yml` file - In the `volumes` section replace `/path/to/data` with the directory to hold persistent storage - - In the `environment` section add correct values to each environment variable. All available environment variables are below. + - In the `environment` section add correct values to each environment variable. 3. Run `docker compose up` -#### Environment Variables +### Environment Variables + +Settings for Valentina are controlled by environment variables. The following is a list of the available variables and their default values. | Variable | Default Value | Usage | | --- | --- | --- | @@ -83,14 +120,25 @@ Before running Valentina, the following must be configured or installed. | VALENTINA_MONGO_DATABASE_NAME | `valentina` | Production Database name | | VALENTINA_GITHUB_REPO | | Optional: Sets the Github repo to use for Github integration `username/repo` | | VALENTINA_GITHUB_TOKEN | | Optional: Sets the Github API Access token to use for Github integration | +| VALENTINA_WEBUI_ENABLE | `false` | Optional: Enables the web UI. Set to `true` to enable. | +| VALENTINA_WEBUI_HOST | `127.0.0.1` | Set the host IP for the web UI. Note: when running in Docker this should always be `0.0.0.0` | +| VALENTINA_WEBUI_PORT | `8000` | Set the port for the web UI. | +| VALENTINA_WEBUI_LOG_LEVEL | `INFO` | Sets the log level for the web UI. One of `TRACE`, `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` | +| VALENTINA_WEBUI_BASE_URL | `http://127.0.0.1:8088` | Base URL for the web service. | +| VALENTINA_WEBUI_DEBUG | `false` | Enables debug mode for the web UI. Set to `true` to enable. | +| VALENTINA_REDIS_ADDRESS | `127.0.0.1:6379` | Sets the IP and port for the Redis instance | +| VALENTINA_REDIS_PASSWORD | | Optional: Sets the password for the Redis instance | +| VALENTINA_WEBUI_SECRET_KEY | | Sets the secret key for the web UI. This is required to run the web UI. | +| VALENTINA_DISCORD_OAUTH_SECRET | | Sets the secret for the Discord OAuth. This is required to run the web UI. | +| VALENTINA_DISCORD_OAUTH_CLIENT_ID | | Sets the ID for the Discord OAuth. This is required to run the web UI. | --- -## Contributing +# Contributing ## Setup: Once per project -1. Install Python 3.11 and [Poetry](https://python-poetry.org) +1. Install Python >= 3.11 and [Poetry](https://python-poetry.org) 2. Clone this repository. `git clone https://github.com/natelandau/valentina.git` 3. Install the Poetry environment with `poetry install`. 4. Activate your Poetry environment with `poetry shell`. @@ -114,6 +162,24 @@ Before running Valentina, the following must be configured or installed. - Run `poetry remove {package}` from within the development environment to uninstall a runtime dependency and remove it from `pyproject.toml` and `poetry.lock`. - Run `poetry update` from within the development environment to upgrade all dependencies to the latest versions allowed by `pyproject.toml`. +## Packages Used + +**Discord Bot** + +- [Pycord](https://docs.pycord.dev/en/stable/) - Discord API wrapper +- [Beanie ODM](https://beanie-odm.dev/) - MongoDB ODM +- [Typer](https://typer.tiangolo.com/) - CLI app framework +- [ConfZ](https://confz.readthedocs.io/en/latest/index.html) - Configuration management +- [Loguru](https://loguru.readthedocs.io/en/stable/) - Logging + +**Web UI** + +- [Quart](https://quart.palletsprojects.com/en/latest/index.html) - Async web framework based on Flask +- [Bootstrap](https://getbootstrap.com/) - Frontend framework for the web UI +- [Jinja](https://jinja.palletsprojects.com/en/3.0.x/) - Templating engine for the web UI +- [JinjaX](https://jinjax.scaletti.dev/) - Super components powers for your Jinja templates +- [quart-wtf](https://quart-wtf.readthedocs.io/en/latest/index.html) - Integration of Quart and WTForms including CSRF and file uploading. + ## Testing MongoDB locally To run the tests associated with the MongoDB database, you must have MongoDB installed locally. The easiest way to do this is with Docker. Set two additional environment variables to allow the tests to connect to the local MongoDB instance. @@ -125,6 +191,10 @@ To run the tests associated with the MongoDB database, you must have MongoDB ins NOTE: Github CI integrations will ignore these variables and run the tests against a Mongo instance within the workflows. +## Running the webui locally + +A convenience script that runs the webui locally without the Discord bot is available. After setting the required environment variables, and entering the Virtual Environment simply type `webui`. Note, a Redis instance is still required.. + ## Troubleshooting If connecting to Discord with the bot fails due to a certificate error, run `scripts/install_certifi.py` to install the latest certificate bundle. diff --git a/TODO.md b/TODO.md index 32de9969..3fecc1e3 100644 --- a/TODO.md +++ b/TODO.md @@ -30,3 +30,5 @@ - [x] Refactor: Centralize pagination for long responses - [x] Statistics: Track per campaign stats - [x] Storyteller: Associate storyteller characters with campaigns +- [ ] WebUI: Allow production callback url in bot oath settings from Discord developer portal +- [ ] WebUI: Add bind to `0.0.0.0:8000` before release diff --git a/docker-compose.yml b/docker-compose.yml index c7d4ef8a..c171daba 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,16 +1,19 @@ --- -version: '3.8' +version: "3.8" services: - valentina: - image: ghcr.io/natelandau/valentina:latest - hostname: valentina - container_name: valentina - network_mode: "bridge" - volumes: - - path/to/dir:/valentina # Persistent storage for the database - environment: - - VALENTINA_DISCORD_TOKEN=TOKEN # Your discord bot token - - VALENTINA_GUILDS=ID1,ID2 # Guild IDs where you will run the bot, comma separated - - VALENTINA_OWNER_IDS=ID1,ID2 # Discord user IDs for bot owners - - VALENTINA_LOG_LEVEL=INFO # TRACE, DEBUG, INFO, WARNING, ERROR, CRITICAL - restart: unless-stopped + valentina: + image: ghcr.io/natelandau/valentina:latest + hostname: valentina + container_name: valentina + network_mode: "bridge" + # Uncomment if using the web interface + # ports: + # - ${VALENTINA_WEBUI_PORT}:${VALENTINA_WEBUI_PORT} + volumes: + - path/to/dir:/valentina # Persistent storage for the database + environment: + - VALENTINA_DISCORD_TOKEN=TOKEN # Your discord bot token + - VALENTINA_GUILDS=ID1,ID2 # Guild IDs where you will run the bot, comma separated + - VALENTINA_OWNER_IDS=ID1,ID2 # Discord user IDs for bot owners + - VALENTINA_LOG_LEVEL=INFO # TRACE, DEBUG, INFO, WARNING, ERROR, CRITICAL + restart: unless-stopped diff --git a/poetry.lock b/poetry.lock index 2c55aa54..f0c189ce 100644 --- a/poetry.lock +++ b/poetry.lock @@ -11,92 +11,104 @@ files = [ {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}, ] +[[package]] +name = "aiohappyeyeballs" +version = "2.3.6" +description = "Happy Eyeballs for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiohappyeyeballs-2.3.6-py3-none-any.whl", hash = "sha256:15dca2611fa78442f1cb54cf07ffb998573f2b4fbeab45ca8554c045665c896b"}, + {file = "aiohappyeyeballs-2.3.6.tar.gz", hash = "sha256:88211068d2a40e0436033956d7de3926ff36d54776f8b1022d6b21320cadae79"}, +] + [[package]] name = "aiohttp" -version = "3.9.5" +version = "3.10.3" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.8" files = [ - {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"}, + {file = "aiohttp-3.10.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cc36cbdedf6f259371dbbbcaae5bb0e95b879bc501668ab6306af867577eb5db"}, + {file = "aiohttp-3.10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85466b5a695c2a7db13eb2c200af552d13e6a9313d7fa92e4ffe04a2c0ea74c1"}, + {file = "aiohttp-3.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:71bb1d97bfe7e6726267cea169fdf5df7658831bb68ec02c9c6b9f3511e108bb"}, + {file = "aiohttp-3.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baec1eb274f78b2de54471fc4c69ecbea4275965eab4b556ef7a7698dee18bf2"}, + {file = "aiohttp-3.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13031e7ec1188274bad243255c328cc3019e36a5a907978501256000d57a7201"}, + {file = "aiohttp-3.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2bbc55a964b8eecb341e492ae91c3bd0848324d313e1e71a27e3d96e6ee7e8e8"}, + {file = "aiohttp-3.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8cc0564b286b625e673a2615ede60a1704d0cbbf1b24604e28c31ed37dc62aa"}, + {file = "aiohttp-3.10.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f817a54059a4cfbc385a7f51696359c642088710e731e8df80d0607193ed2b73"}, + {file = "aiohttp-3.10.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8542c9e5bcb2bd3115acdf5adc41cda394e7360916197805e7e32b93d821ef93"}, + {file = "aiohttp-3.10.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:671efce3a4a0281060edf9a07a2f7e6230dca3a1cbc61d110eee7753d28405f7"}, + {file = "aiohttp-3.10.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0974f3b5b0132edcec92c3306f858ad4356a63d26b18021d859c9927616ebf27"}, + {file = "aiohttp-3.10.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:44bb159b55926b57812dca1b21c34528e800963ffe130d08b049b2d6b994ada7"}, + {file = "aiohttp-3.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6ae9ae382d1c9617a91647575255ad55a48bfdde34cc2185dd558ce476bf16e9"}, + {file = "aiohttp-3.10.3-cp310-cp310-win32.whl", hash = "sha256:aed12a54d4e1ee647376fa541e1b7621505001f9f939debf51397b9329fd88b9"}, + {file = "aiohttp-3.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:b51aef59370baf7444de1572f7830f59ddbabd04e5292fa4218d02f085f8d299"}, + {file = "aiohttp-3.10.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e021c4c778644e8cdc09487d65564265e6b149896a17d7c0f52e9a088cc44e1b"}, + {file = "aiohttp-3.10.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:24fade6dae446b183e2410a8628b80df9b7a42205c6bfc2eff783cbeedc224a2"}, + {file = "aiohttp-3.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bc8e9f15939dacb0e1f2d15f9c41b786051c10472c7a926f5771e99b49a5957f"}, + {file = "aiohttp-3.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5a9ec959b5381271c8ec9310aae1713b2aec29efa32e232e5ef7dcca0df0279"}, + {file = "aiohttp-3.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a5d0ea8a6467b15d53b00c4e8ea8811e47c3cc1bdbc62b1aceb3076403d551f"}, + {file = "aiohttp-3.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9ed607dbbdd0d4d39b597e5bf6b0d40d844dfb0ac6a123ed79042ef08c1f87e"}, + {file = "aiohttp-3.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3e66d5b506832e56add66af88c288c1d5ba0c38b535a1a59e436b300b57b23e"}, + {file = "aiohttp-3.10.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fda91ad797e4914cca0afa8b6cccd5d2b3569ccc88731be202f6adce39503189"}, + {file = "aiohttp-3.10.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:61ccb867b2f2f53df6598eb2a93329b5eee0b00646ee79ea67d68844747a418e"}, + {file = "aiohttp-3.10.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6d881353264e6156f215b3cb778c9ac3184f5465c2ece5e6fce82e68946868ef"}, + {file = "aiohttp-3.10.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b031ce229114825f49cec4434fa844ccb5225e266c3e146cb4bdd025a6da52f1"}, + {file = "aiohttp-3.10.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5337cc742a03f9e3213b097abff8781f79de7190bbfaa987bd2b7ceb5bb0bdec"}, + {file = "aiohttp-3.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ab3361159fd3dcd0e48bbe804006d5cfb074b382666e6c064112056eb234f1a9"}, + {file = "aiohttp-3.10.3-cp311-cp311-win32.whl", hash = "sha256:05d66203a530209cbe40f102ebaac0b2214aba2a33c075d0bf825987c36f1f0b"}, + {file = "aiohttp-3.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:70b4a4984a70a2322b70e088d654528129783ac1ebbf7dd76627b3bd22db2f17"}, + {file = "aiohttp-3.10.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:166de65e2e4e63357cfa8417cf952a519ac42f1654cb2d43ed76899e2319b1ee"}, + {file = "aiohttp-3.10.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7084876352ba3833d5d214e02b32d794e3fd9cf21fdba99cff5acabeb90d9806"}, + {file = "aiohttp-3.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d98c604c93403288591d7d6d7d6cc8a63459168f8846aeffd5b3a7f3b3e5e09"}, + {file = "aiohttp-3.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d73b073a25a0bb8bf014345374fe2d0f63681ab5da4c22f9d2025ca3e3ea54fc"}, + {file = "aiohttp-3.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8da6b48c20ce78f5721068f383e0e113dde034e868f1b2f5ee7cb1e95f91db57"}, + {file = "aiohttp-3.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a9dcdccf50284b1b0dc72bc57e5bbd3cc9bf019060dfa0668f63241ccc16aa7"}, + {file = "aiohttp-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56fb94bae2be58f68d000d046172d8b8e6b1b571eb02ceee5535e9633dcd559c"}, + {file = "aiohttp-3.10.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bf75716377aad2c718cdf66451c5cf02042085d84522aec1f9246d3e4b8641a6"}, + {file = "aiohttp-3.10.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6c51ed03e19c885c8e91f574e4bbe7381793f56f93229731597e4a499ffef2a5"}, + {file = "aiohttp-3.10.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b84857b66fa6510a163bb083c1199d1ee091a40163cfcbbd0642495fed096204"}, + {file = "aiohttp-3.10.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c124b9206b1befe0491f48185fd30a0dd51b0f4e0e7e43ac1236066215aff272"}, + {file = "aiohttp-3.10.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3461d9294941937f07bbbaa6227ba799bc71cc3b22c40222568dc1cca5118f68"}, + {file = "aiohttp-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:08bd0754d257b2db27d6bab208c74601df6f21bfe4cb2ec7b258ba691aac64b3"}, + {file = "aiohttp-3.10.3-cp312-cp312-win32.whl", hash = "sha256:7f9159ae530297f61a00116771e57516f89a3de6ba33f314402e41560872b50a"}, + {file = "aiohttp-3.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:e1128c5d3a466279cb23c4aa32a0f6cb0e7d2961e74e9e421f90e74f75ec1edf"}, + {file = "aiohttp-3.10.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d1100e68e70eb72eadba2b932b185ebf0f28fd2f0dbfe576cfa9d9894ef49752"}, + {file = "aiohttp-3.10.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a541414578ff47c0a9b0b8b77381ea86b0c8531ab37fc587572cb662ccd80b88"}, + {file = "aiohttp-3.10.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d5548444ef60bf4c7b19ace21f032fa42d822e516a6940d36579f7bfa8513f9c"}, + {file = "aiohttp-3.10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba2e838b5e6a8755ac8297275c9460e729dc1522b6454aee1766c6de6d56e5e"}, + {file = "aiohttp-3.10.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48665433bb59144aaf502c324694bec25867eb6630fcd831f7a893ca473fcde4"}, + {file = "aiohttp-3.10.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bac352fceed158620ce2d701ad39d4c1c76d114255a7c530e057e2b9f55bdf9f"}, + {file = "aiohttp-3.10.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b0f670502100cdc567188c49415bebba947eb3edaa2028e1a50dd81bd13363f"}, + {file = "aiohttp-3.10.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43b09f38a67679e32d380fe512189ccb0b25e15afc79b23fbd5b5e48e4fc8fd9"}, + {file = "aiohttp-3.10.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:cd788602e239ace64f257d1c9d39898ca65525583f0fbf0988bcba19418fe93f"}, + {file = "aiohttp-3.10.3-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:214277dcb07ab3875f17ee1c777d446dcce75bea85846849cc9d139ab8f5081f"}, + {file = "aiohttp-3.10.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:32007fdcaab789689c2ecaaf4b71f8e37bf012a15cd02c0a9db8c4d0e7989fa8"}, + {file = "aiohttp-3.10.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:123e5819bfe1b87204575515cf448ab3bf1489cdeb3b61012bde716cda5853e7"}, + {file = "aiohttp-3.10.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:812121a201f0c02491a5db335a737b4113151926a79ae9ed1a9f41ea225c0e3f"}, + {file = "aiohttp-3.10.3-cp38-cp38-win32.whl", hash = "sha256:b97dc9a17a59f350c0caa453a3cb35671a2ffa3a29a6ef3568b523b9113d84e5"}, + {file = "aiohttp-3.10.3-cp38-cp38-win_amd64.whl", hash = "sha256:3731a73ddc26969d65f90471c635abd4e1546a25299b687e654ea6d2fc052394"}, + {file = "aiohttp-3.10.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38d91b98b4320ffe66efa56cb0f614a05af53b675ce1b8607cdb2ac826a8d58e"}, + {file = "aiohttp-3.10.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9743fa34a10a36ddd448bba8a3adc2a66a1c575c3c2940301bacd6cc896c6bf1"}, + {file = "aiohttp-3.10.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7c126f532caf238031c19d169cfae3c6a59129452c990a6e84d6e7b198a001dc"}, + {file = "aiohttp-3.10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:926e68438f05703e500b06fe7148ef3013dd6f276de65c68558fa9974eeb59ad"}, + {file = "aiohttp-3.10.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:434b3ab75833accd0b931d11874e206e816f6e6626fd69f643d6a8269cd9166a"}, + {file = "aiohttp-3.10.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d35235a44ec38109b811c3600d15d8383297a8fab8e3dec6147477ec8636712a"}, + {file = "aiohttp-3.10.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59c489661edbd863edb30a8bd69ecb044bd381d1818022bc698ba1b6f80e5dd1"}, + {file = "aiohttp-3.10.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50544fe498c81cb98912afabfc4e4d9d85e89f86238348e3712f7ca6a2f01dab"}, + {file = "aiohttp-3.10.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:09bc79275737d4dc066e0ae2951866bb36d9c6b460cb7564f111cc0427f14844"}, + {file = "aiohttp-3.10.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:af4dbec58e37f5afff4f91cdf235e8e4b0bd0127a2a4fd1040e2cad3369d2f06"}, + {file = "aiohttp-3.10.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b22cae3c9dd55a6b4c48c63081d31c00fc11fa9db1a20c8a50ee38c1a29539d2"}, + {file = "aiohttp-3.10.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ba562736d3fbfe9241dad46c1a8994478d4a0e50796d80e29d50cabe8fbfcc3f"}, + {file = "aiohttp-3.10.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f25d6c4e82d7489be84f2b1c8212fafc021b3731abdb61a563c90e37cced3a21"}, + {file = "aiohttp-3.10.3-cp39-cp39-win32.whl", hash = "sha256:b69d832e5f5fa15b1b6b2c8eb6a9fd2c0ec1fd7729cb4322ed27771afc9fc2ac"}, + {file = "aiohttp-3.10.3-cp39-cp39-win_amd64.whl", hash = "sha256:673bb6e3249dc8825df1105f6ef74e2eab779b7ff78e96c15cadb78b04a83752"}, + {file = "aiohttp-3.10.3.tar.gz", hash = "sha256:21650e7032cc2d31fc23d353d7123e771354f2a3d5b05a5647fc30fea214e696"}, ] [package.dependencies] +aiohappyeyeballs = ">=2.3.0" aiosignal = ">=1.1.2" attrs = ">=17.3.0" frozenlist = ">=1.1.1" @@ -104,7 +116,7 @@ multidict = ">=4.5,<7.0" yarl = ">=1.0,<2.0" [package.extras] -speedups = ["Brotli", "aiodns", "brotlicffi"] +speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] [[package]] name = "aiosignal" @@ -133,13 +145,13 @@ files = [ [[package]] name = "argcomplete" -version = "3.3.0" +version = "3.5.0" description = "Bash tab completion for argparse" optional = false python-versions = ">=3.8" files = [ - {file = "argcomplete-3.3.0-py3-none-any.whl", hash = "sha256:c168c3723482c031df3c207d4ba8fa702717ccb9fc0bfe4117166c1f537b4a54"}, - {file = "argcomplete-3.3.0.tar.gz", hash = "sha256:fd03ff4a5b9e6580569d34b273f741e85cd9e072f3feeeee3eba4891c70eda62"}, + {file = "argcomplete-3.5.0-py3-none-any.whl", hash = "sha256:d4bcf3ff544f51e16e54228a7ac7f486ed70ebf2ecfe49a63a91171c76bf029b"}, + {file = "argcomplete-3.5.0.tar.gz", hash = "sha256:4349400469dccfb7950bb60334a680c58d88699bff6159df61251878dc6bf74b"}, ] [package.extras] @@ -164,24 +176,35 @@ types-python-dateutil = ">=2.8.10" doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"] test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2021.1)", "simplejson (==3.*)"] +[[package]] +name = "async-timeout" +version = "4.0.3" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.7" +files = [ + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, +] + [[package]] name = "attrs" -version = "23.2.0" +version = "24.2.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.7" files = [ - {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, - {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, + {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, + {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, ] [package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] -tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] [[package]] name = "beanie" @@ -206,19 +229,30 @@ doc = ["Markdown (>=3.3)", "Pygments (>=2.8.0)", "jinja2 (>=3.0.3)", "mkdocs (>= queue = ["beanie-batteries-queue (>=0.2)"] test = ["asgi-lifespan (>=1.0.1)", "dnspython (>=2.1.0)", "fastapi (>=0.100)", "flake8 (>=3)", "httpx (>=0.23.0)", "pre-commit (>=2.3.0)", "pydantic-extra-types (>=2)", "pydantic-settings (>=2)", "pydantic[email]", "pyright (>=0)", "pytest (>=6.0.0)", "pytest-asyncio (>=0.21.0)", "pytest-cov (>=2.8.1)"] +[[package]] +name = "blinker" +version = "1.8.2" +description = "Fast, simple object-to-object and broadcast signaling" +optional = false +python-versions = ">=3.8" +files = [ + {file = "blinker-1.8.2-py3-none-any.whl", hash = "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01"}, + {file = "blinker-1.8.2.tar.gz", hash = "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83"}, +] + [[package]] name = "boto3" -version = "1.34.143" +version = "1.34.162" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.34.143-py3-none-any.whl", hash = "sha256:0d16832f23e6bd3ae94e35ea8e625529850bfad9baccd426de96ad8f445d8e03"}, - {file = "boto3-1.34.143.tar.gz", hash = "sha256:b590ce80c65149194def43ebf0ea1cf0533945502507837389a8d22e3ecbcf05"}, + {file = "boto3-1.34.162-py3-none-any.whl", hash = "sha256:d6f6096bdab35a0c0deff469563b87d184a28df7689790f7fe7be98502b7c590"}, + {file = "boto3-1.34.162.tar.gz", hash = "sha256:873f8f5d2f6f85f1018cbb0535b03cceddc7b655b61f66a0a56995238804f41f"}, ] [package.dependencies] -botocore = ">=1.34.143,<1.35.0" +botocore = ">=1.34.162,<1.35.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -227,13 +261,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.34.143" +version = "1.34.162" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.34.143-py3-none-any.whl", hash = "sha256:094aea179e8aaa1bc957ad49cc27d93b189dd3a1f3075d8b0ca7c445a2a88430"}, - {file = "botocore-1.34.143.tar.gz", hash = "sha256:059f032ec05733a836e04e869c5a15534420102f93116f3bc9a5b759b0651caf"}, + {file = "botocore-1.34.162-py3-none-any.whl", hash = "sha256:2d918b02db88d27a75b48275e6fb2506e9adaaddbec1ffa6a8a0898b34e769be"}, + {file = "botocore-1.34.162.tar.gz", hash = "sha256:adc23be4fb99ad31961236342b7cbf3c0bfc62532cd02852196032e8c0d682f3"}, ] [package.dependencies] @@ -242,7 +276,18 @@ python-dateutil = ">=2.1,<3.0.0" urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} [package.extras] -crt = ["awscrt (==0.20.11)"] +crt = ["awscrt (==0.21.2)"] + +[[package]] +name = "cachetools" +version = "5.4.0" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.7" +files = [ + {file = "cachetools-5.4.0-py3-none-any.whl", hash = "sha256:3ae3b49a3d5e28a77a0be2b37dbcb89005058959cb2323858c2657c4a8cab474"}, + {file = "cachetools-5.4.0.tar.gz", hash = "sha256:b8adc2e7c07f105ced7bc56dbb6dfbe7c4a00acce20e2227b3f355be89bc6827"}, +] [[package]] name = "certifi" @@ -257,63 +302,78 @@ files = [ [[package]] name = "cffi" -version = "1.16.0" +version = "1.17.0" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" files = [ - {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, - {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, - {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, - {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, - {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, - {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, - {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, - {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, - {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, - {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, - {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, - {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, - {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, - {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, - {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, + {file = "cffi-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f9338cc05451f1942d0d8203ec2c346c830f8e86469903d5126c1f0a13a2bcbb"}, + {file = "cffi-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0ce71725cacc9ebf839630772b07eeec220cbb5f03be1399e0457a1464f8e1a"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c815270206f983309915a6844fe994b2fa47e5d05c4c4cef267c3b30e34dbe42"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6bdcd415ba87846fd317bee0774e412e8792832e7805938987e4ede1d13046d"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a98748ed1a1df4ee1d6f927e151ed6c1a09d5ec21684de879c7ea6aa96f58f2"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a048d4f6630113e54bb4b77e315e1ba32a5a31512c31a273807d0027a7e69ab"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24aa705a5f5bd3a8bcfa4d123f03413de5d86e497435693b638cbffb7d5d8a1b"}, + {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:856bf0924d24e7f93b8aee12a3a1095c34085600aa805693fb7f5d1962393206"}, + {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4304d4416ff032ed50ad6bb87416d802e67139e31c0bde4628f36a47a3164bfa"}, + {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:331ad15c39c9fe9186ceaf87203a9ecf5ae0ba2538c9e898e3a6967e8ad3db6f"}, + {file = "cffi-1.17.0-cp310-cp310-win32.whl", hash = "sha256:669b29a9eca6146465cc574659058ed949748f0809a2582d1f1a324eb91054dc"}, + {file = "cffi-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:48b389b1fd5144603d61d752afd7167dfd205973a43151ae5045b35793232aa2"}, + {file = "cffi-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720"}, + {file = "cffi-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6"}, + {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91"}, + {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8"}, + {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb"}, + {file = "cffi-1.17.0-cp311-cp311-win32.whl", hash = "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9"}, + {file = "cffi-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0"}, + {file = "cffi-1.17.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc"}, + {file = "cffi-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150"}, + {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a"}, + {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885"}, + {file = "cffi-1.17.0-cp312-cp312-win32.whl", hash = "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492"}, + {file = "cffi-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2"}, + {file = "cffi-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118"}, + {file = "cffi-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f"}, + {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0"}, + {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4"}, + {file = "cffi-1.17.0-cp313-cp313-win32.whl", hash = "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a"}, + {file = "cffi-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7"}, + {file = "cffi-1.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:964823b2fc77b55355999ade496c54dde161c621cb1f6eac61dc30ed1b63cd4c"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:516a405f174fd3b88829eabfe4bb296ac602d6a0f68e0d64d5ac9456194a5b7e"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dec6b307ce928e8e112a6bb9921a1cb00a0e14979bf28b98e084a4b8a742bd9b"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4094c7b464cf0a858e75cd14b03509e84789abf7b79f8537e6a72152109c76e"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2404f3de742f47cb62d023f0ba7c5a916c9c653d5b368cc966382ae4e57da401"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa9d43b02a0c681f0bfbc12d476d47b2b2b6a3f9287f11ee42989a268a1833c"}, + {file = "cffi-1.17.0-cp38-cp38-win32.whl", hash = "sha256:0bb15e7acf8ab35ca8b24b90af52c8b391690ef5c4aec3d31f38f0d37d2cc499"}, + {file = "cffi-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:93a7350f6706b31f457c1457d3a3259ff9071a66f312ae64dc024f049055f72c"}, + {file = "cffi-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a2ddbac59dc3716bc79f27906c010406155031a1c801410f1bafff17ea304d2"}, + {file = "cffi-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6327b572f5770293fc062a7ec04160e89741e8552bf1c358d1a23eba68166759"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbc183e7bef690c9abe5ea67b7b60fdbca81aa8da43468287dae7b5c046107d4"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bdc0f1f610d067c70aa3737ed06e2726fd9d6f7bfee4a351f4c40b6831f4e82"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d872186c1617d143969defeadac5a904e6e374183e07977eedef9c07c8953bf"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d46ee4764b88b91f16661a8befc6bfb24806d885e27436fdc292ed7e6f6d058"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f76a90c345796c01d85e6332e81cab6d70de83b829cf1d9762d0a3da59c7932"}, + {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e60821d312f99d3e1569202518dddf10ae547e799d75aef3bca3a2d9e8ee693"}, + {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:eb09b82377233b902d4c3fbeeb7ad731cdab579c6c6fda1f763cd779139e47c3"}, + {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:24658baf6224d8f280e827f0a50c46ad819ec8ba380a42448e24459daf809cf4"}, + {file = "cffi-1.17.0-cp39-cp39-win32.whl", hash = "sha256:0fdacad9e0d9fc23e519efd5ea24a70348305e8d7d85ecbb1a5fa66dc834e7fb"}, + {file = "cffi-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:7cbc78dc018596315d4e7841c8c3a7ae31cc4d638c9b627f87d52e8abaaf2d29"}, + {file = "cffi-1.17.0.tar.gz", hash = "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76"}, ] [package.dependencies] @@ -456,21 +516,20 @@ files = [ [[package]] name = "commitizen" -version = "3.27.0" +version = "3.29.0" description = "Python commitizen client tool" optional = false python-versions = ">=3.8" files = [ - {file = "commitizen-3.27.0-py3-none-any.whl", hash = "sha256:11948fa563d5ad5464baf09eaacff3cf8cbade1ca029ed9c4978f2227f033130"}, - {file = "commitizen-3.27.0.tar.gz", hash = "sha256:5874d0c7e8e1be3b75b1b0a2269cffe3dd5c843b860d84b0bdbb9ea86e3474b8"}, + {file = "commitizen-3.29.0-py3-none-any.whl", hash = "sha256:0c6c479dbee6d19292315c6fca3782cf5c1f7f1638bc4bb5ab4cfb67f4e11894"}, + {file = "commitizen-3.29.0.tar.gz", hash = "sha256:586b30c1976850d244b836cd4730771097ba362c9c1684d1f8c379176c2ea532"}, ] [package.dependencies] -argcomplete = ">=1.12.1,<3.4" +argcomplete = ">=1.12.1,<3.6" charset-normalizer = ">=2.1.0,<4" colorama = ">=0.4.1,<0.5.0" decli = ">=0.6.0,<0.7.0" -importlib_metadata = ">=4.13,<8" jinja2 = ">=2.10.3" packaging = ">=19" pyyaml = ">=3.08" @@ -497,63 +556,83 @@ toml = ">=0.10.2,<0.11.0" [[package]] name = "coverage" -version = "7.5.4" +version = "7.6.1" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {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"}, + {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, + {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, + {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, + {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, + {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, + {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, + {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, + {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, + {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, + {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, + {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, + {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, + {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, + {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, + {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, + {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, + {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, + {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, ] [package.extras] @@ -561,43 +640,38 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "42.0.8" +version = "43.0.0" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e"}, - {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d"}, - {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902"}, - {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801"}, - {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949"}, - {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9"}, - {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583"}, - {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7"}, - {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b"}, - {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7"}, - {file = "cryptography-42.0.8-cp37-abi3-win32.whl", hash = "sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2"}, - {file = "cryptography-42.0.8-cp37-abi3-win_amd64.whl", hash = "sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba"}, - {file = "cryptography-42.0.8-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28"}, - {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e"}, - {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70"}, - {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c"}, - {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7"}, - {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e"}, - {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961"}, - {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1"}, - {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14"}, - {file = "cryptography-42.0.8-cp39-abi3-win32.whl", hash = "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c"}, - {file = "cryptography-42.0.8-cp39-abi3-win_amd64.whl", hash = "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a"}, - {file = "cryptography-42.0.8-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe"}, - {file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c"}, - {file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71"}, - {file = "cryptography-42.0.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d"}, - {file = "cryptography-42.0.8-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c"}, - {file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842"}, - {file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648"}, - {file = "cryptography-42.0.8-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad"}, - {file = "cryptography-42.0.8.tar.gz", hash = "sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2"}, + {file = "cryptography-43.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf"}, + {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55"}, + {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431"}, + {file = "cryptography-43.0.0-cp37-abi3-win32.whl", hash = "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc"}, + {file = "cryptography-43.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778"}, + {file = "cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f"}, + {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0"}, + {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b"}, + {file = "cryptography-43.0.0-cp39-abi3-win32.whl", hash = "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf"}, + {file = "cryptography-43.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1"}, + {file = "cryptography-43.0.0.tar.gz", hash = "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e"}, ] [package.dependencies] @@ -610,9 +684,24 @@ nox = ["nox"] pep8test = ["check-sdist", "click", "mypy", "ruff"] sdist = ["build"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test = ["certifi", "cryptography-vectors (==43.0.0)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] +[[package]] +name = "cssbeautifier" +version = "1.15.1" +description = "CSS unobfuscator and beautifier." +optional = false +python-versions = "*" +files = [ + {file = "cssbeautifier-1.15.1.tar.gz", hash = "sha256:9f7064362aedd559c55eeecf6b6bed65e05f33488dcbe39044f0403c26e1c006"}, +] + +[package.dependencies] +editorconfig = ">=0.12.2" +jsbeautifier = "*" +six = ">=1.13.0" + [[package]] name = "decli" version = "0.6.2" @@ -669,6 +758,30 @@ files = [ {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, ] +[[package]] +name = "djlint" +version = "1.34.1" +description = "HTML Template Linter and Formatter" +optional = false +python-versions = ">=3.8.0,<4.0.0" +files = [ + {file = "djlint-1.34.1-py3-none-any.whl", hash = "sha256:96ff1c464fb6f061130ebc88663a2ea524d7ec51f4b56221a2b3f0320a3cfce8"}, + {file = "djlint-1.34.1.tar.gz", hash = "sha256:db93fa008d19eaadb0454edf1704931d14469d48508daba2df9941111f408346"}, +] + +[package.dependencies] +click = ">=8.0.1,<9.0.0" +colorama = ">=0.4.4,<0.5.0" +cssbeautifier = ">=1.14.4,<2.0.0" +html-tag-names = ">=0.1.2,<0.2.0" +html-void-elements = ">=0.1.0,<0.2.0" +jsbeautifier = ">=1.14.4,<2.0.0" +json5 = ">=0.9.11,<0.10.0" +pathspec = ">=0.12.0,<0.13.0" +PyYAML = ">=6.0,<7.0" +regex = ">=2023.0.0,<2024.0.0" +tqdm = ">=4.62.2,<5.0.0" + [[package]] name = "dnspython" version = "2.6.1" @@ -689,6 +802,16 @@ idna = ["idna (>=3.6)"] trio = ["trio (>=0.23)"] wmi = ["wmi (>=1.5.1)"] +[[package]] +name = "editorconfig" +version = "0.12.4" +description = "EditorConfig File Locator and Interpreter for Python" +optional = false +python-versions = "*" +files = [ + {file = "EditorConfig-0.12.4.tar.gz", hash = "sha256:24857fa1793917dd9ccf0c7810a07e05404ce9b823521c7dce22a4fb5d125f80"}, +] + [[package]] name = "execnet" version = "2.1.1" @@ -705,13 +828,13 @@ testing = ["hatch", "pre-commit", "pytest", "tox"] [[package]] name = "faker" -version = "26.0.0" +version = "26.3.0" description = "Faker is a Python package that generates fake data for you." optional = false python-versions = ">=3.8" files = [ - {file = "Faker-26.0.0-py3-none-any.whl", hash = "sha256:886ee28219be96949cd21ecc96c4c742ee1680e77f687b095202c8def1a08f06"}, - {file = "Faker-26.0.0.tar.gz", hash = "sha256:0f60978314973de02c00474c2ae899785a42b2cf4f41b7987e93c132a2b8a4a9"}, + {file = "Faker-26.3.0-py3-none-any.whl", hash = "sha256:97fe1e7e953dd640ca2cd4dfac4db7c4d2432dd1b7a244a3313517707f3b54e9"}, + {file = "Faker-26.3.0.tar.gz", hash = "sha256:7c10ebdf74aaa0cc4fe6ec6db5a71e8598ec33503524bd4b5f4494785a5670dd"}, ] [package.dependencies] @@ -733,6 +856,50 @@ docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1 testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] typing = ["typing-extensions (>=4.8)"] +[[package]] +name = "flask" +version = "3.0.3" +description = "A simple framework for building complex web applications." +optional = false +python-versions = ">=3.8" +files = [ + {file = "flask-3.0.3-py3-none-any.whl", hash = "sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3"}, + {file = "flask-3.0.3.tar.gz", hash = "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842"}, +] + +[package.dependencies] +blinker = ">=1.6.2" +click = ">=8.1.3" +itsdangerous = ">=2.1.2" +Jinja2 = ">=3.1.2" +Werkzeug = ">=3.0.0" + +[package.extras] +async = ["asgiref (>=3.2)"] +dotenv = ["python-dotenv"] + +[[package]] +name = "flask-discord" +version = "0.1.69" +description = "Discord OAuth2 extension for Flask." +optional = false +python-versions = "*" +files = [ + {file = "Flask-Discord-0.1.69.tar.gz", hash = "sha256:1029a26a57a1edcd78fc1243d8cf39385adad27cbbfd01d3c59cb5ceb8834645"}, + {file = "Flask_Discord-0.1.69-py3-none-any.whl", hash = "sha256:d48fc6c2975f876b9c1476f285d98966039be396b9d9585402e01350cf1d39fb"}, +] + +[package.dependencies] +cachetools = "*" +Flask = "*" +oauthlib = "*" +pyjwt = ">=2.4.0" +requests = "*" +requests-oauthlib = "*" + +[package.extras] +docs = ["sphinx (==1.8.3)"] + [[package]] name = "frozenlist" version = "1.4.1" @@ -819,6 +986,99 @@ files = [ {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, ] +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "h2" +version = "4.1.0" +description = "HTTP/2 State-Machine based protocol implementation" +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d"}, + {file = "h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb"}, +] + +[package.dependencies] +hpack = ">=4.0,<5" +hyperframe = ">=6.0,<7" + +[[package]] +name = "hpack" +version = "4.0.0" +description = "Pure-Python HPACK header compression" +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"}, + {file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"}, +] + +[[package]] +name = "html-tag-names" +version = "0.1.2" +description = "List of known HTML tag names" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "html-tag-names-0.1.2.tar.gz", hash = "sha256:04924aca48770f36b5a41c27e4d917062507be05118acb0ba869c97389084297"}, + {file = "html_tag_names-0.1.2-py3-none-any.whl", hash = "sha256:eeb69ef21078486b615241f0393a72b41352c5219ee648e7c61f5632d26f0420"}, +] + +[[package]] +name = "html-void-elements" +version = "0.1.0" +description = "List of HTML void tag names." +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "html-void-elements-0.1.0.tar.gz", hash = "sha256:931b88f84cd606fee0b582c28fcd00e41d7149421fb673e1e1abd2f0c4f231f0"}, + {file = "html_void_elements-0.1.0-py3-none-any.whl", hash = "sha256:784cf39db03cdeb017320d9301009f8f3480f9d7b254d0974272e80e0cb5e0d2"}, +] + +[[package]] +name = "hypercorn" +version = "0.17.3" +description = "A ASGI Server based on Hyper libraries and inspired by Gunicorn" +optional = false +python-versions = ">=3.8" +files = [ + {file = "hypercorn-0.17.3-py3-none-any.whl", hash = "sha256:059215dec34537f9d40a69258d323f56344805efb462959e727152b0aa504547"}, + {file = "hypercorn-0.17.3.tar.gz", hash = "sha256:1b37802ee3ac52d2d85270700d565787ab16cf19e1462ccfa9f089ca17574165"}, +] + +[package.dependencies] +h11 = "*" +h2 = ">=3.1.0" +priority = "*" +wsproto = ">=0.14.0" + +[package.extras] +docs = ["pydata_sphinx_theme", "sphinxcontrib_mermaid"] +h3 = ["aioquic (>=0.9.0,<1.0)"] +trio = ["trio (>=0.22.0)"] +uvloop = ["uvloop (>=0.18)"] + +[[package]] +name = "hyperframe" +version = "6.0.1" +description = "HTTP/2 framing layer for Python" +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15"}, + {file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"}, +] + [[package]] name = "identify" version = "2.6.0" @@ -844,25 +1104,6 @@ files = [ {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] -[[package]] -name = "importlib-metadata" -version = "7.2.1" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_metadata-7.2.1-py3-none-any.whl", hash = "sha256:ffef94b0b66046dd8ea2d619b701fe978d9264d38f3998bc4c27ec3b146a87c8"}, - {file = "importlib_metadata-7.2.1.tar.gz", hash = "sha256:509ecb2ab77071db5137c655e24ceb3eee66e7bbc6574165d0d114d9fc4bbe68"}, -] - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] - [[package]] name = "inflect" version = "7.3.1" @@ -893,6 +1134,17 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "itsdangerous" +version = "2.2.0" +description = "Safely pass data to untrusted environments and back." +optional = false +python-versions = ">=3.8" +files = [ + {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, + {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, +] + [[package]] name = "jinja2" version = "3.1.4" @@ -910,6 +1162,22 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "jinjax" +version = "0.43" +description = "Replace your HTML templates with Python server-Side components" +optional = false +python-versions = "<4.0,>=3.10" +files = [ + {file = "jinjax-0.43-py3-none-any.whl", hash = "sha256:1b96442d1d3cce1151f1f547151894d212eecccab470d7925513cb9460a595ec"}, + {file = "jinjax-0.43.tar.gz", hash = "sha256:b7265bdcfbbcfdcdbf7297a694eb1f3193029c5b51532d33eafbb3aecd02adb8"}, +] + +[package.dependencies] +jinja2 = ">=3.0" +markupsafe = ">=2.0" +whitenoise = ">=5.3" + [[package]] name = "jmespath" version = "1.0.1" @@ -921,6 +1189,31 @@ files = [ {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, ] +[[package]] +name = "jsbeautifier" +version = "1.15.1" +description = "JavaScript unobfuscator and beautifier." +optional = false +python-versions = "*" +files = [ + {file = "jsbeautifier-1.15.1.tar.gz", hash = "sha256:ebd733b560704c602d744eafc839db60a1ee9326e30a2a80c4adb8718adc1b24"}, +] + +[package.dependencies] +editorconfig = ">=0.12.2" +six = ">=1.13.0" + +[[package]] +name = "json5" +version = "0.9.25" +description = "A Python implementation of the JSON5 data format." +optional = false +python-versions = ">=3.8" +files = [ + {file = "json5-0.9.25-py3-none-any.whl", hash = "sha256:34ed7d834b1341a86987ed52f3f76cd8ee184394906b6e22a1e0deb9ab294e8f"}, + {file = "json5-0.9.25.tar.gz", hash = "sha256:548e41b9be043f9426776f05df8635a00fe06104ea51ed24b67f908856e151ae"}, +] + [[package]] name = "lazy-model" version = "0.2.0" @@ -977,6 +1270,23 @@ profiling = ["gprof2dot"] rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] +[[package]] +name = "markdown2" +version = "2.5.0" +description = "A fast and complete Python implementation of Markdown" +optional = false +python-versions = "<4,>=3.8" +files = [ + {file = "markdown2-2.5.0-py2.py3-none-any.whl", hash = "sha256:300d4429b620ebc974ef512339a9e08bc080473f95135a91f33906e24e8280c1"}, + {file = "markdown2-2.5.0.tar.gz", hash = "sha256:9bff02911f8b617b61eb269c4c1a5f9b2087d7ff051604f66a61b63cab30adc2"}, +] + +[package.extras] +all = ["latex2mathml", "pygments (>=2.7.3)", "wavedrom"] +code-syntax-highlighting = ["pygments (>=2.7.3)"] +latex = ["latex2mathml"] +wavedrom = ["wavedrom"] + [[package]] name = "markupsafe" version = "2.1.5" @@ -1059,13 +1369,13 @@ files = [ [[package]] name = "more-itertools" -version = "10.3.0" +version = "10.4.0" description = "More routines for operating on iterables, beyond itertools" optional = false python-versions = ">=3.8" files = [ - {file = "more-itertools-10.3.0.tar.gz", hash = "sha256:e5d93ef411224fbcef366a6e8ddc4c5781bc6359d43412a65dd5964e46111463"}, - {file = "more_itertools-10.3.0-py3-none-any.whl", hash = "sha256:ea6a02e24a9161e51faad17a8782b92a0df82c12c1c8886fec7f0c3fa1a1b320"}, + {file = "more-itertools-10.4.0.tar.gz", hash = "sha256:fe0e63c4ab068eac62410ab05cccca2dc71ec44ba8ef29916a0090df061cf923"}, + {file = "more_itertools-10.4.0-py3-none-any.whl", hash = "sha256:0f7d9f83a0a8dcfa8a2694a770590d98a67ea943e3d9f5298309a484758c4e27"}, ] [[package]] @@ -1193,43 +1503,43 @@ files = [ [[package]] name = "mypy" -version = "1.10.1" +version = "1.11.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e36f229acfe250dc660790840916eb49726c928e8ce10fbdf90715090fe4ae02"}, - {file = "mypy-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:51a46974340baaa4145363b9e051812a2446cf583dfaeba124af966fa44593f7"}, - {file = "mypy-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:901c89c2d67bba57aaaca91ccdb659aa3a312de67f23b9dfb059727cce2e2e0a"}, - {file = "mypy-1.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0cd62192a4a32b77ceb31272d9e74d23cd88c8060c34d1d3622db3267679a5d9"}, - {file = "mypy-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a2cbc68cb9e943ac0814c13e2452d2046c2f2b23ff0278e26599224cf164e78d"}, - {file = "mypy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bd6f629b67bb43dc0d9211ee98b96d8dabc97b1ad38b9b25f5e4c4d7569a0c6a"}, - {file = "mypy-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1bbb3a6f5ff319d2b9d40b4080d46cd639abe3516d5a62c070cf0114a457d84"}, - {file = "mypy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8edd4e9bbbc9d7b79502eb9592cab808585516ae1bcc1446eb9122656c6066f"}, - {file = "mypy-1.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6166a88b15f1759f94a46fa474c7b1b05d134b1b61fca627dd7335454cc9aa6b"}, - {file = "mypy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:5bb9cd11c01c8606a9d0b83ffa91d0b236a0e91bc4126d9ba9ce62906ada868e"}, - {file = "mypy-1.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d8681909f7b44d0b7b86e653ca152d6dff0eb5eb41694e163c6092124f8246d7"}, - {file = "mypy-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:378c03f53f10bbdd55ca94e46ec3ba255279706a6aacaecac52ad248f98205d3"}, - {file = "mypy-1.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bacf8f3a3d7d849f40ca6caea5c055122efe70e81480c8328ad29c55c69e93e"}, - {file = "mypy-1.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:701b5f71413f1e9855566a34d6e9d12624e9e0a8818a5704d74d6b0402e66c04"}, - {file = "mypy-1.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c4c2992f6ea46ff7fce0072642cfb62af7a2484efe69017ed8b095f7b39ef31"}, - {file = "mypy-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:604282c886497645ffb87b8f35a57ec773a4a2721161e709a4422c1636ddde5c"}, - {file = "mypy-1.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37fd87cab83f09842653f08de066ee68f1182b9b5282e4634cdb4b407266bade"}, - {file = "mypy-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8addf6313777dbb92e9564c5d32ec122bf2c6c39d683ea64de6a1fd98b90fe37"}, - {file = "mypy-1.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cc3ca0a244eb9a5249c7c583ad9a7e881aa5d7b73c35652296ddcdb33b2b9c7"}, - {file = "mypy-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:1b3a2ffce52cc4dbaeee4df762f20a2905aa171ef157b82192f2e2f368eec05d"}, - {file = "mypy-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe85ed6836165d52ae8b88f99527d3d1b2362e0cb90b005409b8bed90e9059b3"}, - {file = "mypy-1.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2ae450d60d7d020d67ab440c6e3fae375809988119817214440033f26ddf7bf"}, - {file = "mypy-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6be84c06e6abd72f960ba9a71561c14137a583093ffcf9bbfaf5e613d63fa531"}, - {file = "mypy-1.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2189ff1e39db399f08205e22a797383613ce1cb0cb3b13d8bcf0170e45b96cc3"}, - {file = "mypy-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:97a131ee36ac37ce9581f4220311247ab6cba896b4395b9c87af0675a13a755f"}, - {file = "mypy-1.10.1-py3-none-any.whl", hash = "sha256:71d8ac0b906354ebda8ef1673e5fde785936ac1f29ff6987c7483cfbd5a4235a"}, - {file = "mypy-1.10.1.tar.gz", hash = "sha256:1f8f492d7db9e3593ef42d4f115f04e556130f2819ad33ab84551403e97dd4c0"}, + {file = "mypy-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a32fc80b63de4b5b3e65f4be82b4cfa362a46702672aa6a0f443b4689af7008c"}, + {file = "mypy-1.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1952f5ea8a5a959b05ed5f16452fddadbaae48b5d39235ab4c3fc444d5fd411"}, + {file = "mypy-1.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1e30dc3bfa4e157e53c1d17a0dad20f89dc433393e7702b813c10e200843b03"}, + {file = "mypy-1.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c63350af88f43a66d3dfeeeb8d77af34a4f07d760b9eb3a8697f0386c7590b4"}, + {file = "mypy-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:a831671bad47186603872a3abc19634f3011d7f83b083762c942442d51c58d58"}, + {file = "mypy-1.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7b6343d338390bb946d449677726edf60102a1c96079b4f002dedff375953fc5"}, + {file = "mypy-1.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4fe9f4e5e521b458d8feb52547f4bade7ef8c93238dfb5bbc790d9ff2d770ca"}, + {file = "mypy-1.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:886c9dbecc87b9516eff294541bf7f3655722bf22bb898ee06985cd7269898de"}, + {file = "mypy-1.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fca4a60e1dd9fd0193ae0067eaeeb962f2d79e0d9f0f66223a0682f26ffcc809"}, + {file = "mypy-1.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:0bd53faf56de9643336aeea1c925012837432b5faf1701ccca7fde70166ccf72"}, + {file = "mypy-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8"}, + {file = "mypy-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a"}, + {file = "mypy-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417"}, + {file = "mypy-1.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e"}, + {file = "mypy-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525"}, + {file = "mypy-1.11.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:749fd3213916f1751fff995fccf20c6195cae941dc968f3aaadf9bb4e430e5a2"}, + {file = "mypy-1.11.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b639dce63a0b19085213ec5fdd8cffd1d81988f47a2dec7100e93564f3e8fb3b"}, + {file = "mypy-1.11.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c956b49c5d865394d62941b109728c5c596a415e9c5b2be663dd26a1ff07bc0"}, + {file = "mypy-1.11.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45df906e8b6804ef4b666af29a87ad9f5921aad091c79cc38e12198e220beabd"}, + {file = "mypy-1.11.1-cp38-cp38-win_amd64.whl", hash = "sha256:d44be7551689d9d47b7abc27c71257adfdb53f03880841a5db15ddb22dc63edb"}, + {file = "mypy-1.11.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2684d3f693073ab89d76da8e3921883019ea8a3ec20fa5d8ecca6a2db4c54bbe"}, + {file = "mypy-1.11.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79c07eb282cb457473add5052b63925e5cc97dfab9812ee65a7c7ab5e3cb551c"}, + {file = "mypy-1.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11965c2f571ded6239977b14deebd3f4c3abd9a92398712d6da3a772974fad69"}, + {file = "mypy-1.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a2b43895a0f8154df6519706d9bca8280cda52d3d9d1514b2d9c3e26792a0b74"}, + {file = "mypy-1.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:1a81cf05975fd61aec5ae16501a091cfb9f605dc3e3c878c0da32f250b74760b"}, + {file = "mypy-1.11.1-py3-none-any.whl", hash = "sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54"}, + {file = "mypy-1.11.1.tar.gz", hash = "sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08"}, ] [package.dependencies] mypy-extensions = ">=1.0.0" -typing-extensions = ">=4.1.0" +typing-extensions = ">=4.6.0" [package.extras] dmypy = ["psutil (>=4.0)"] @@ -1261,58 +1571,74 @@ files = [ [[package]] name = "numpy" -version = "2.0.0" +version = "2.0.1" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.9" files = [ - {file = "numpy-2.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:04494f6ec467ccb5369d1808570ae55f6ed9b5809d7f035059000a37b8d7e86f"}, - {file = "numpy-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2635dbd200c2d6faf2ef9a0d04f0ecc6b13b3cad54f7c67c61155138835515d2"}, - {file = "numpy-2.0.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:0a43f0974d501842866cc83471bdb0116ba0dffdbaac33ec05e6afed5b615238"}, - {file = "numpy-2.0.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:8d83bb187fb647643bd56e1ae43f273c7f4dbcdf94550d7938cfc32566756514"}, - {file = "numpy-2.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79e843d186c8fb1b102bef3e2bc35ef81160ffef3194646a7fdd6a73c6b97196"}, - {file = "numpy-2.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d7696c615765091cc5093f76fd1fa069870304beaccfd58b5dcc69e55ef49c1"}, - {file = "numpy-2.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b4c76e3d4c56f145d41b7b6751255feefae92edbc9a61e1758a98204200f30fc"}, - {file = "numpy-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:acd3a644e4807e73b4e1867b769fbf1ce8c5d80e7caaef0d90dcdc640dfc9787"}, - {file = "numpy-2.0.0-cp310-cp310-win32.whl", hash = "sha256:cee6cc0584f71adefe2c908856ccc98702baf95ff80092e4ca46061538a2ba98"}, - {file = "numpy-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:ed08d2703b5972ec736451b818c2eb9da80d66c3e84aed1deeb0c345fefe461b"}, - {file = "numpy-2.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad0c86f3455fbd0de6c31a3056eb822fc939f81b1618f10ff3406971893b62a5"}, - {file = "numpy-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7f387600d424f91576af20518334df3d97bc76a300a755f9a8d6e4f5cadd289"}, - {file = "numpy-2.0.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:34f003cb88b1ba38cb9a9a4a3161c1604973d7f9d5552c38bc2f04f829536609"}, - {file = "numpy-2.0.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:b6f6a8f45d0313db07d6d1d37bd0b112f887e1369758a5419c0370ba915b3871"}, - {file = "numpy-2.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f64641b42b2429f56ee08b4f427a4d2daf916ec59686061de751a55aafa22e4"}, - {file = "numpy-2.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7039a136017eaa92c1848152827e1424701532ca8e8967fe480fe1569dae581"}, - {file = "numpy-2.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:46e161722e0f619749d1cd892167039015b2c2817296104487cd03ed4a955995"}, - {file = "numpy-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0e50842b2295ba8414c8c1d9d957083d5dfe9e16828b37de883f51fc53c4016f"}, - {file = "numpy-2.0.0-cp311-cp311-win32.whl", hash = "sha256:2ce46fd0b8a0c947ae047d222f7136fc4d55538741373107574271bc00e20e8f"}, - {file = "numpy-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd6acc766814ea6443628f4e6751d0da6593dae29c08c0b2606164db026970c"}, - {file = "numpy-2.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:354f373279768fa5a584bac997de6a6c9bc535c482592d7a813bb0c09be6c76f"}, - {file = "numpy-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d2f62e55a4cd9c58c1d9a1c9edaedcd857a73cb6fda875bf79093f9d9086f85"}, - {file = "numpy-2.0.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:1e72728e7501a450288fc8e1f9ebc73d90cfd4671ebbd631f3e7857c39bd16f2"}, - {file = "numpy-2.0.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:84554fc53daa8f6abf8e8a66e076aff6ece62de68523d9f665f32d2fc50fd66e"}, - {file = "numpy-2.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c73aafd1afca80afecb22718f8700b40ac7cab927b8abab3c3e337d70e10e5a2"}, - {file = "numpy-2.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49d9f7d256fbc804391a7f72d4a617302b1afac1112fac19b6c6cec63fe7fe8a"}, - {file = "numpy-2.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0ec84b9ba0654f3b962802edc91424331f423dcf5d5f926676e0150789cb3d95"}, - {file = "numpy-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:feff59f27338135776f6d4e2ec7aeeac5d5f7a08a83e80869121ef8164b74af9"}, - {file = "numpy-2.0.0-cp312-cp312-win32.whl", hash = "sha256:c5a59996dc61835133b56a32ebe4ef3740ea5bc19b3983ac60cc32be5a665d54"}, - {file = "numpy-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:a356364941fb0593bb899a1076b92dfa2029f6f5b8ba88a14fd0984aaf76d0df"}, - {file = "numpy-2.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e61155fae27570692ad1d327e81c6cf27d535a5d7ef97648a17d922224b216de"}, - {file = "numpy-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4554eb96f0fd263041baf16cf0881b3f5dafae7a59b1049acb9540c4d57bc8cb"}, - {file = "numpy-2.0.0-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:903703372d46bce88b6920a0cd86c3ad82dae2dbef157b5fc01b70ea1cfc430f"}, - {file = "numpy-2.0.0-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:3e8e01233d57639b2e30966c63d36fcea099d17c53bf424d77f088b0f4babd86"}, - {file = "numpy-2.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cde1753efe513705a0c6d28f5884e22bdc30438bf0085c5c486cdaff40cd67a"}, - {file = "numpy-2.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821eedb7165ead9eebdb569986968b541f9908979c2da8a4967ecac4439bae3d"}, - {file = "numpy-2.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9a1712c015831da583b21c5bfe15e8684137097969c6d22e8316ba66b5baabe4"}, - {file = "numpy-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9c27f0946a3536403efb0e1c28def1ae6730a72cd0d5878db38824855e3afc44"}, - {file = "numpy-2.0.0-cp39-cp39-win32.whl", hash = "sha256:63b92c512d9dbcc37f9d81b123dec99fdb318ba38c8059afc78086fe73820275"}, - {file = "numpy-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:3f6bed7f840d44c08ebdb73b1825282b801799e325bcbdfa6bc5c370e5aecc65"}, - {file = "numpy-2.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9416a5c2e92ace094e9f0082c5fd473502c91651fb896bc17690d6fc475128d6"}, - {file = "numpy-2.0.0-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:17067d097ed036636fa79f6a869ac26df7db1ba22039d962422506640314933a"}, - {file = "numpy-2.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ecb5b0582cd125f67a629072fed6f83562d9dd04d7e03256c9829bdec027ad"}, - {file = "numpy-2.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cef04d068f5fb0518a77857953193b6bb94809a806bd0a14983a8f12ada060c9"}, - {file = "numpy-2.0.0.tar.gz", hash = "sha256:cf5d1c9e6837f8af9f92b6bd3e86d513cdc11f60fd62185cc49ec7d1aba34864"}, + {file = "numpy-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fbb536eac80e27a2793ffd787895242b7f18ef792563d742c2d673bfcb75134"}, + {file = "numpy-2.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:69ff563d43c69b1baba77af455dd0a839df8d25e8590e79c90fcbe1499ebde42"}, + {file = "numpy-2.0.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:1b902ce0e0a5bb7704556a217c4f63a7974f8f43e090aff03fcf262e0b135e02"}, + {file = "numpy-2.0.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:f1659887361a7151f89e79b276ed8dff3d75877df906328f14d8bb40bb4f5101"}, + {file = "numpy-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4658c398d65d1b25e1760de3157011a80375da861709abd7cef3bad65d6543f9"}, + {file = "numpy-2.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4127d4303b9ac9f94ca0441138acead39928938660ca58329fe156f84b9f3015"}, + {file = "numpy-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e5eeca8067ad04bc8a2a8731183d51d7cbaac66d86085d5f4766ee6bf19c7f87"}, + {file = "numpy-2.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9adbd9bb520c866e1bfd7e10e1880a1f7749f1f6e5017686a5fbb9b72cf69f82"}, + {file = "numpy-2.0.1-cp310-cp310-win32.whl", hash = "sha256:7b9853803278db3bdcc6cd5beca37815b133e9e77ff3d4733c247414e78eb8d1"}, + {file = "numpy-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:81b0893a39bc5b865b8bf89e9ad7807e16717f19868e9d234bdaf9b1f1393868"}, + {file = "numpy-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75b4e316c5902d8163ef9d423b1c3f2f6252226d1aa5cd8a0a03a7d01ffc6268"}, + {file = "numpy-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6e4eeb6eb2fced786e32e6d8df9e755ce5be920d17f7ce00bc38fcde8ccdbf9e"}, + {file = "numpy-2.0.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1e01dcaab205fbece13c1410253a9eea1b1c9b61d237b6fa59bcc46e8e89343"}, + {file = "numpy-2.0.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:a8fc2de81ad835d999113ddf87d1ea2b0f4704cbd947c948d2f5513deafe5a7b"}, + {file = "numpy-2.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a3d94942c331dd4e0e1147f7a8699a4aa47dffc11bf8a1523c12af8b2e91bbe"}, + {file = "numpy-2.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15eb4eca47d36ec3f78cde0a3a2ee24cf05ca7396ef808dda2c0ddad7c2bde67"}, + {file = "numpy-2.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b83e16a5511d1b1f8a88cbabb1a6f6a499f82c062a4251892d9ad5d609863fb7"}, + {file = "numpy-2.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f87fec1f9bc1efd23f4227becff04bd0e979e23ca50cc92ec88b38489db3b55"}, + {file = "numpy-2.0.1-cp311-cp311-win32.whl", hash = "sha256:36d3a9405fd7c511804dc56fc32974fa5533bdeb3cd1604d6b8ff1d292b819c4"}, + {file = "numpy-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:08458fbf403bff5e2b45f08eda195d4b0c9b35682311da5a5a0a0925b11b9bd8"}, + {file = "numpy-2.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6bf4e6f4a2a2e26655717a1983ef6324f2664d7011f6ef7482e8c0b3d51e82ac"}, + {file = "numpy-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6fddc5fe258d3328cd8e3d7d3e02234c5d70e01ebe377a6ab92adb14039cb4"}, + {file = "numpy-2.0.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5daab361be6ddeb299a918a7c0864fa8618af66019138263247af405018b04e1"}, + {file = "numpy-2.0.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:ea2326a4dca88e4a274ba3a4405eb6c6467d3ffbd8c7d38632502eaae3820587"}, + {file = "numpy-2.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529af13c5f4b7a932fb0e1911d3a75da204eff023ee5e0e79c1751564221a5c8"}, + {file = "numpy-2.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6790654cb13eab303d8402354fabd47472b24635700f631f041bd0b65e37298a"}, + {file = "numpy-2.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cbab9fc9c391700e3e1287666dfd82d8666d10e69a6c4a09ab97574c0b7ee0a7"}, + {file = "numpy-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99d0d92a5e3613c33a5f01db206a33f8fdf3d71f2912b0de1739894668b7a93b"}, + {file = "numpy-2.0.1-cp312-cp312-win32.whl", hash = "sha256:173a00b9995f73b79eb0191129f2455f1e34c203f559dd118636858cc452a1bf"}, + {file = "numpy-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:bb2124fdc6e62baae159ebcfa368708867eb56806804d005860b6007388df171"}, + {file = "numpy-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bfc085b28d62ff4009364e7ca34b80a9a080cbd97c2c0630bb5f7f770dae9414"}, + {file = "numpy-2.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8fae4ebbf95a179c1156fab0b142b74e4ba4204c87bde8d3d8b6f9c34c5825ef"}, + {file = "numpy-2.0.1-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:72dc22e9ec8f6eaa206deb1b1355eb2e253899d7347f5e2fae5f0af613741d06"}, + {file = "numpy-2.0.1-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:ec87f5f8aca726117a1c9b7083e7656a9d0d606eec7299cc067bb83d26f16e0c"}, + {file = "numpy-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f682ea61a88479d9498bf2091fdcd722b090724b08b31d63e022adc063bad59"}, + {file = "numpy-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8efc84f01c1cd7e34b3fb310183e72fcdf55293ee736d679b6d35b35d80bba26"}, + {file = "numpy-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3fdabe3e2a52bc4eff8dc7a5044342f8bd9f11ef0934fcd3289a788c0eb10018"}, + {file = "numpy-2.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:24a0e1befbfa14615b49ba9659d3d8818a0f4d8a1c5822af8696706fbda7310c"}, + {file = "numpy-2.0.1-cp39-cp39-win32.whl", hash = "sha256:f9cf5ea551aec449206954b075db819f52adc1638d46a6738253a712d553c7b4"}, + {file = "numpy-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:e9e81fa9017eaa416c056e5d9e71be93d05e2c3c2ab308d23307a8bc4443c368"}, + {file = "numpy-2.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:61728fba1e464f789b11deb78a57805c70b2ed02343560456190d0501ba37b0f"}, + {file = "numpy-2.0.1-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:12f5d865d60fb9734e60a60f1d5afa6d962d8d4467c120a1c0cda6eb2964437d"}, + {file = "numpy-2.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eacf3291e263d5a67d8c1a581a8ebbcfd6447204ef58828caf69a5e3e8c75990"}, + {file = "numpy-2.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2c3a346ae20cfd80b6cfd3e60dc179963ef2ea58da5ec074fd3d9e7a1e7ba97f"}, + {file = "numpy-2.0.1.tar.gz", hash = "sha256:485b87235796410c3519a699cfe1faab097e509e90ebb05dcd098db2ae87e7b3"}, +] + +[[package]] +name = "oauthlib" +version = "3.2.2" +description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +optional = false +python-versions = ">=3.6" +files = [ + {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, + {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, ] +[package.extras] +rsa = ["cryptography (>=3.0.0)"] +signals = ["blinker (>=1.4.0)"] +signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] + [[package]] name = "packaging" version = "24.1" @@ -1348,13 +1674,13 @@ files = [ [[package]] name = "pdoc" -version = "14.5.1" +version = "14.6.0" description = "API Documentation for Python Projects" optional = false python-versions = ">=3.8" files = [ - {file = "pdoc-14.5.1-py3-none-any.whl", hash = "sha256:fda6365a06e438b43ca72235b58a2e2ecd66445fcc444313f6ebbde4b0abd94b"}, - {file = "pdoc-14.5.1.tar.gz", hash = "sha256:4ddd9c5123a79f511cedffd7231bf91a6e0bd0968610f768342ec5d00b5eefee"}, + {file = "pdoc-14.6.0-py3-none-any.whl", hash = "sha256:36c42c546a317d8e3e8c0b39645f24161374de0c7066ccaae76628d721e49ba5"}, + {file = "pdoc-14.6.0.tar.gz", hash = "sha256:6e98a24c5e0ca5d188397969cf82581836eaef13f172fc3820047bfe15c61c9a"}, ] [package.dependencies] @@ -1451,13 +1777,13 @@ files = [ [[package]] name = "pre-commit" -version = "3.7.1" +version = "3.8.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" files = [ - {file = "pre_commit-3.7.1-py2.py3-none-any.whl", hash = "sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5"}, - {file = "pre_commit-3.7.1.tar.gz", hash = "sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a"}, + {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, + {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, ] [package.dependencies] @@ -1467,6 +1793,17 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" +[[package]] +name = "priority" +version = "2.0.0" +description = "A pure-Python implementation of the HTTP/2 priority tree" +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "priority-2.0.0-py3-none-any.whl", hash = "sha256:6f8eefce5f3ad59baf2c080a664037bb4725cd0a790d53d59ab4059288faf6aa"}, + {file = "priority-2.0.0.tar.gz", hash = "sha256:c965d54f1b8d0d0b19479db3924c7c36cf672dbf2aec92d43fbdaf4492ba18c0"}, +] + [[package]] name = "prompt-toolkit" version = "3.0.36" @@ -1666,13 +2003,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyjwt" -version = "2.8.0" +version = "2.9.0" description = "JSON Web Token implementation in Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"}, - {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"}, + {file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"}, + {file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"}, ] [package.dependencies] @@ -1680,8 +2017,8 @@ cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"cryp [package.extras] crypto = ["cryptography (>=3.4.0)"] -dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] -docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] [[package]] @@ -1784,33 +2121,33 @@ tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] [[package]] name = "pytest" -version = "8.2.2" +version = "8.3.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, - {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, + {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, + {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} iniconfig = "*" packaging = "*" -pluggy = ">=1.5,<2.0" +pluggy = ">=1.5,<2" [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-asyncio" -version = "0.23.7" +version = "0.23.8" description = "Pytest support for asyncio" optional = false python-versions = ">=3.8" files = [ - {file = "pytest_asyncio-0.23.7-py3-none-any.whl", hash = "sha256:009b48127fbe44518a547bddd25611551b0e43ccdbf1e67d12479f569832c20b"}, - {file = "pytest_asyncio-0.23.7.tar.gz", hash = "sha256:5f5c72948f4c49e7db4f29f2521d4031f1c27f86e57b046126654083d4770268"}, + {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"}, + {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"}, ] [package.dependencies] @@ -1949,53 +2286,155 @@ files = [ [[package]] name = "pyyaml" -version = "6.0.1" +version = "6.0.2" description = "YAML parser and emitter for Python" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "quart" +version = "0.19.6" +description = "A Python ASGI web microframework with the same API as Flask" +optional = false +python-versions = ">=3.8" +files = [ + {file = "quart-0.19.6-py3-none-any.whl", hash = "sha256:f9092310f4eb120903da692a5e4354f05d48c28ca7ec3054d3d94dd862412c58"}, + {file = "quart-0.19.6.tar.gz", hash = "sha256:89ddda6da24300a5ea4f21e4582d5e89bc8ea678e724e0b747767143401e4558"}, +] + +[package.dependencies] +aiofiles = "*" +blinker = ">=1.6" +click = ">=8.0.0" +flask = ">=3.0.0" +hypercorn = ">=0.11.2" +itsdangerous = "*" +jinja2 = "*" +markupsafe = "*" +werkzeug = ">=3.0.0" + +[package.extras] +docs = ["pydata_sphinx_theme"] +dotenv = ["python-dotenv"] + +[[package]] +name = "quart-flask-patch" +version = "0.3.0" +description = "Quart-Flask-Patch is a Quart extension that patches Quart to work with Flask extensions." +optional = false +python-versions = "*" +files = [ + {file = "quart_flask_patch-0.3.0-py2.py3-none-any.whl", hash = "sha256:b5a1d97a8edee9f40082db278fb491687d712af9bc8eb774830ea6f92790b990"}, + {file = "quart_flask_patch-0.3.0.tar.gz", hash = "sha256:81dc073698d54ffe1d8c85a556881c13215923a920f9300a02b5005d5447268d"}, +] + +[package.dependencies] +quart = ">=0.19" + +[[package]] +name = "quart-session" +version = "3.0.0" +description = "Adds server-side session support to your Quart application" +optional = false +python-versions = "*" +files = [ + {file = "Quart-Session-3.0.0.tar.gz", hash = "sha256:b9c466c705773f13141b210ac6b20381b667c6a518e7aa49bb134cdde80f3bb6"}, +] + +[package.dependencies] +Quart = ">=0.19.0" + +[package.extras] +dotenv = ["python-dotenv"] +mongodb = ["motor (>=2.5.1)"] +redis = ["redis (>=4.4.0)"] + +[[package]] +name = "quart-uploads" +version = "0.0.2" +description = "" +optional = false +python-versions = ">=3.8" 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"}, + {file = "quart_uploads-0.0.2-py3-none-any.whl", hash = "sha256:46ca8d8676545da5ebae926a171d5c19e87e5ab46be7c4421c64971d2eda5b83"}, + {file = "quart_uploads-0.0.2.tar.gz", hash = "sha256:c23167a3bcda987128805d0ca42725d753404a4587a588043d0bb2fa9464b3a9"}, ] +[package.dependencies] +aiofiles = "*" +quart = ">=0.19,<0.20" + +[[package]] +name = "quart-wtforms" +version = "1.0.1" +description = "Simple Integration of Quart and WTForms." +optional = false +python-versions = ">=3.8" +files = [ + {file = "quart_wtforms-1.0.1-py3-none-any.whl", hash = "sha256:3c956d9b08463af4f829c1b0707990d2913c42194b82318239a76ac161149a2b"}, + {file = "quart_wtforms-1.0.1.tar.gz", hash = "sha256:550a66016864ef92384c959900db5858b08e141cbf603fee67739f3460e6f86d"}, +] + +[package.dependencies] +quart = ">=0.19,<0.20" +quart-uploads = ">=0.0.2,<0.0.3" +WTForms = ">=3.0,<4.0" + [[package]] name = "questionary" version = "2.0.1" @@ -2010,6 +2449,126 @@ files = [ [package.dependencies] prompt_toolkit = ">=2.0,<=3.0.36" +[[package]] +name = "redis" +version = "5.0.8" +description = "Python client for Redis database and key-value store" +optional = false +python-versions = ">=3.7" +files = [ + {file = "redis-5.0.8-py3-none-any.whl", hash = "sha256:56134ee08ea909106090934adc36f65c9bcbbaecea5b21ba704ba6fb561f8eb4"}, + {file = "redis-5.0.8.tar.gz", hash = "sha256:0c5b10d387568dfe0698c6fad6615750c24170e548ca2deac10c649d463e9870"}, +] + +[package.dependencies] +async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""} + +[package.extras] +hiredis = ["hiredis (>1.0.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] + +[[package]] +name = "regex" +version = "2023.12.25" +description = "Alternative regular expression module, to replace re." +optional = false +python-versions = ">=3.7" +files = [ + {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0694219a1d54336fd0445ea382d49d36882415c0134ee1e8332afd1529f0baa5"}, + {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b014333bd0217ad3d54c143de9d4b9a3ca1c5a29a6d0d554952ea071cff0f1f8"}, + {file = "regex-2023.12.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d865984b3f71f6d0af64d0d88f5733521698f6c16f445bb09ce746c92c97c586"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e0eabac536b4cc7f57a5f3d095bfa557860ab912f25965e08fe1545e2ed8b4c"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c25a8ad70e716f96e13a637802813f65d8a6760ef48672aa3502f4c24ea8b400"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9b6d73353f777630626f403b0652055ebfe8ff142a44ec2cf18ae470395766e"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9cc99d6946d750eb75827cb53c4371b8b0fe89c733a94b1573c9dd16ea6c9e4"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88d1f7bef20c721359d8675f7d9f8e414ec5003d8f642fdfd8087777ff7f94b5"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cb3fe77aec8f1995611f966d0c656fdce398317f850d0e6e7aebdfe61f40e1cd"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7aa47c2e9ea33a4a2a05f40fcd3ea36d73853a2aae7b4feab6fc85f8bf2c9704"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:df26481f0c7a3f8739fecb3e81bc9da3fcfae34d6c094563b9d4670b047312e1"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c40281f7d70baf6e0db0c2f7472b31609f5bc2748fe7275ea65a0b4601d9b392"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:d94a1db462d5690ebf6ae86d11c5e420042b9898af5dcf278bd97d6bda065423"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ba1b30765a55acf15dce3f364e4928b80858fa8f979ad41f862358939bdd1f2f"}, + {file = "regex-2023.12.25-cp310-cp310-win32.whl", hash = "sha256:150c39f5b964e4d7dba46a7962a088fbc91f06e606f023ce57bb347a3b2d4630"}, + {file = "regex-2023.12.25-cp310-cp310-win_amd64.whl", hash = "sha256:09da66917262d9481c719599116c7dc0c321ffcec4b1f510c4f8a066f8768105"}, + {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1b9d811f72210fa9306aeb88385b8f8bcef0dfbf3873410413c00aa94c56c2b6"}, + {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d902a43085a308cef32c0d3aea962524b725403fd9373dea18110904003bac97"}, + {file = "regex-2023.12.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d166eafc19f4718df38887b2bbe1467a4f74a9830e8605089ea7a30dd4da8887"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7ad32824b7f02bb3c9f80306d405a1d9b7bb89362d68b3c5a9be53836caebdb"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:636ba0a77de609d6510235b7f0e77ec494d2657108f777e8765efc060094c98c"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fda75704357805eb953a3ee15a2b240694a9a514548cd49b3c5124b4e2ad01b"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f72cbae7f6b01591f90814250e636065850c5926751af02bb48da94dfced7baa"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db2a0b1857f18b11e3b0e54ddfefc96af46b0896fb678c85f63fb8c37518b3e7"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7502534e55c7c36c0978c91ba6f61703faf7ce733715ca48f499d3dbbd7657e0"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e8c7e08bb566de4faaf11984af13f6bcf6a08f327b13631d41d62592681d24fe"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:283fc8eed679758de38fe493b7d7d84a198b558942b03f017b1f94dda8efae80"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f44dd4d68697559d007462b0a3a1d9acd61d97072b71f6d1968daef26bc744bd"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:67d3ccfc590e5e7197750fcb3a2915b416a53e2de847a728cfa60141054123d4"}, + {file = "regex-2023.12.25-cp311-cp311-win32.whl", hash = "sha256:68191f80a9bad283432385961d9efe09d783bcd36ed35a60fb1ff3f1ec2efe87"}, + {file = "regex-2023.12.25-cp311-cp311-win_amd64.whl", hash = "sha256:7d2af3f6b8419661a0c421584cfe8aaec1c0e435ce7e47ee2a97e344b98f794f"}, + {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8a0ccf52bb37d1a700375a6b395bff5dd15c50acb745f7db30415bae3c2b0715"}, + {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c3c4a78615b7762740531c27cf46e2f388d8d727d0c0c739e72048beb26c8a9d"}, + {file = "regex-2023.12.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ad83e7545b4ab69216cef4cc47e344d19622e28aabec61574b20257c65466d6a"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7a635871143661feccce3979e1727c4e094f2bdfd3ec4b90dfd4f16f571a87a"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d498eea3f581fbe1b34b59c697512a8baef88212f92e4c7830fcc1499f5b45a5"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:43f7cd5754d02a56ae4ebb91b33461dc67be8e3e0153f593c509e21d219c5060"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51f4b32f793812714fd5307222a7f77e739b9bc566dc94a18126aba3b92b98a3"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba99d8077424501b9616b43a2d208095746fb1284fc5ba490139651f971d39d9"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4bfc2b16e3ba8850e0e262467275dd4d62f0d045e0e9eda2bc65078c0110a11f"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8c2c19dae8a3eb0ea45a8448356ed561be843b13cbc34b840922ddf565498c1c"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:60080bb3d8617d96f0fb7e19796384cc2467447ef1c491694850ebd3670bc457"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b77e27b79448e34c2c51c09836033056a0547aa360c45eeeb67803da7b0eedaf"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:518440c991f514331f4850a63560321f833979d145d7d81186dbe2f19e27ae3d"}, + {file = "regex-2023.12.25-cp312-cp312-win32.whl", hash = "sha256:e2610e9406d3b0073636a3a2e80db05a02f0c3169b5632022b4e81c0364bcda5"}, + {file = "regex-2023.12.25-cp312-cp312-win_amd64.whl", hash = "sha256:cc37b9aeebab425f11f27e5e9e6cf580be7206c6582a64467a14dda211abc232"}, + {file = "regex-2023.12.25-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:da695d75ac97cb1cd725adac136d25ca687da4536154cdc2815f576e4da11c69"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d126361607b33c4eb7b36debc173bf25d7805847346dd4d99b5499e1fef52bc7"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4719bb05094d7d8563a450cf8738d2e1061420f79cfcc1fa7f0a44744c4d8f73"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5dd58946bce44b53b06d94aa95560d0b243eb2fe64227cba50017a8d8b3cd3e2"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22a86d9fff2009302c440b9d799ef2fe322416d2d58fc124b926aa89365ec482"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2aae8101919e8aa05ecfe6322b278f41ce2994c4a430303c4cd163fef746e04f"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e692296c4cc2873967771345a876bcfc1c547e8dd695c6b89342488b0ea55cd8"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:263ef5cc10979837f243950637fffb06e8daed7f1ac1e39d5910fd29929e489a"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d6f7e255e5fa94642a0724e35406e6cb7001c09d476ab5fce002f652b36d0c39"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:88ad44e220e22b63b0f8f81f007e8abbb92874d8ced66f32571ef8beb0643b2b"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:3a17d3ede18f9cedcbe23d2daa8a2cd6f59fe2bf082c567e43083bba3fb00347"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d15b274f9e15b1a0b7a45d2ac86d1f634d983ca40d6b886721626c47a400bf39"}, + {file = "regex-2023.12.25-cp37-cp37m-win32.whl", hash = "sha256:ed19b3a05ae0c97dd8f75a5d8f21f7723a8c33bbc555da6bbe1f96c470139d3c"}, + {file = "regex-2023.12.25-cp37-cp37m-win_amd64.whl", hash = "sha256:a6d1047952c0b8104a1d371f88f4ab62e6275567d4458c1e26e9627ad489b445"}, + {file = "regex-2023.12.25-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b43523d7bc2abd757119dbfb38af91b5735eea45537ec6ec3a5ec3f9562a1c53"}, + {file = "regex-2023.12.25-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:efb2d82f33b2212898f1659fb1c2e9ac30493ac41e4d53123da374c3b5541e64"}, + {file = "regex-2023.12.25-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b7fca9205b59c1a3d5031f7e64ed627a1074730a51c2a80e97653e3e9fa0d415"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086dd15e9435b393ae06f96ab69ab2d333f5d65cbe65ca5a3ef0ec9564dfe770"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e81469f7d01efed9b53740aedd26085f20d49da65f9c1f41e822a33992cb1590"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34e4af5b27232f68042aa40a91c3b9bb4da0eeb31b7632e0091afc4310afe6cb"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9852b76ab558e45b20bf1893b59af64a28bd3820b0c2efc80e0a70a4a3ea51c1"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff100b203092af77d1a5a7abe085b3506b7eaaf9abf65b73b7d6905b6cb76988"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cc038b2d8b1470364b1888a98fd22d616fba2b6309c5b5f181ad4483e0017861"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:094ba386bb5c01e54e14434d4caabf6583334090865b23ef58e0424a6286d3dc"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5cd05d0f57846d8ba4b71d9c00f6f37d6b97d5e5ef8b3c3840426a475c8f70f4"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:9aa1a67bbf0f957bbe096375887b2505f5d8ae16bf04488e8b0f334c36e31360"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:98a2636994f943b871786c9e82bfe7883ecdaba2ef5df54e1450fa9869d1f756"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:37f8e93a81fc5e5bd8db7e10e62dc64261bcd88f8d7e6640aaebe9bc180d9ce2"}, + {file = "regex-2023.12.25-cp38-cp38-win32.whl", hash = "sha256:d78bd484930c1da2b9679290a41cdb25cc127d783768a0369d6b449e72f88beb"}, + {file = "regex-2023.12.25-cp38-cp38-win_amd64.whl", hash = "sha256:b521dcecebc5b978b447f0f69b5b7f3840eac454862270406a39837ffae4e697"}, + {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f7bc09bc9c29ebead055bcba136a67378f03d66bf359e87d0f7c759d6d4ffa31"}, + {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e14b73607d6231f3cc4622809c196b540a6a44e903bcfad940779c80dffa7be7"}, + {file = "regex-2023.12.25-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9eda5f7a50141291beda3edd00abc2d4a5b16c29c92daf8d5bd76934150f3edc"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc6bb9aa69aacf0f6032c307da718f61a40cf970849e471254e0e91c56ffca95"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:298dc6354d414bc921581be85695d18912bea163a8b23cac9a2562bbcd5088b1"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f4e475a80ecbd15896a976aa0b386c5525d0ed34d5c600b6d3ebac0a67c7ddf"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:531ac6cf22b53e0696f8e1d56ce2396311254eb806111ddd3922c9d937151dae"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22f3470f7524b6da61e2020672df2f3063676aff444db1daa283c2ea4ed259d6"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:89723d2112697feaa320c9d351e5f5e7b841e83f8b143dba8e2d2b5f04e10923"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0ecf44ddf9171cd7566ef1768047f6e66975788258b1c6c6ca78098b95cf9a3d"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:905466ad1702ed4acfd67a902af50b8db1feeb9781436372261808df7a2a7bca"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:4558410b7a5607a645e9804a3e9dd509af12fb72b9825b13791a37cd417d73a5"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7e316026cc1095f2a3e8cc012822c99f413b702eaa2ca5408a513609488cb62f"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3b1de218d5375cd6ac4b5493e0b9f3df2be331e86520f23382f216c137913d20"}, + {file = "regex-2023.12.25-cp39-cp39-win32.whl", hash = "sha256:11a963f8e25ab5c61348d090bf1b07f1953929c13bd2309a0662e9ff680763c9"}, + {file = "regex-2023.12.25-cp39-cp39-win_amd64.whl", hash = "sha256:e693e233ac92ba83a87024e1d32b5f9ab15ca55ddd916d878146f4e3406b5c91"}, + {file = "regex-2023.12.25.tar.gz", hash = "sha256:29171aa128da69afdf4bde412d5bedc335f2ca8fcfe4489038577d05f16181e5"}, +] + [[package]] name = "requests" version = "2.32.3" @@ -2031,6 +2590,24 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +description = "OAuthlib authentication support for Requests." +optional = false +python-versions = ">=3.4" +files = [ + {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, + {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, +] + +[package.dependencies] +oauthlib = ">=3.0.0" +requests = ">=2.0.0" + +[package.extras] +rsa = ["oauthlib[signedtoken] (>=3.0.0)"] + [[package]] name = "rich" version = "13.7.1" @@ -2051,29 +2628,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.5.1" +version = "0.6.1" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.5.1-py3-none-linux_armv6l.whl", hash = "sha256:6ecf968fcf94d942d42b700af18ede94b07521bd188aaf2cd7bc898dd8cb63b6"}, - {file = "ruff-0.5.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:204fb0a472f00f2e6280a7c8c7c066e11e20e23a37557d63045bf27a616ba61c"}, - {file = "ruff-0.5.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d235968460e8758d1e1297e1de59a38d94102f60cafb4d5382033c324404ee9d"}, - {file = "ruff-0.5.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38beace10b8d5f9b6bdc91619310af6d63dd2019f3fb2d17a2da26360d7962fa"}, - {file = "ruff-0.5.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e478d2f09cf06add143cf8c4540ef77b6599191e0c50ed976582f06e588c994"}, - {file = "ruff-0.5.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0368d765eec8247b8550251c49ebb20554cc4e812f383ff9f5bf0d5d94190b0"}, - {file = "ruff-0.5.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3a9a9a1b582e37669b0138b7c1d9d60b9edac880b80eb2baba6d0e566bdeca4d"}, - {file = "ruff-0.5.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bdd9f723e16003623423affabcc0a807a66552ee6a29f90eddad87a40c750b78"}, - {file = "ruff-0.5.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be9fd62c1e99539da05fcdc1e90d20f74aec1b7a1613463ed77870057cd6bd96"}, - {file = "ruff-0.5.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e216fc75a80ea1fbd96af94a6233d90190d5b65cc3d5dfacf2bd48c3e067d3e1"}, - {file = "ruff-0.5.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c4c2112e9883a40967827d5c24803525145e7dab315497fae149764979ac7929"}, - {file = "ruff-0.5.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dfaf11c8a116394da3b65cd4b36de30d8552fa45b8119b9ef5ca6638ab964fa3"}, - {file = "ruff-0.5.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d7ceb9b2fe700ee09a0c6b192c5ef03c56eb82a0514218d8ff700f6ade004108"}, - {file = "ruff-0.5.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bac6288e82f6296f82ed5285f597713acb2a6ae26618ffc6b429c597b392535c"}, - {file = "ruff-0.5.1-py3-none-win32.whl", hash = "sha256:5c441d9c24ec09e1cb190a04535c5379b36b73c4bc20aa180c54812c27d1cca4"}, - {file = "ruff-0.5.1-py3-none-win_amd64.whl", hash = "sha256:b1789bf2cd3d1b5a7d38397cac1398ddf3ad7f73f4de01b1e913e2abc7dfc51d"}, - {file = "ruff-0.5.1-py3-none-win_arm64.whl", hash = "sha256:2875b7596a740cbbd492f32d24be73e545a4ce0a3daf51e4f4e609962bfd3cd2"}, - {file = "ruff-0.5.1.tar.gz", hash = "sha256:3164488aebd89b1745b47fd00604fb4358d774465f20d1fcd907f9c0fc1b0655"}, + {file = "ruff-0.6.1-py3-none-linux_armv6l.whl", hash = "sha256:b4bb7de6a24169dc023f992718a9417380301b0c2da0fe85919f47264fb8add9"}, + {file = "ruff-0.6.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:45efaae53b360c81043e311cdec8a7696420b3d3e8935202c2846e7a97d4edae"}, + {file = "ruff-0.6.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:bc60c7d71b732c8fa73cf995efc0c836a2fd8b9810e115be8babb24ae87e0850"}, + {file = "ruff-0.6.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c7477c3b9da822e2db0b4e0b59e61b8a23e87886e727b327e7dcaf06213c5cf"}, + {file = "ruff-0.6.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a0af7ab3f86e3dc9f157a928e08e26c4b40707d0612b01cd577cc84b8905cc9"}, + {file = "ruff-0.6.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:392688dbb50fecf1bf7126731c90c11a9df1c3a4cdc3f481b53e851da5634fa5"}, + {file = "ruff-0.6.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5278d3e095ccc8c30430bcc9bc550f778790acc211865520f3041910a28d0024"}, + {file = "ruff-0.6.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe6d5f65d6f276ee7a0fc50a0cecaccb362d30ef98a110f99cac1c7872df2f18"}, + {file = "ruff-0.6.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2e0dd11e2ae553ee5c92a81731d88a9883af8db7408db47fc81887c1f8b672e"}, + {file = "ruff-0.6.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d812615525a34ecfc07fd93f906ef5b93656be01dfae9a819e31caa6cfe758a1"}, + {file = "ruff-0.6.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:faaa4060f4064c3b7aaaa27328080c932fa142786f8142aff095b42b6a2eb631"}, + {file = "ruff-0.6.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:99d7ae0df47c62729d58765c593ea54c2546d5de213f2af2a19442d50a10cec9"}, + {file = "ruff-0.6.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9eb18dfd7b613eec000e3738b3f0e4398bf0153cb80bfa3e351b3c1c2f6d7b15"}, + {file = "ruff-0.6.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c62bc04c6723a81e25e71715aa59489f15034d69bf641df88cb38bdc32fd1dbb"}, + {file = "ruff-0.6.1-py3-none-win32.whl", hash = "sha256:9fb4c4e8b83f19c9477a8745e56d2eeef07a7ff50b68a6998f7d9e2e3887bdc4"}, + {file = "ruff-0.6.1-py3-none-win_amd64.whl", hash = "sha256:c2ebfc8f51ef4aca05dad4552bbcf6fe8d1f75b2f6af546cc47cc1c1ca916b5b"}, + {file = "ruff-0.6.1-py3-none-win_arm64.whl", hash = "sha256:3bc81074971b0ffad1bd0c52284b22411f02a11a012082a76ac6da153536e014"}, + {file = "ruff-0.6.1.tar.gz", hash = "sha256:af3ffd8c6563acb8848d33cd19a69b9bfe943667f0419ca083f8ebe4224a3436"}, ] [[package]] @@ -2177,15 +2754,35 @@ files = [ [[package]] name = "tomlkit" -version = "0.13.0" +version = "0.13.2" description = "Style preserving TOML library" optional = false python-versions = ">=3.8" files = [ - {file = "tomlkit-0.13.0-py3-none-any.whl", hash = "sha256:7075d3042d03b80f603482d69bf0c8f345c2b30e41699fd8883227f89972b264"}, - {file = "tomlkit-0.13.0.tar.gz", hash = "sha256:08ad192699734149f5b97b45f1f18dad7eb1b6d16bc72ad0c2335772650d7b72"}, + {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, + {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, ] +[[package]] +name = "tqdm" +version = "4.66.5" +description = "Fast, Extensible Progress Meter" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tqdm-4.66.5-py3-none-any.whl", hash = "sha256:90279a3770753eafc9194a0364852159802111925aa30eb3f9d85b0e805ac7cd"}, + {file = "tqdm-4.66.5.tar.gz", hash = "sha256:e1020aef2e5096702d8a025ac7d16b1577279c9d63f8375b63083e9a5f0fcbad"}, +] + +[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 = "typeguard" version = "4.3.0" @@ -2256,21 +2853,21 @@ files = [ [[package]] name = "typos" -version = "1.23.2" +version = "1.23.6" description = "Source Code Spelling Correction" optional = false python-versions = ">=3.7" files = [ - {file = "typos-1.23.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b1549225f26cbcb6640e87999f20496287751428c71a7650e6afe3143e39112f"}, - {file = "typos-1.23.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:290c6628b60f570999dfcdcf0ce5c90f195cceba160a1b16316eebd5c68129f2"}, - {file = "typos-1.23.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0461d8e33c9ba51518203ef59df19b0945293626ce620652650927f3e3b1a9af"}, - {file = "typos-1.23.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2873d83a562725c841e6ee3ee03865ebcee7a1b5add57ca7a88578cb752c7f4a"}, - {file = "typos-1.23.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16ea2a308f731729711b660e0c68753047e6b9937d8d97bd6e1d1c274303ad36"}, - {file = "typos-1.23.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2dcd035661c1a45688fd0ffb14fd242bf8907996e12a8cbf8166bec0a6b360e5"}, - {file = "typos-1.23.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:38da8651809e86de5cd338a68552688ab10fb45056d86c1f90a36f7911d29bd4"}, - {file = "typos-1.23.2-py3-none-win32.whl", hash = "sha256:de20d68507126f2577c7dca88ec8d52364e8c519218d72791edcfe256622948a"}, - {file = "typos-1.23.2-py3-none-win_amd64.whl", hash = "sha256:85f0877de4c4024fc846a08ece5a8f56dd85ab69d42d23608c29783e1d6899ed"}, - {file = "typos-1.23.2.tar.gz", hash = "sha256:2a7b0c3523140f1c32ed91e46171a925d3748735648381cec4f6b992217d4167"}, + {file = "typos-1.23.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9209947ab1e815bcb8cb781fc73fd6ad88eacdea7b1c15e73ca49217fa7c44e7"}, + {file = "typos-1.23.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b049bfce407d7d61c5be4955d2fae6db644dc5d56ca236224cae0c3978024a75"}, + {file = "typos-1.23.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0b17e19c5e6b4f46acf0f60d053e0c188d31c09748f487f171465623f5f3380"}, + {file = "typos-1.23.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b609d525078b222cf8e25bd8e5cd60a56a542129d7bccb4f6cc992f686410331"}, + {file = "typos-1.23.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fbf955dc4a09a95d3358f8edb10c1418e45bf07a6c9c414432320009a74dd5f"}, + {file = "typos-1.23.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c686b06039b7fd95eed661cd2093fa7f048c76cb40b6bad55827a68aa707240a"}, + {file = "typos-1.23.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0fda8c8502bce101277eb0a4b4d04847fc7018e2f9cff6d2fc86b3fdec239755"}, + {file = "typos-1.23.6-py3-none-win32.whl", hash = "sha256:8edaba24813be7ef678868e8ed49c48eb70cf128afc41ae86cc2127fb32e326b"}, + {file = "typos-1.23.6-py3-none-win_amd64.whl", hash = "sha256:d47b7d0e08975adf67873a8e43dc09fc1b6ff655a4241497348808ee54442668"}, + {file = "typos-1.23.6.tar.gz", hash = "sha256:2691988d2a15cde2cdd4f2fa5fd32880765b2a68ed6ccd48d6dc693c44447bcf"}, ] [[package]] @@ -2321,6 +2918,37 @@ files = [ {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, ] +[[package]] +name = "werkzeug" +version = "3.0.3" +description = "The comprehensive WSGI web application library." +optional = false +python-versions = ">=3.8" +files = [ + {file = "werkzeug-3.0.3-py3-none-any.whl", hash = "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8"}, + {file = "werkzeug-3.0.3.tar.gz", hash = "sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18"}, +] + +[package.dependencies] +MarkupSafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog (>=2.3)"] + +[[package]] +name = "whitenoise" +version = "6.7.0" +description = "Radically simplified static file serving for WSGI applications" +optional = false +python-versions = ">=3.8" +files = [ + {file = "whitenoise-6.7.0-py3-none-any.whl", hash = "sha256:a1ae85e01fdc9815d12fa33f17765bc132ed2c54fa76daf9e39e879dd93566f6"}, + {file = "whitenoise-6.7.0.tar.gz", hash = "sha256:58c7a6cd811e275a6c91af22e96e87da0b1109e9a53bb7464116ef4c963bf636"}, +] + +[package.extras] +brotli = ["brotli"] + [[package]] name = "win32-setctime" version = "1.1.0" @@ -2414,6 +3042,37 @@ files = [ {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, ] +[[package]] +name = "wsproto" +version = "1.2.0" +description = "WebSockets state-machine based protocol implementation" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, + {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, +] + +[package.dependencies] +h11 = ">=0.9.0,<1" + +[[package]] +name = "wtforms" +version = "3.1.2" +description = "Form validation and rendering for Python web development." +optional = false +python-versions = ">=3.8" +files = [ + {file = "wtforms-3.1.2-py3-none-any.whl", hash = "sha256:bf831c042829c8cdbad74c27575098d541d039b1faa74c771545ecac916f2c07"}, + {file = "wtforms-3.1.2.tar.gz", hash = "sha256:f8d76180d7239c94c6322f7990ae1216dae3659b7aa1cee94b6318bdffb474b9"}, +] + +[package.dependencies] +markupsafe = "*" + +[package.extras] +email = ["email-validator"] + [[package]] name = "yamllint" version = "1.35.1" @@ -2535,22 +3194,7 @@ files = [ idna = ">=2.0" multidict = ">=4.0" -[[package]] -name = "zipp" -version = "3.19.2" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.8" -files = [ - {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"}, - {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"}, -] - -[package.extras] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] - [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.13" -content-hash = "fb63e14724b5fcc00bfce1f0d403cdf518207e033754a3dafb6509d569417e91" +content-hash = "04ddc91403dc15f010809e03514ca40312369eb05cd391b55043eefd4e3819f5" diff --git a/pyproject.toml b/pyproject.toml index 311e163f..5cf4c89d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,24 +15,34 @@ [tool.poetry.scripts] # https://python-poetry.org/docs/pyproject/#scripts valentina = "valentina.main:app" + webui = "valentina.debug_webui:dev" [tool.poetry.dependencies] - aiofiles = "^24.1.0" - arrow = "^1.3.0" - beanie = "^1.25.0" - boto3 = "^1.34.142" - confz = "^2.0.1" - faker = "^26.0.0" - inflect = "^7.3.1" - loguru = "^0.7.2" - numpy = "^2.0.0" - py-cord = "^2.5.0" - pydantic = "^2.8.2" - pygithub = "^2.3.0" - python = ">=3.11,<3.13" - rich = "^13.7.1" - semver = "^3.0.2" - typer = "^0.12.3" + aiofiles = "^24.1.0" + arrow = "^1.3.0" + beanie = "^1.25.0" + boto3 = "^1.34.142" + confz = "^2.0.1" + faker = "^26.0.0" + flask-discord = "^0.1.69" + inflect = "^7.3.1" + jinjax = "^0.43" + loguru = "^0.7.2" + markdown2 = "^2.5.0" + markupsafe = "^2.1.5" + numpy = "^2.0.0" + py-cord = "^2.5.0" + pydantic = "^2.8.2" + pygithub = "^2.3.0" + python = ">=3.11,<3.13" + quart = "^0.19.6" + quart-flask-patch = "^0.3.0" + quart-session = "^3.0.0" + quart-wtforms = "^1.0.1" + redis = "^5.0.8" + rich = "^13.7.1" + semver = "^3.0.2" + typer = "^0.12.3" [tool.poetry.group.test.dependencies] dirty-equals = "^0.7.1.post0" @@ -48,11 +58,12 @@ [tool.poetry.group.dev.dependencies] commitizen = "^3.27.0" coverage = "^7.5.4" + djlint = "^1.34.1" mypy = "^1.10.1" pdoc = "^14.4.0" poethepoet = "^0.26.1" pre-commit = "^3.7.1" - ruff = ">=0.5.1" + ruff = "^0.6.0" shellcheck-py = "^0.9.0.6" types-aiofiles = "^23.2.0.0" typos = "^1.23.2" @@ -113,6 +124,11 @@ [tool.coverage.xml] output = "reports/coverage.xml" +[tool.djlint] + extend_exclude = ".vscode, .github, .git, .ruff_cache, .pytest_cache, __pycache__, .mypy_cache, .venv, tests, src/valentina/cogs, src/valentina/models, src/valentina/utils, src/valentina/views, tests" + ignore = "H030,H031,H021,H006,H013" + # ignore_case = true + use_gitignore = true [tool.mypy] # https://mypy.readthedocs.io/en/latest/config_file.html disallow_any_unimported = false disallow_subclassing_any = false @@ -186,47 +202,48 @@ target-version = "py311" [tool.ruff.lint] ignore = [ - "ANN001", # Missing type annotation for function argument `cls` - "ANN002", # Missing type annotation for `*args` - "ANN003", # Missing type annotation for `**kwargs` - "ANN101", # missing-type-self - "ANN204", # Missing return type annotation for special method `__init__` - "ANN401", # Dynamically typed expressions (typing.Any) are disallowed, - "B006", # mutable-argument-default - "B008", # function-call-in-default-argument - "COM812", # Trailing comma missing" - "CPY001", # Missing copyright notice at top of file - "D107", # undocumented-public-init + "ANN001", # Missing type annotation for function argument `cls` + "ANN002", # Missing type annotation for `*args` + "ANN003", # Missing type annotation for `**kwargs` + "ANN101", # missing-type-self + "ANN204", # Missing return type annotation for special method `__init__` + "ANN401", # Dynamically typed expressions (typing.Any) are disallowed, + "ASYNC110", # Use `anyio.Event` instead of awaiting `anyio.sleep` in a `while` loop + "B006", # mutable-argument-default + "B008", # function-call-in-default-argument + "COM812", # Trailing comma missing" + "CPY001", # Missing copyright notice at top of file + "D107", # undocumented-public-init "D203", "D204", "D213", "D215", "D404", - "D406", # new-line-after-section-name - "D407", # dashed-underline-after-section - "D408", # section-underline-after-name - "D409", # section-underline-matches-section-length - "D413", # blank-line-after-last-section - "DTZ", # TODO: Fix naive datetime usage + "D406", # new-line-after-section-name + "D407", # dashed-underline-after-section + "D408", # section-underline-after-name + "D409", # section-underline-matches-section-length + "D413", # blank-line-after-last-section + "DTZ", # TODO: Fix naive datetime usage "E266", "E501", - "FBT001", # Boolean-typed positional argument in function definition - "FBT002", # Boolean-typed positional argument in function definition - "FIX002", # Line contains TODO, consider resolving the issue" - "ISC001", # single-line-implicit-string-concatenation - "N805", # invalid-first-argument-name-for-method - "PD011", # Use `.to_numpy()` instead of `.values`" + "FBT001", # Boolean-typed positional argument in function definition + "FBT002", # Boolean-typed positional argument in function definition + "FIX002", # Line contains TODO, consider resolving the issue" + "ISC001", # single-line-implicit-string-concatenation + "N805", # invalid-first-argument-name-for-method + "PD011", # Use `.to_numpy()` instead of `.values`" "PGH003", - "PIE796", # Enum contains duplicate value - "PLR0913", # too-many-arguments + "PIE796", # Enum contains duplicate value + "PLR0913", # too-many-arguments "PLR0917", - "RSE102", # Unnecessary parentheses on raised exception" + "RSE102", # Unnecessary parentheses on raised exception" "S307", - "S311", # suspicious-non-cryptographic-random-usage - "S603", #`subprocess` call: check for execution of untrusted input - "TD002", # Missing author in TODO - "TD003", # Missing issue link on the line following this TODO - "UP007", # non-pep604-annotation + "S311", # suspicious-non-cryptographic-random-usage + "S603", #`subprocess` call: check for execution of untrusted input + "TD002", # Missing author in TODO + "TD003", # Missing issue link on the line following this TODO + "UP007", # non-pep604-annotation ] per-file-ignores = { "main.py" = [ diff --git a/src/valentina/characters/add_from_sheet.py b/src/valentina/characters/add_from_sheet.py index ca6a45ed..9421402f 100644 --- a/src/valentina/characters/add_from_sheet.py +++ b/src/valentina/characters/add_from_sheet.py @@ -245,7 +245,7 @@ async def __send_messages( ) else: # Subsequent sends, edit the interaction of the DM - await interaction.response.edit_message(embed=embed, view=self.view) # type: ignore [union-attr] + await interaction.response.edit_message(embed=embed, view=self.view) async def __timeout(self) -> None: """Inform the user they took too long.""" diff --git a/src/valentina/cogs/misc.py b/src/valentina/cogs/misc.py index dc81855b..85b73bf5 100644 --- a/src/valentina/cogs/misc.py +++ b/src/valentina/cogs/misc.py @@ -166,7 +166,7 @@ async def probability( pool (int): The number of dice to roll """ probabilities = Probability( - ctx, pool=pool, difficulty=difficulty, dice_size=DiceType.D10.value + ctx=ctx, pool=pool, difficulty=difficulty, dice_size=DiceType.D10.value ) embed = await probabilities.get_embed() await ctx.respond(embed=embed, ephemeral=hidden) diff --git a/src/valentina/debug_webui.py b/src/valentina/debug_webui.py new file mode 100644 index 00000000..be7a7c9c --- /dev/null +++ b/src/valentina/debug_webui.py @@ -0,0 +1,37 @@ +"""Start webui without Discord bot for debugging and development.""" + +import asyncio + +import pymongo +from loguru import logger + +from valentina.utils import ValentinaConfig, instantiate_logger +from valentina.utils.database import init_database +from valentina.webui import create_dev_app + +# async def run_webserver() -> None: +# pass + + +async def create_db_pool() -> None: + """Initialize the database connection pool.""" + while True: + try: + await init_database() + except pymongo.errors.ServerSelectionTimeoutError as e: + logger.error(f"DB: Failed to initialize database: {e}") + await asyncio.sleep(60) + else: + break + + +def dev() -> None: + """Run the web server for development.""" + instantiate_logger() + app = create_dev_app() + app.run( + host=ValentinaConfig().webui_host, + port=int(ValentinaConfig().webui_port), + debug=True, + use_reloader=True, + ) diff --git a/src/valentina/models/bot.py b/src/valentina/models/bot.py index 7d85d4a7..bee4a77d 100644 --- a/src/valentina/models/bot.py +++ b/src/valentina/models/bot.py @@ -62,7 +62,6 @@ def log_command(self, msg: str, level: LogLevel = LogLevel.INFO) -> None: # pra name2 = inspect.stack()[2].filename.split("/")[-2].split(".")[0] name3 = inspect.stack()[2].filename.split("/")[-1].split(".")[0] new_name = f"{name1}.{name2}.{name3}" - else: name1 = inspect.stack()[1].filename.split("/")[-3].split(".")[0] name2 = inspect.stack()[1].filename.split("/")[-2].split(".")[0] @@ -522,14 +521,20 @@ async def _provision_guild(guild: discord.Guild) -> None: for member in guild.members: if not member.bot: logger.debug(f"DATABASE: Update user `{member.name}`") + user = await User.find_one(User.id == member.id).upsert( Set( { "date_modified": datetime.now(UTC).replace(microsecond=0), "name": member.display_name, + "avatar_url": str(member.display_avatar.url), } ), - on_insert=User(id=member.id, name=member.display_name), + on_insert=User( + id=member.id, + name=member.display_name, + avatar_url=str(member.display_avatar.url), + ), response_type=UpdateResponse.NEW_DOCUMENT, ) if guild.id not in user.guilds: @@ -559,7 +564,7 @@ async def on_ready(self) -> None: await asyncio.sleep(10) # Needed for computing uptime - self.start_time = datetime.utcnow() + self.start_time = datetime.now(UTC) if not self.welcomed: await self.change_presence( @@ -590,6 +595,11 @@ async def on_ready(self) -> None: self.welcomed = True logger.info(f"{self.user} is ready") + if ValentinaConfig().webui_enable: + from valentina.webui import run_webserver + + await run_webserver() + # Define a custom application context class async def get_application_context( # type: ignore self, interaction: discord.Interaction, cls=ValentinaContext diff --git a/src/valentina/models/character.py b/src/valentina/models/character.py index 47ea135b..b011a948 100644 --- a/src/valentina/models/character.py +++ b/src/valentina/models/character.py @@ -107,9 +107,9 @@ class Character(Document): campaign: str | None = None # id of the character's campaign # Profile - bio: str | None = None age: int | None = None auspice: str | None = None + bio: str | None = None breed: str | None = None clan_name: str | None = None # VampireClan enum name concept_name: str | None = None # CharacterConcept enum name @@ -439,3 +439,86 @@ async def update_channel_permissions( permissions_user_post=owned_by_user, topic=f"Character channel for {self.name}", ) + + def sheet_section_top_items(self) -> dict[str, str]: + """Return the items to populate the top portion of a character sheet. + + Populate a dictionary with attributes that are present in the character + and return the dictionary with properly titled values. + + Returns: + dict[str, str]: Dictionary with character attributes. + """ + attributes = [ + ("Class", self.char_class_name if self.char_class_name else "-"), + ("Concept", self.concept_name), + ("Demeanor", self.demeanor if self.demeanor else "-"), + ("Nature", self.nature if self.nature else "-"), + ( + "Auspice", + self.auspice + if self.auspice + else "-" + if self.char_class_name.lower() == "werewolf" + else None, + ), + ( + "Breed", + self.breed + if self.breed + else "-" + if self.char_class_name.lower() == "werewolf" + else None, + ), + ( + "Clan", + self.clan_name + if self.clan_name + else "-" + if self.char_class_name.lower() == "vampire" + else None, + ), + ( + "Creed", + self.creed_name + if self.creed_name + else "-" + if self.char_class_name.lower() == "hunter" + else None, + ), + ("Essence", self.essence), + ( + "Generation", + self.generation + if self.generation + else "-" + if self.char_class_name.lower() == "vampire" + else None, + ), + ( + "Sire", + self.sire + if self.sire + else "-" + if self.char_class_name.lower() == "vampire" + else None, + ), + ("Tradition", self.tradition), + ( + "Tribe", + self.tribe + if self.tribe + else "-" + if self.char_class_name.lower() == "werewolf" + else None, + ), + ("Date of Birth", self.dob.strftime("%Y-%m-%d") if self.dob else "-"), + ("Age", str(self.age)), + ] + + # Create dictionary using a comprehension with conditional inclusion + return { + name: str(value).title().replace("_", " ") + for name, value in attributes + if value and value != "None" + } diff --git a/src/valentina/models/dicerolls.py b/src/valentina/models/dicerolls.py index 150ff675..bcf7d1d3 100644 --- a/src/valentina/models/dicerolls.py +++ b/src/valentina/models/dicerolls.py @@ -1,6 +1,6 @@ """Models for dice rolls.""" -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional import inflect from loguru import logger @@ -8,6 +8,7 @@ from valentina.constants import MAX_POOL_SIZE, DiceType, EmbedColor, RollResultType from valentina.models import Campaign, Character, Guild, RollStatistic from valentina.utils import errors, random_num +from valentina.utils.helpers import convert_int_to_emoji p = inflect.engine() @@ -45,8 +46,8 @@ class DiceRoll: is_critical (bool): Whether the roll is a critical success. is_failure (bool): Whether the roll is a failure. is_success (bool): Whether the roll is a success. - embed_title (str): The title of the roll response embed. - embed_description (str): The description of the roll response embed. + roll_result_humanized (str): The result of the roll, humanized + num_successes_humanized (str): The number of successes, humanized pool (int): The pool's total size, including hunger. result (int): The number of successes after accounting for botches and cancelling ones and tens. result_type(RollResultType): The result type of the roll. @@ -56,29 +57,42 @@ class DiceRoll: def __init__( self, - ctx: "ValentinaContext", pool: int, + ctx: Optional["ValentinaContext"] = None, difficulty: int = 6, dice_size: int = 10, character: Character = None, desperation_pool: int = 0, campaign: Campaign = None, + guild_id: int | None = None, + author_id: int | None = None, + author_name: str | None = None, ) -> None: """A container class that determines the result of a roll. Args: - ctx (ValentinaContext): The context of the command. + author_id (int, optional): The author ID to log the roll for. Defaults to None. + author_name (str, optional): The author name to log the roll for. Defaults to None. + campaign (Campaign, optional): The campaign to log the roll for. Defaults to None. + character (Character, optional): The character to log the roll for. Defaults to None. + ctx (ValentinaContext, optional): The context of the command. + desperation_pool (int): The number of dice to roll from the desperation pool. Defaults to 0. dice_size (int, optional): The size of the dice. Defaults to 10. difficulty (int, optional): The difficulty of the roll. Defaults to 6. + guild_id (int, optional): The guild ID to log the roll for. Defaults to None. pool (int): The pool's total size, including hunger - character (Character, optional): The character to log the roll for. Defaults to None. - desperation_pool (int): The number of dice to roll from the desperation pool. Defaults to 0. - campaign (Campaign, optional): The campaign to log the roll for. Defaults to None. """ self.ctx = ctx self.character = character self.desperation_pool = desperation_pool self.campaign = campaign + self.guild_id = guild_id + self.author_id = author_id + self.author_name = author_name + + if not self.ctx and (not self.guild_id or not self.author_id or not self.author_name): + msg = "A context must be provided if guild_id, author_id, or author_name are not provided." + raise errors.ValidationError(msg) dice_size_values = [member.value for member in DiceType] if dice_size not in dice_size_values: @@ -102,6 +116,8 @@ def __init__( self.difficulty = difficulty self.pool = pool + + # Set property defaults self._roll: list[int] = None self._desperation_roll: list[int] = None self._botches: int = None @@ -111,6 +127,8 @@ def __init__( self._result: int = None self._result_type: RollResultType = None self._desperation_botches: int = None + self._dice_as_emoji_images: str = None + self._desperation_dice_as_emoji_images: str = None def _calculate_result(self) -> RollResultType: """Calculate the result type of the roll.""" @@ -139,8 +157,8 @@ async def log_roll(self, traits: list[str] = []) -> None: # Log the roll to the database if self.dice_type == DiceType.D10: stat = RollStatistic( - guild=self.ctx.guild.id, - user=self.ctx.author.id, + guild=self.guild_id or self.ctx.guild.id, + user=self.author_id or self.ctx.author.id, character=str(self.character.id) if self.character else None, result=self.result_type, pool=self.pool, @@ -151,7 +169,7 @@ async def log_roll(self, traits: list[str] = []) -> None: await stat.insert() logger.debug( - f"DICEROLL: {self.ctx.author.display_name} rolled {self.roll} for {self.result_type.name}" + f"DICEROLL: {self.author_name or self.ctx.author.display_name} rolled {self.roll} for {self.result_type.name}" ) @property @@ -166,7 +184,7 @@ def result_type(self) -> RollResultType: def roll(self) -> list[int]: """Roll the dice and return the results.""" if not self._roll: - self._roll = [random_num(self.dice_type.value) for x in range(self.pool)] + self._roll = [int(random_num(self.dice_type.value)) for x in range(self.pool)] return self._roll @@ -268,7 +286,7 @@ def result(self) -> int: async def thumbnail_url(self) -> str: # pragma: no cover """Determine the thumbnail to use for the Discord embed.""" - guild = await Guild.get(self.ctx.guild.id) + guild = await Guild.get(self.guild_id or self.ctx.guild.id) return await guild.fetch_diceroll_thumbnail(self.result_type) @property @@ -284,25 +302,43 @@ def embed_color(self) -> int: # pragma: no cover return color_map[self.result_type].value @property - def embed_title(self) -> str: # pragma: no cover - """The title of the roll response embed.""" + def roll_result_humanized(self) -> str: + """The humanized result of the dice roll. ie - "botch", "2 successes", etc.""" title_map = { RollResultType.OTHER: "Dice roll", - RollResultType.BOTCH: "__**BOTCH!**__", - RollResultType.CRITICAL: "__**CRITICAL SUCCESS!**__", - RollResultType.SUCCESS: f"**{self.result} {p.plural_noun('SUCCESS', self.result)}**", - RollResultType.FAILURE: f"**{self.result} {p.plural_noun('SUCCESS', self.result)}**", + RollResultType.BOTCH: "Botch!", + RollResultType.CRITICAL: "Critical Success!", + RollResultType.SUCCESS: "Success", + RollResultType.FAILURE: "Failure", } return title_map[self.result_type] @property - def embed_description(self) -> str: # pragma: no cover - """The description of the roll response embed.""" + def num_successes_humanized(self) -> str: + """The number of successes rolled written as `x successess`.""" description_map = { RollResultType.OTHER: "", RollResultType.BOTCH: f"{self.result} {p.plural_noun('Success', self.result)}", RollResultType.CRITICAL: f"{self.result} {p.plural_noun('Success', self.result)}", - RollResultType.SUCCESS: "", - RollResultType.FAILURE: "", + RollResultType.SUCCESS: f"{self.result} {p.plural_noun('SUCCESS', self.result)}", + RollResultType.FAILURE: f"{self.result} {p.plural_noun('SUCCESS', self.result)}", } return description_map[self.result_type] + + @property + def dice_as_emoji_images(self) -> str: + """Return the rolled dice as emoji images.""" + if not self._dice_as_emoji_images: + self._dice_as_emoji_images = " ".join( + f"{convert_int_to_emoji(die, images=True)}" for die in sorted(self.roll) + ) + return self._dice_as_emoji_images + + @property + def desperation_dice_as_emoji_images(self) -> str: + """Return the rolled desperation dice as emoji images.""" + if not self._desperation_dice_as_emoji_images: + self._desperation_dice_as_emoji_images = " ".join( + f"{convert_int_to_emoji(die, images=True)}" for die in sorted(self.desperation_roll) + ) + return self._desperation_dice_as_emoji_images diff --git a/src/valentina/models/errors.py b/src/valentina/models/errors.py index f188cedb..5786f782 100644 --- a/src/valentina/models/errors.py +++ b/src/valentina/models/errors.py @@ -88,6 +88,11 @@ def _handle_known_exceptions( # noqa: C901 log_msg = f"ERROR: Bot tried to run `/{ctx.command}` without the correct permissions" show_traceback = True + if isinstance(error, errors.NoCTXError): + user_msg = "Sorry, something went wrong. This has been reported." + log_msg = "ERROR: No context provided" + show_traceback = True + if isinstance(error, commands.BadArgument): user_msg = "Invalid argument provided" diff --git a/src/valentina/models/guild.py b/src/valentina/models/guild.py index 0403cd51..3ab39d53 100644 --- a/src/valentina/models/guild.py +++ b/src/valentina/models/guild.py @@ -204,7 +204,7 @@ async def fetch_diceroll_thumbnail(self, result: RollResultType) -> str: result (RollResultType): The roll result type. Returns: - Optional[str]: The thumbnail URL, or None if no thumbnail is found. + Optional[str]: The thumbnail URL, or None if no thumbnail is found. """ # Get the list of default thumbnails for the result type thumb_list = DICEROLL_THUMBS.get(result.name, []) diff --git a/src/valentina/models/probability.py b/src/valentina/models/probability.py index ceae78e4..fa0bce18 100644 --- a/src/valentina/models/probability.py +++ b/src/valentina/models/probability.py @@ -96,8 +96,8 @@ async def _calculate(self) -> RollProbability: for _ in range(self.trials): roll = DiceRoll( - self.ctx, pool=self.pool, + ctx=self.ctx, difficulty=self.difficulty, dice_size=self.dice_size, ) diff --git a/src/valentina/models/statistics.py b/src/valentina/models/statistics.py index f9f3b376..b8b6ebb1 100644 --- a/src/valentina/models/statistics.py +++ b/src/valentina/models/statistics.py @@ -8,7 +8,8 @@ from pydantic import Field from valentina.constants import EmbedColor, RollResultType -from valentina.models import Campaign, Character +from valentina.models import Campaign, Character, Guild +from valentina.utils import errors from valentina.utils.helpers import time_now if TYPE_CHECKING: @@ -34,9 +35,11 @@ class Statistics: def __init__( self, - ctx: "ValentinaContext", + ctx: "ValentinaContext" = None, + guild_id: int | None = None, ) -> None: self.ctx = ctx + self.guild_id = guild_id self.botches = 0 self.successes = 0 self.failures = 0 @@ -47,6 +50,46 @@ def __init__( self.title = "Roll Statistics" self.thumbnail = "" + @property + def criticals_percentage(self) -> str: + """Return the percentage of critical successes.""" + return f"{self.criticals / self.total_rolls * 100:.2f}" if self.total_rolls > 0 else "0" + + @property + def success_percentage(self) -> str: + """Return the percentage of successful rolls.""" + return f"{self.successes / self.total_rolls * 100:.2f}" if self.total_rolls > 0 else "0" + + @property + def failure_percentage(self) -> str: + """Return the percentage of failed rolls.""" + return f"{self.failures / self.total_rolls * 100:.2f}" if self.total_rolls > 0 else "0" + + @property + def botch_percentage(self) -> str: + """Return the percentage of botched rolls.""" + return f"{self.botches / self.total_rolls * 100:.2f}" if self.total_rolls > 0 else "0" + + def _get_json(self) -> dict[str, str]: + """Return a dictionary with the statistics. + + Returns: + dict: Dictionary with the statistics. + """ + return { + "total_rolls": str(self.total_rolls), + "criticals": str(self.criticals), + "criticals_percentage": self.criticals_percentage, + "successes": str(self.successes), + "successes_percentage": self.success_percentage, + "failures": str(self.failures), + "failures_percentage": self.failure_percentage, + "botches": str(self.botches), + "botches_percentage": self.botch_percentage, + "average_difficulty": str(self.average_difficulty), + "average_pool": str(self.average_pool), + } + def _get_text(self, with_title: bool = True, with_help: bool = True) -> str: """Return a string with the statistics. @@ -67,10 +110,10 @@ def _get_text(self, with_title: bool = True, with_help: bool = True) -> str: msg += f"""\ `Total Rolls {'.':.<{25 - 12}} {self.total_rolls}` -`Critical Success Rolls {'.':.<{25 - 23}} {self.criticals:<3} ({self.criticals / self.total_rolls * 100:.2f}%)` -`Successful Rolls {'.':.<{25 - 17}} {self.successes:<3} ({self.successes / self.total_rolls * 100:.2f}%)` -`Failed Rolls {'.':.<{25 - 13}} {self.failures:<3} ({self.failures / self.total_rolls * 100:.2f}%)` -`Botched Rolls {'.':.<{25 - 14}} {self.botches:<3} ({self.botches / self.total_rolls * 100:.2f}%)` +`Critical Success Rolls {'.':.<{25 - 23}} {self.criticals:<3} ({self.criticals_percentage}%)` +`Successful Rolls {'.':.<{25 - 17}} {self.successes:<3} ({self.success_percentage}%)` +`Failed Rolls {'.':.<{25 - 13}} {self.failures:<3} ({self.failure_percentage}%)` +`Botched Rolls {'.':.<{25 - 14}} {self.botches:<3} ({self.botch_percentage}%)` `Average Difficulty {'.':.<{25 - 19}} {self.average_difficulty}` `Average Pool Size {'.':.<{25 - 18}} {self.average_pool}` """ @@ -91,6 +134,10 @@ async def _get_embed(self, with_title: bool = True, with_help: bool = True) -> d Returns: discord.Embed: Embed with the statistics. """ + if not self.ctx: + msg = "No context provided." + raise errors.NoCTXError(msg) + embed = discord.Embed( title="", description=self._get_text(with_title=with_title, with_help=with_help), @@ -105,52 +152,67 @@ async def _get_embed(self, with_title: bool = True, with_help: bool = True) -> d return embed async def guild_statistics( - self, as_embed: bool = False, with_title: bool = True, with_help: bool = True - ) -> discord.Embed | str: + self, + as_embed: bool = False, + as_json: bool = False, + with_title: bool = True, + with_help: bool = True, + ) -> discord.Embed | str | dict[str, str]: """Compute and display guild statistics. Args: as_embed (bool, optional): Whether to return an embed. Defaults to False. When False, returns a string. + as_json (bool, optional): Whether to return a JSON object. Defaults to False. with_title (bool, optional): Whether to include the title. Defaults to True. with_help (bool, optional): Whether to include the help text. Defaults to True. Returns: discord.Embed | str: Embed or string with the statistics. """ - self.title = f"Roll statistics for guild `{self.ctx.guild.name}`" - self.thumbnail = self.ctx.guild.icon.url if self.ctx.guild.icon else "" + if not self.ctx and not self.guild_id: + msg = "No context or guild ID provided." + raise errors.NoCTXError(msg) + + guild_id = self.guild_id or self.ctx.guild.id + + if not self.ctx: + guild_object = await Guild.get(guild_id) + + self.title = ( + f"Roll statistics for guild `{self.ctx.guild.name}`" if self.ctx else guild_object.name + ) + if self.ctx: + self.thumbnail = self.ctx.guild.icon.url if self.ctx.guild.icon else "" # Grab the data from the database self.botches = await RollStatistic.find( - RollStatistic.guild == self.ctx.guild.id, + RollStatistic.guild == guild_id, RollStatistic.result == RollResultType.BOTCH, ).count() self.successes = await RollStatistic.find( - RollStatistic.guild == self.ctx.guild.id, + RollStatistic.guild == guild_id, RollStatistic.result == RollResultType.SUCCESS, ).count() self.criticals = await RollStatistic.find( - RollStatistic.guild == self.ctx.guild.id, + RollStatistic.guild == guild_id, RollStatistic.result == RollResultType.CRITICAL, ).count() self.failures = await RollStatistic.find( - RollStatistic.guild == self.ctx.guild.id, + RollStatistic.guild == guild_id, RollStatistic.result == RollResultType.FAILURE, ).count() self.other = await RollStatistic.find( - RollStatistic.guild == self.ctx.guild.id, + RollStatistic.guild == guild_id, RollStatistic.result == RollResultType.OTHER, ).count() - avg_diff = await RollStatistic.find(RollStatistic.guild == self.ctx.guild.id).avg( + avg_diff = await RollStatistic.find(RollStatistic.guild == guild_id).avg( RollStatistic.difficulty ) if avg_diff: self.average_difficulty = round(avg_diff) - avg_pool = await RollStatistic.find(RollStatistic.guild == self.ctx.guild.id).avg( - RollStatistic.pool - ) + avg_pool = await RollStatistic.find(RollStatistic.guild == guild_id).avg(RollStatistic.pool) if avg_pool: self.average_pool = round(avg_pool) @@ -162,28 +224,34 @@ async def guild_statistics( if as_embed: return await self._get_embed(with_title=with_title, with_help=with_help) + if as_json: + return self._get_json() + return self._get_text(with_title=with_title, with_help=with_help) async def user_statistics( self, user: discord.Member, as_embed: bool = False, + as_json: bool = False, with_title: bool = True, with_help: bool = True, - ) -> discord.Embed | str: + ) -> discord.Embed | str | dict[str, str]: """Compute and display user statistics. Args: user (discord.Member): The user to get statistics for. as_embed (bool, optional): Whether to return an embed. Defaults to False. When False, returns a string. + as_json (bool, optional): Whether to return a JSON object. Defaults to False. with_title (bool, optional): Whether to include the title. Defaults to True. with_help (bool, optional): Whether to include the help text. Defaults to True. Returns: discord.Embed | str: Embed or string with the statistics. """ - self.title = f"Roll statistics for @{user.display_name}" - self.thumbnail = user.display_avatar.url + if not as_json: + self.title = f"Roll statistics for @{user.display_name}" + self.thumbnail = user.display_avatar.url # Grab the data from the database self.botches = await RollStatistic.find( @@ -207,15 +275,13 @@ async def user_statistics( RollStatistic.result == RollResultType.OTHER, ).count() - avg_diff = await RollStatistic.find(RollStatistic.guild == self.ctx.guild.id).avg( + avg_diff = await RollStatistic.find(RollStatistic.user == user.id).avg( RollStatistic.difficulty ) if avg_diff: self.average_difficulty = round(avg_diff) - avg_pool = await RollStatistic.find(RollStatistic.guild == self.ctx.guild.id).avg( - RollStatistic.pool - ) + avg_pool = await RollStatistic.find(RollStatistic.user == user.id).avg(RollStatistic.pool) if avg_pool: self.average_pool = round(avg_pool) @@ -227,20 +293,25 @@ async def user_statistics( if as_embed: return await self._get_embed(with_title=with_title, with_help=with_help) + if as_json: + return self._get_json() + return self._get_text(with_title=with_title, with_help=with_help) async def character_statistics( self, character: Character, as_embed: bool = False, + as_json: bool = False, with_title: bool = True, with_help: bool = True, - ) -> discord.Embed | str: + ) -> discord.Embed | str | dict[str, str]: """Compute and display character statistics. Args: character (Character): The character to get statistics for. as_embed (bool, optional): Whether to return an embed. Defaults to False. When False, returns a string. + as_json (bool, optional): Whether to return a JSON object. Defaults to False. with_title (bool, optional): Whether to include the title. Defaults to True. with_help (bool, optional): Whether to include the help text. Defaults to True. @@ -271,13 +342,13 @@ async def character_statistics( RollStatistic.result == RollResultType.OTHER, ).count() - avg_diff = await RollStatistic.find(RollStatistic.guild == self.ctx.guild.id).avg( + avg_diff = await RollStatistic.find(RollStatistic.character == str(character.id)).avg( RollStatistic.difficulty ) if avg_diff: self.average_difficulty = round(avg_diff) - avg_pool = await RollStatistic.find(RollStatistic.guild == self.ctx.guild.id).avg( + avg_pool = await RollStatistic.find(RollStatistic.character == str(character.id)).avg( RollStatistic.pool ) if avg_pool: @@ -291,20 +362,25 @@ async def character_statistics( if as_embed: return await self._get_embed(with_title=with_title, with_help=with_help) + if as_json: + return self._get_json() + return self._get_text(with_title=with_title, with_help=with_help) async def campaign_statistics( self, campaign: Campaign, as_embed: bool = False, + as_json: bool = False, with_title: bool = True, with_help: bool = True, - ) -> discord.Embed | str: + ) -> discord.Embed | str | dict[str, str]: """Compute and display character statistics. Args: campaign (Campaign): The campaign to get statistics for. as_embed (bool, optional): Whether to return an embed. Defaults to False. When False, returns a string. + as_json (bool, optional): Whether to return a JSON object. Defaults to False. with_title (bool, optional): Whether to include the title. Defaults to True. with_help (bool, optional): Whether to include the help text. Defaults to True. @@ -335,13 +411,13 @@ async def campaign_statistics( RollStatistic.result == RollResultType.OTHER, ).count() - avg_diff = await RollStatistic.find(RollStatistic.guild == self.ctx.guild.id).avg( + avg_diff = await RollStatistic.find(RollStatistic.campaign == str(campaign.id)).avg( RollStatistic.difficulty ) if avg_diff: self.average_difficulty = round(avg_diff) - avg_pool = await RollStatistic.find(RollStatistic.guild == self.ctx.guild.id).avg( + avg_pool = await RollStatistic.find(RollStatistic.campaign == str(campaign.id)).avg( RollStatistic.pool ) if avg_pool: @@ -355,4 +431,7 @@ async def campaign_statistics( if as_embed: return await self._get_embed(with_title=with_title, with_help=with_help) + if as_json: + return self._get_json() + return self._get_text(with_title=with_title, with_help=with_help) diff --git a/src/valentina/models/user.py b/src/valentina/models/user.py index b93d38c3..bb284cfa 100644 --- a/src/valentina/models/user.py +++ b/src/valentina/models/user.py @@ -46,6 +46,7 @@ class User(Document): id: int # type: ignore [assignment] + avatar_url: str | None = None characters: list[Link[Character]] = Field(default_factory=list) campaign_experience: dict[str, CampaignExperience] = Field(default_factory=dict) date_created: datetime = Field(default_factory=time_now) diff --git a/src/valentina/utils/config.py b/src/valentina/utils/config.py index 9be0d455..ed810943 100644 --- a/src/valentina/utils/config.py +++ b/src/valentina/utils/config.py @@ -1,13 +1,25 @@ """Gather configuration from environment variables.""" from pathlib import Path -from typing import ClassVar +from typing import Annotated, ClassVar from confz import BaseConfig, ConfigSources, EnvSource +from pydantic import BeforeValidator DIR = Path(__file__).parents[3].absolute() +def convert_to_boolean(value: str) -> bool: + """Confz does not work well with Typer options. Confz requires a value for each CLI option, but Typer does not. To workaround this, for example, if --log-to-file is passed, we set the value to "True" regardless of what follows the CLI option.""" + return bool(value.lower() in ["true", "t", "1"]) + + +ENV_BOOLEAN = Annotated[ + bool, + BeforeValidator(convert_to_boolean), +] + + #### NEW CONFIG #### class ValentinaConfig(BaseConfig): # type: ignore [misc] """Valentina configuration.""" @@ -31,6 +43,19 @@ class ValentinaConfig(BaseConfig): # type: ignore [misc] test_mongo_uri: str = "mongodb://localhost:27017" test_mongo_database_name: str = "test_db" + # WebUI Configuration + webui_enable: ENV_BOOLEAN = False + webui_host: str = "127.0.0.1" + webui_port: str = "8000" + webui_log_level: str = "INFO" + webui_debug: ENV_BOOLEAN = False + webui_base_url: str = "" + discord_oauth_secret: str = "" + discord_oauth_client_id: str = "" + redis_password: str = "" + redis_addr: str = "127.0.0.1:6379" + webui_secret_key: str = "" + CONFIG_SOURCES: ClassVar[ConfigSources | None] = [ EnvSource(prefix="VALENTINA_", file=DIR / ".env", allow_all=True), EnvSource(prefix="VALENTINA_", file=DIR / ".env.secrets", allow_all=True), diff --git a/src/valentina/utils/errors.py b/src/valentina/utils/errors.py index 4600faf0..00f3292f 100644 --- a/src/valentina/utils/errors.py +++ b/src/valentina/utils/errors.py @@ -235,3 +235,21 @@ def __init__( msg += f"\nRaised from: {e.__class__.__name__}: {e}" super().__init__(msg, *args, **kwargs) + + +class NoCTXError(Exception): + """Raised when the context was not passed.""" + + def __init__( + self, + msg: str | None = None, + e: Exception | None = None, + *args: str | int, + **kwargs: int | str | bool, + ): + if not msg: + msg = "The context object was not passed." + if e: + msg += f"\nRaised from: {e.__class__.__name__}: {e}" + + super().__init__(msg, *args, **kwargs) diff --git a/src/valentina/utils/helpers.py b/src/valentina/utils/helpers.py index d440324d..8af3ce4c 100644 --- a/src/valentina/utils/helpers.py +++ b/src/valentina/utils/helpers.py @@ -14,16 +14,17 @@ _rng = default_rng() -def convert_int_to_emoji(num: int, markdown: bool = False) -> str: +def convert_int_to_emoji(num: int, markdown: bool = False, images: bool = False) -> str: """Convert an integer to an emoji or a string. This method converts an integer to its corresponding emoji representation if it is between 0 and 10. For integers outside this range, it returns the number as a string. Optionally, it can wrap numbers - larger than emojis in markdown code. + larger than emojis within in markdown
 markers.
 
     Args:
         num (int): The integer to convert.
         markdown (bool, optional): Whether to wrap numbers larger than emojis in markdown code. Defaults to False.
+        images (bool, optional): Whether to use images instead of Discord emoji codes. Defaults to False.
 
     Returns:
         str: The emoji corresponding to the integer, or the integer as a string.
@@ -42,6 +43,22 @@ def convert_int_to_emoji(num: int, markdown: bool = False) -> str:
         '`11`'
     """
     if 0 <= num <= 10:  # noqa: PLR2004
+        if images:
+            return (
+                str(num)
+                .replace("10", "🔟")
+                .replace("0", "0️⃣")
+                .replace("1", "1️⃣")
+                .replace("2", "2️⃣")
+                .replace("3", "3️⃣")
+                .replace("4", "4️⃣")
+                .replace("5", "5️⃣")
+                .replace("6", "6️⃣")
+                .replace("7", "7️⃣")
+                .replace("8", "8️⃣")
+                .replace("9", "9️⃣")
+            )
+
         return (
             str(num)
             .replace("10", ":keycap_ten:")
@@ -56,6 +73,7 @@ def convert_int_to_emoji(num: int, markdown: bool = False) -> str:
             .replace("8", ":eight:")
             .replace("9", ":nine:")
         )
+
     if markdown:
         return f"`{num}`"
 
diff --git a/src/valentina/utils/logging.py b/src/valentina/utils/logging.py
index 86776531..39695b85 100644
--- a/src/valentina/utils/logging.py
+++ b/src/valentina/utils/logging.py
@@ -51,6 +51,13 @@ def instantiate_logger(log_level: LogLevel | None = None) -> None:  # pragma: no
     logging.getLogger("discord.client").setLevel(level=http_log_level.upper())
     logging.getLogger("pymongo").setLevel(level=ValentinaConfig().log_level_pymongo.upper())
     logging.getLogger("faker").setLevel(level="INFO")
+    logging.getLogger("hypercorn").setLevel(level=ValentinaConfig().webui_log_level.upper())
+    logging.getLogger("requests_oauthlib").setLevel(level="INFO")
+    logging.getLogger("quart.app").setLevel(level=ValentinaConfig().webui_log_level.upper())
+    logging.getLogger("quart_wtf").setLevel(level=ValentinaConfig().webui_log_level.upper())
+    logging.getLogger("jinjax").setLevel(level="INFO")
+    logging.getLogger("asyncio").setLevel(level="ERROR")
+
     for service in ["urllib3", "boto3", "botocore", "s3transfer"]:
         logging.getLogger(service).setLevel(level=aws_log_level.upper())
     logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True)
diff --git a/src/valentina/utils/perform_roll.py b/src/valentina/utils/perform_roll.py
index e78ca11f..2ad34c3b 100644
--- a/src/valentina/utils/perform_roll.py
+++ b/src/valentina/utils/perform_roll.py
@@ -38,7 +38,7 @@ async def perform_roll(  # pragma: no cover
         desperation_pool (int, optional): The number of dice in the desperation pool. Defaults to 0.
     """
     roll = DiceRoll(
-        ctx,
+        ctx=ctx,
         pool=pool,
         difficulty=difficulty,
         dice_size=dice_size,
diff --git a/src/valentina/views/roll_display.py b/src/valentina/views/roll_display.py
index 5e8f1ae2..619f0605 100644
--- a/src/valentina/views/roll_display.py
+++ b/src/valentina/views/roll_display.py
@@ -53,8 +53,8 @@ async def get_embed(self) -> discord.Embed:
 
         description = f"""\
 ### {self.ctx.author.mention} rolled `{self.desperation_pool + self.roll.pool}{self.roll.dice_type.name.lower()}`
-## {self.roll.embed_title}
-{self.roll.embed_description}
+## {self.roll.roll_result_humanized.upper()}
+**{self.roll.num_successes_humanized}**
 """
 
         embed = discord.Embed(
diff --git a/src/valentina/webui/WTForms/__init__.py b/src/valentina/webui/WTForms/__init__.py
new file mode 100644
index 00000000..83fb9775
--- /dev/null
+++ b/src/valentina/webui/WTForms/__init__.py
@@ -0,0 +1 @@
+"""Forms using QuartWTForms."""
diff --git a/src/valentina/webui/WTForms/character_create_full.py b/src/valentina/webui/WTForms/character_create_full.py
new file mode 100644
index 00000000..bd65b60e
--- /dev/null
+++ b/src/valentina/webui/WTForms/character_create_full.py
@@ -0,0 +1,151 @@
+"""Custom Forms model for Valentina WebUI."""
+
+from quart_wtf import QuartForm
+from wtforms import DateField, SelectField, StringField, SubmitField, ValidationError
+from wtforms.validators import DataRequired, Length, Optional
+
+from valentina.constants import CharClass, HunterCreed, VampireClan
+from valentina.models import Character
+
+
+class CharacterCreateFullStep1(QuartForm):
+    """Form for creating a character."""
+
+    title = "Step 1"
+    prefix = "step1"
+
+    firstname = StringField(
+        "First Name",
+        default="",
+        validators=[DataRequired(), Length(min=3, message="Must be at least 3 characters")],
+        filters=[str.strip, str.title],
+    )
+    lastname = StringField(
+        "Last Name",
+        default="",
+        validators=[DataRequired(), Length(min=3, message="Must be at least 3 characters")],
+        filters=[str.strip, str.title],
+    )
+    nickname = StringField(
+        "Nickname",
+        default="",
+        validators=[Optional()],
+        filters=[str.strip, str.title],
+    )
+    char_class = SelectField(
+        "Character Class",
+        choices=[("", "-- Select --")]
+        + [(x.name, x.value.name) for x in CharClass.playable_classes()],
+        validators=[DataRequired()],
+    )
+    demeanor = StringField(
+        "Demeanor",
+        default="",
+        validators=[Optional(), Length(min=3, message="Must be at least 3 characters")],
+        filters=[str.strip, str.title],
+    )
+    nature = StringField(
+        "Nature",
+        default="",
+        validators=[Optional(), Length(min=3, message="Must be at least 3 characters")],
+        filters=[str.strip, str.title],
+    )
+    dob = DateField(
+        "Date of Birth", description="Used to calculate age", validators=[Optional()], default=""
+    )
+    submit = SubmitField("Next")
+
+    async def async_validators_lastname(self, lastname: StringField) -> None:
+        """Check if the first + lastname are unique in the database."""
+        name_first = self.firstname.data
+        name_last = lastname.data
+
+        if (
+            await Character.find(
+                Character.name_first == name_first, Character.name_last == name_last
+            ).count()
+            > 0
+        ):
+            msg = "Character name must not already exist."
+            raise ValidationError(msg)
+
+    async def async_validators_firstname(self, firstname: StringField) -> None:
+        """Check if the first + lastname are unique in the database."""
+        name_first = firstname.data
+        name_last = self.lastname.data
+
+        if (
+            await Character.find(
+                Character.name_first == name_first, Character.name_last == name_last
+            ).count()
+            > 0
+        ):
+            msg = "Character name must not already exist."
+            raise ValidationError(msg)
+
+
+class VampireClassSpecifics(QuartForm):
+    """Class specific items for Vampires."""
+
+    title = "Vampire Class Specifics"
+    prefix = "vampire"
+
+    clan_name = SelectField(
+        "Vampire Clan",
+        choices=[("", "-- Select --")] + [(x.name, x.value.name) for x in VampireClan],
+        validators=[DataRequired()],
+    )
+    sire = StringField(
+        "Sire",
+        default="",
+        validators=[Optional()],
+        filters=[str.strip, str.title],
+    )
+    generation = StringField(
+        "Generation",
+        default="",
+        validators=[Optional()],
+        filters=[str.strip, str.title],
+    )
+    submit = SubmitField("Next")
+
+
+class WerewolfClassSpecifics(QuartForm):
+    """Class specific items for Werewolfs."""
+
+    title = "Werewolf Class Specifics"
+    prefix = "werewolf"
+
+    breed = StringField(
+        "Breed",
+        default="",
+        validators=[Optional()],
+        filters=[str.strip, str.title],
+    )
+    auspice = StringField(
+        "Auspice",
+        default="",
+        validators=[Optional()],
+        filters=[str.strip, str.title],
+    )
+    tribe = StringField(
+        "Tribe",
+        default="",
+        validators=[Optional()],
+        filters=[str.strip, str.title],
+    )
+    submit = SubmitField("Next")
+
+
+class HunterClassSpecifics(QuartForm):
+    """Class specific items for Hunters."""
+
+    title = "Hunter Class Specifics"
+    prefix = "hunter"
+
+    creed = SelectField(
+        "Creed",
+        choices=[("", "-- Select --")] + [(x.name, x.value.get("name")) for x in HunterCreed],
+        validators=[DataRequired()],
+    )
+    submit = SubmitField("Next")
diff --git a/src/valentina/webui/WTForms/character_edit.py b/src/valentina/webui/WTForms/character_edit.py
new file mode 100644
index 00000000..726b19f4
--- /dev/null
+++ b/src/valentina/webui/WTForms/character_edit.py
@@ -0,0 +1,103 @@
+"""Forms for individual character fields."""
+
+import uuid
+
+from quart_wtf import QuartForm
+from wtforms import DateField, HiddenField, StringField, SubmitField, ValidationError
+from wtforms.validators import DataRequired, Length, Optional
+
+from valentina.utils import console
+
+from .validators import validate_unique_character_name
+
+
+class EmptyForm(QuartForm):
+    """Form for creating a character."""
+
+    title = ""
+    prefix = str(uuid.uuid4())[:8]
+
+    async def async_validators_name_last(self, name_last: StringField) -> None:
+        """Check if the first + lastname are unique in the database."""
+        console.log(f"{self.character_id.data=}")
+        if not await validate_unique_character_name(
+            name_first=self.name_first.data,
+            name_last=name_last.data,
+            character_id=self.character_id.data,
+        ):
+            msg = "Character name must not already exist."
+            raise ValidationError(msg)
+
+    async def async_validators_name_first(self, name_first: StringField) -> None:
+        """Check if the first + lastname are unique in the database."""
+        console.log(f"{self.character_id.data=}")
+        if not await validate_unique_character_name(
+            name_first=name_first.data,
+            name_last=self.name_last.data,
+            character_id=self.character_id.data,
+        ):
+            msg = "Character name must not already exist."
+            raise ValidationError(msg)
+
+
+class CharacterEditForm(QuartForm):
+    """Form for creating a character."""
+
+    title = ""
+    prefix = str(uuid.uuid4())[:8]
+    character_id = HiddenField()
+
+    name_first = StringField(
+        "First Name",
+        default="",
+        validators=[DataRequired(), Length(min=3, message="Must be at least 3 characters")],
+        filters=[str.strip, str.title],
+    )
+    name_last = StringField(
+        "Last Name",
+        default="",
+        validators=[DataRequired(), Length(min=3, message="Must be at least 3 characters")],
+        filters=[str.strip, str.title],
+    )
+    name_nick = StringField(
+        "Nickname",
+        default="",
+        validators=[Optional()],
+        filters=[str.strip, str.title],
+    )
+    demeanor = StringField(
+        "Demeanor",
+        default="",
+        validators=[Optional(), Length(min=3, message="Must be at least 3 characters")],
+        filters=[str.strip, str.title],
+    )
+    nature = StringField(
+        "Nature",
+        default="",
+        validators=[Optional(), Length(min=3, message="Must be at least 3 characters")],
+        filters=[str.strip, str.title],
+    )
+    dob = DateField(
+        "Date of Birth", description="Used to calculate age", validators=[Optional()], default=""
+    )
+    submit = SubmitField("Submit")
+
+    async def async_validators_name_last(self, name_last: StringField) -> None:
+        """Check if the first + lastname are unique in the database."""
+        if not await validate_unique_character_name(
+            name_first=self.name_first.data,
+            name_last=name_last.data,
+            character_id=self.character_id.data,
+        ):
+            msg = "Character name must not already exist."
+            raise ValidationError(msg)
+
+    async def async_validators_name_first(self, name_first: StringField) -> None:
+        """Check if the first + lastname are unique in the database."""
+        if not await validate_unique_character_name(
+            name_first=name_first.data,
+            name_last=self.name_last.data,
+            character_id=self.character_id.data,
+        ):
+            msg = "Character name must not already exist."
+            raise ValidationError(msg)
diff --git a/src/valentina/webui/WTForms/fields.py b/src/valentina/webui/WTForms/fields.py
new file mode 100644
index 00000000..301c6517
--- /dev/null
+++ b/src/valentina/webui/WTForms/fields.py
@@ -0,0 +1,70 @@
+"""Fields for WTForms that are specific to the Valentina web UI."""
+
+from wtforms import DateField, HiddenField, StringField, SubmitField
+from wtforms.validators import DataRequired, Length, Optional
+
+name_first = StringField(
+    "First Name",
+    default="",
+    validators=[DataRequired(), Length(min=3, message="Must be at least 3 characters")],
+    filters=[str.strip, str.title],
+)
+name_last = StringField(
+    "Last Name",
+    default="",
+    validators=[DataRequired(), Length(min=3, message="Must be at least 3 characters")],
+    filters=[str.strip, str.title],
+)
+name_nick = StringField(
+    "Nickname",
+    default="",
+    validators=[Optional()],
+    filters=[str.strip, str.title],
+)
+demeanor = StringField(
+    "Demeanor",
+    default="",
+    validators=[Optional(), Length(min=3, message="Must be at least 3 characters")],
+    filters=[str.strip, str.title],
+)
+nature = StringField(
+    "Nature",
+    default="",
+    validators=[Optional(), Length(min=3, message="Must be at least 3 characters")],
+    filters=[str.strip, str.title],
+)
+dob = DateField(
+    "Date of Birth", description="Used to calculate age", validators=[Optional()], default=""
+)
+breed = StringField(
+    "Breed",
+    default="",
+    validators=[Optional()],
+    filters=[str.strip, str.title],
+)
+auspice = StringField(
+    "Auspice",
+    default="",
+    validators=[Optional()],
+    filters=[str.strip, str.title],
+)
+tribe = StringField(
+    "Tribe",
+    default="",
+    validators=[Optional()],
+    filters=[str.strip, str.title],
+)
+sire = StringField(
+    "Sire",
+    default="",
+    validators=[Optional(), Length(min=3, message="Must be at least 3 characters")],
+    filters=[str.strip, str.title],
+)
+generation = StringField(
+    "Generation",
+    default="",
+    validators=[Optional()],
+    filters=[str.strip, str.title],
+)
+submit = SubmitField("Submit")
+character_id = HiddenField()
diff --git a/src/valentina/webui/WTForms/validators.py b/src/valentina/webui/WTForms/validators.py
new file mode 100644
index 00000000..ac5a4eaa
--- /dev/null
+++ b/src/valentina/webui/WTForms/validators.py
@@ -0,0 +1,36 @@
+"""Validators for WTF forms."""
+
+from bson import ObjectId
+
+from valentina.models import Character
+from valentina.utils import console
+
+
+async def validate_unique_character_name(
+    name_first: str, name_last: str, character_id: str = ""
+) -> bool:
+    """Check if the first + lastname are unique in the database."""
+    if character_id:
+        result = await Character.find(
+            Character.id != ObjectId(character_id),
+            Character.name_first == name_first,
+            Character.name_last == name_last,
+        ).count()
+        console.log(f"{result=}")
+
+        return (
+            await Character.find(
+                Character.id != ObjectId(character_id),
+                Character.name_first == name_first,
+                Character.name_last == name_last,
+            ).count()
+            == 0
+        )
+
+    return (
+        await Character.find(
+            Character.name_first == name_first,
+            Character.name_last == name_last,
+        ).count()
+        == 0
+    )
diff --git a/src/valentina/webui/__init__.py b/src/valentina/webui/__init__.py
new file mode 100644
index 00000000..c1b3c76c
--- /dev/null
+++ b/src/valentina/webui/__init__.py
@@ -0,0 +1,119 @@
+"""Quart application for the Valentina web interface."""
+
+import quart_flask_patch  # isort: skip # noqa: F401
+import asyncio
+from pathlib import Path
+
+from flask_discord import DiscordOAuth2Session
+from hypercorn.asyncio import serve
+from hypercorn.config import Config
+from loguru import logger
+from quart import Quart, redirect, request
+from quart_session import Session
+from werkzeug.wrappers.response import Response
+
+from valentina.utils import ValentinaConfig
+from valentina.webui.utils.errors import register_error_handlers
+from valentina.webui.utils.jinja_filters import register_filters
+from valentina.webui.utils.jinjax import register_jinjax_catalog
+
+# Allow insecure transport for debugging from localhost
+if ValentinaConfig().webui_debug:
+    import os
+
+    os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
+
+
+template_dir = Path(__file__).parent / "templates"
+static_dir = Path(__file__).parent / "static"
+app = Quart(
+    __name__, template_folder=str(template_dir), static_url_path="/static", static_folder="static"
+)
+app.config["SECRET_KEY"] = ValentinaConfig().webui_secret_key
+app.config["DISCORD_CLIENT_ID"] = ValentinaConfig().discord_oauth_client_id
+app.config["DISCORD_CLIENT_SECRET"] = ValentinaConfig().discord_oauth_secret
+app.config["DISCORD_REDIRECT_URI"] = f"{ValentinaConfig().webui_base_url}/callback"
+app.config["DISCORD_BOT_TOKEN"] = ValentinaConfig().discord_token
+discord_oauth = DiscordOAuth2Session(app)
+register_filters(app)
+register_error_handlers(app)
+catalog = register_jinjax_catalog(app)
+app.config["SESSION_TYPE"] = "redis"
+app.config["SESSION_URI"] = (
+    (f"redis://:{ValentinaConfig().redis_password}@{ValentinaConfig().redis_addr}")
+    if {ValentinaConfig().redis_addr}
+    else f"redis://{ValentinaConfig().redis_addr}"
+)
+Session(app)
+
+
+def import_blueprints() -> None:
+    """Import routes to avoid circular imports."""
+    from .blueprints import campaign_bp, character_bp, gameplay_bp
+    from .routes import home, oauth
+
+    app.register_blueprint(campaign_bp)
+    app.register_blueprint(character_bp)
+    app.register_blueprint(home.bp)
+    app.register_blueprint(oauth.bp)
+    app.register_blueprint(gameplay_bp)
+
+
+def create_dev_app() -> Quart:
+    """Create a new Quart app with the given configuration. This is used for development only."""
+    # Always import blueprints to avoid circular imports
+    import_blueprints()
+
+    @app.before_serving
+    async def create_db_pool() -> None:
+        """Initialize the database connection pool."""
+        import pymongo
+
+        from valentina.utils.database import init_database
+
+        while True:
+            try:
+                await init_database()
+            except pymongo.errors.ServerSelectionTimeoutError as e:
+                logger.error(f"DB: Failed to initialize database: {e}")
+                await asyncio.sleep(60)
+            else:
+                break
+
+    @app.before_request
+    def remove_trailing_slash() -> Response:
+        """Redirect requests with trailing slashes to the correct URL.
+
+        example.com/url/ -> example.com/url
+        """
+        request_path: str = request.path
+        if request_path != "/" and request_path.endswith("/"):
+            return redirect(request_path[:-1], 301)
+
+        return None
+
+    return app
+
+
+async def run_webserver() -> None:
+    """Run the web server in production."""
+    # Imnport these here to avoid circular imports
+    import_blueprints()
+
+    @app.before_request
+    def remove_trailing_slash() -> Response:
+        """Redirect requests with trailing slashes to the correct URL.
+
+        example.com/url/ -> example.com/url
+        """
+        request_path: str = request.path
+        if request_path != "/" and request_path.endswith("/"):
+            return redirect(request_path[:-1], 301)
+
+        return None
+
+    hypercorn_config = Config()
+    hypercorn_config.bind = [f"{ValentinaConfig().webui_host}:{ValentinaConfig().webui_port}"]
+    hypercorn_config.loglevel = ValentinaConfig().webui_log_level.upper()
+    hypercorn_config.use_reloader = ValentinaConfig().webui_debug
+    await serve(app, hypercorn_config, shutdown_trigger=lambda: asyncio.Future())
diff --git a/src/valentina/webui/blueprints.py b/src/valentina/webui/blueprints.py
new file mode 100644
index 00000000..9a8cfdc7
--- /dev/null
+++ b/src/valentina/webui/blueprints.py
@@ -0,0 +1,63 @@
+"""Routes for the webui module."""
+
+from quart import Blueprint
+
+from valentina.webui.views import (
+    CampaignView,
+    CharacterEdit,
+    CharacterView,
+    CreateCharacterStart,
+    CreateCharacterStep1,
+    CreateCharacterStep2,
+    CreateCharacterStep3,
+    DiceRollView,
+    GameplayView,
+)
+
+campaign_bp = Blueprint("campaign", __name__)
+campaign_bp.add_url_rule(
+    "/campaign/",
+    view_func=CampaignView.as_view("view_campaign"),
+    methods=["GET", "POST"],
+)
+
+
+character_bp = Blueprint("character", __name__)
+character_bp.add_url_rule(
+    "/create_full",
+    view_func=CreateCharacterStart.as_view("create_full_start"),
+    methods=["GET"],
+)
+character_bp.add_url_rule(
+    "/create_full/1",
+    view_func=CreateCharacterStep1.as_view("create_full_1"),
+    methods=["GET", "POST"],
+)
+character_bp.add_url_rule(
+    "/create_full/2//",
+    view_func=CreateCharacterStep2.as_view("create_full_2"),
+    methods=["GET", "POST"],
+)
+character_bp.add_url_rule(
+    "/create_full/3/",
+    view_func=CreateCharacterStep3.as_view("create_full_3"),
+    methods=["GET", "POST"],
+)
+character_bp.add_url_rule(
+    "/character/",
+    view_func=CharacterView.as_view("character_view"),
+    methods=["GET", "POST"],
+)
+character_bp.add_url_rule(
+    "/character//edit",
+    view_func=CharacterEdit.as_view("character_edit"),
+    methods=["GET", "POST"],
+)
+
+gameplay_bp = Blueprint("gameplay", __name__)
+gameplay_bp.add_url_rule("/gameplay", view_func=GameplayView.as_view("gameplay"), methods=["GET"])
+gameplay_bp.add_url_rule(
+    "/gameplay/diceroll",
+    view_func=DiceRollView.as_view("diceroll"),
+    methods=["GET", "POST"],
+)
diff --git a/src/valentina/webui/components/NavBar.jinja b/src/valentina/webui/components/NavBar.jinja
new file mode 100644
index 00000000..2b1038f7
--- /dev/null
+++ b/src/valentina/webui/components/NavBar.jinja
@@ -0,0 +1,96 @@
+
diff --git a/src/valentina/webui/components/PageLayout.jinja b/src/valentina/webui/components/PageLayout.jinja
new file mode 100644
index 00000000..f8bd51e3
--- /dev/null
+++ b/src/valentina/webui/components/PageLayout.jinja
@@ -0,0 +1,31 @@
+{# def title="" #}
+
+
+    
+        
+        
+        Valentina Noir
+            {% if title %}: {{ title }}{% endif %}
+        
+        
+        
+        
+        
+        {{ catalog.render_assets() }}
+        
+    
+    
+        {# include nav bar only when user is logged in #}
+        {% if session["USER_ID"] is defined and session["GUILD_ID"] is defined %}{% endif %}
+        
{{ content }}
+ + + diff --git a/src/valentina/webui/components/global/AccordionGroup.jinja b/src/valentina/webui/components/global/AccordionGroup.jinja new file mode 100644 index 00000000..1f679ebd --- /dev/null +++ b/src/valentina/webui/components/global/AccordionGroup.jinja @@ -0,0 +1,6 @@ +{# def + name:str, + flush:bool = False, +#} +
{{ content }}
diff --git a/src/valentina/webui/components/global/AccordionItem.jinja b/src/valentina/webui/components/global/AccordionItem.jinja new file mode 100644 index 00000000..bc9ce6e0 --- /dev/null +++ b/src/valentina/webui/components/global/AccordionItem.jinja @@ -0,0 +1,24 @@ +{# def + accordion_name:str, + index:int|str, + title:str, + start_open:bool = False, +#} +{% set accordion_index = accordion_name ~ index %} +
+

+ +

+
+
{{ content }}
+
+
diff --git a/src/valentina/webui/components/global/LargeImageModal.jinja b/src/valentina/webui/components/global/LargeImageModal.jinja new file mode 100644 index 00000000..94f78f6e --- /dev/null +++ b/src/valentina/webui/components/global/LargeImageModal.jinja @@ -0,0 +1,28 @@ +{# def + modal_id:str, + title:str="", + img_src:str +#} + diff --git a/src/valentina/webui/components/global/PageTitle.jinja b/src/valentina/webui/components/global/PageTitle.jinja new file mode 100644 index 00000000..973b7fef --- /dev/null +++ b/src/valentina/webui/components/global/PageTitle.jinja @@ -0,0 +1,17 @@ +{# def + right_button:bool = False, + button_text:str = "", + button_url:str = "", +#} +{% if not right_button %} +
+

{{ content }}

+
+{% else %} +
+

{{ content }}

+ {{ button_text }} +
+{% endif %} +{# #} diff --git a/src/valentina/webui/components/global/Redirect.jinja b/src/valentina/webui/components/global/Redirect.jinja new file mode 100644 index 00000000..5d55dd3c --- /dev/null +++ b/src/valentina/webui/components/global/Redirect.jinja @@ -0,0 +1,4 @@ +{# def + url:str, +#} + diff --git a/src/valentina/webui/components/global/Statistics.jinja b/src/valentina/webui/components/global/Statistics.jinja new file mode 100644 index 00000000..23f39fbc --- /dev/null +++ b/src/valentina/webui/components/global/Statistics.jinja @@ -0,0 +1,42 @@ +{# def + statistics:dict[str,str], + with_title:bool=True, + title:str="Statistics" +#} +{% if with_title %}

{{ title }}

{% endif %} +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Total Rolls{{ statistics.total_rolls }}
Avg. Difficulty{{ statistics.average_difficulty }}
Avg. Pool{{ statistics.average_pool }}
Avg. Criticals{{ statistics.criticals }} ({{ statistics.criticals_percentage }}%)
Avg. Successes{{ statistics.successes }} ({{ statistics.successes_percentage }}%)
Avg. Failures{{ statistics.failures }} ({{ statistics.failures_percentage }}%)
Avg. Botches{{ statistics.botches }} ({{ statistics.botches_percentage }}%)
+
+
+
diff --git a/src/valentina/webui/components/global/TabGroup.jinja b/src/valentina/webui/components/global/TabGroup.jinja new file mode 100644 index 00000000..473d01a8 --- /dev/null +++ b/src/valentina/webui/components/global/TabGroup.jinja @@ -0,0 +1,9 @@ +{# def + justified: bool = False, + tab_type:str = "nav-tabs" +#} +{# tab_type options: nav-tabs nav-pills nav-underline #} + diff --git a/src/valentina/webui/components/global/TabItem.jinja b/src/valentina/webui/components/global/TabItem.jinja new file mode 100644 index 00000000..7bf77852 --- /dev/null +++ b/src/valentina/webui/components/global/TabItem.jinja @@ -0,0 +1,11 @@ +{# def + number: int, + url: str = "", + text: str, + target: str, +#} + diff --git a/src/valentina/webui/components/global/WTFormContainer.jinja b/src/valentina/webui/components/global/WTFormContainer.jinja new file mode 100644 index 00000000..0d223cd4 --- /dev/null +++ b/src/valentina/webui/components/global/WTFormContainer.jinja @@ -0,0 +1,61 @@ +{# def + form:QuartForm, + join_label:bool = False, + floating_label:bool = False, + progress:int = None, + post_url:str = "", +#} +{# Determine the encoding type based on the presence of file fields #} +{% set encoding = "multipart/form-data" if form.__iter__() | selectattr("type", "in", ["FileField","MultipleFileField"]) | list else "application/x-www-form-urlencoded" %} +
+ {% if progress %} +
+
{{ progress }}%
+
+ {% endif %} +

{{ form.title }}

+
+ {% if form.form_errors %} + + {% endif %} + {% for field in form %} + {% set valid_css = " is-invalid" if field.errors else " is-valid" if field.raw_data else "" %} + {% if field.type == "SubmitField" %} + + {% elif field.type in ["CSRFTokenField","HiddenField"] %} + {{ field() }} + {% elif field.type == "BooleanField" %} + + {% elif field.type == "RadioField" %} + + {% elif field.type == "ColorField" %} + + {% elif field.type == "IntegerField" %} + + {% elif field.type in ["SelectField", "SelectMultipleField"] %} + + {% elif field.type in ["IntegerRangeField", "DecimalRangeField"] %} + + {% else %} + {# All other field types use text inputs #} + + {% endif %} + {% endfor %} + +
diff --git a/src/valentina/webui/components/global/valentinaform/Increment.jinja b/src/valentina/webui/components/global/valentinaform/Increment.jinja new file mode 100644 index 00000000..7dd2fc3a --- /dev/null +++ b/src/valentina/webui/components/global/valentinaform/Increment.jinja @@ -0,0 +1,28 @@ +{# def + form:ValentinaForm, + name:str, + label:str, + start_val:int = 1, + max_val:int=5, + min_val:int=0, +#} +
+ {% if not form.join_labels %}{% endif %} +
+ {% if form.join_labels %}{% endif %} + + + +
+
diff --git a/src/valentina/webui/components/global/valentinaform/Select.jinja b/src/valentina/webui/components/global/valentinaform/Select.jinja new file mode 100644 index 00000000..c64832e5 --- /dev/null +++ b/src/valentina/webui/components/global/valentinaform/Select.jinja @@ -0,0 +1,48 @@ +{# def + form:ValentinaForm, + name:str, + label:str, + description:str = "", + choices:list[tuple[str,str]] = [], + value:str="", + is_valid:bool = False, + is_invalid:bool = False, + error_msg:str = "", +#} +{# Choices + a list of tuples (value, name) +#} +{% set valid_css = " is-invalid" if is_invalid else " is-valid" if is_valid else "" %} +{% if form.join_labels %} +
+
+ {{ label }} + +
{{ error_msg }}
+ +
+ {% if description %}
{{ description }}
{% endif %} +
+{% else %} +
+ + +
{{ error_msg }}
+ {% if description %}
{{ description }}
{% endif %} + +
+{% endif %} diff --git a/src/valentina/webui/components/global/valentinaform/TextInput.jinja b/src/valentina/webui/components/global/valentinaform/TextInput.jinja new file mode 100644 index 00000000..8fbbeb38 --- /dev/null +++ b/src/valentina/webui/components/global/valentinaform/TextInput.jinja @@ -0,0 +1,47 @@ +{# def + form:ValentinaForm, + name:str, + label:str, + value:str="", + description:str = "", + type:str = "text", + required:bool = False, + is_valid:bool = False, + is_invalid:bool = False, + error_msg:str="", +#} +{% set valid_css = " is-invalid" if is_invalid else " is-valid" if is_valid else "" %} +{% set value_insert = 'value="'~value~'"' if value else "" %} +{% if form.join_labels %} +
+
+ {{ label }} + + + {% if error_msg and is_invalid %}
{{ error_msg }}
{% endif %} +
+ {% if description %}
{{ description }}
{% endif %} +
+{% elif form.floating_labels %} +
+ + + + {% if error_msg and is_invalid %}
{{ error_msg }}
{% endif %} + {% if description %}
{{ description }}
{% endif %} +
+{% else %} +
+ + + + {% if error_msg and is_invalid %}
{{ error_msg }}
{% endif %} + {% if description %}
{{ description }}
{% endif %} +
+{% endif %} diff --git a/src/valentina/webui/components/global/valentinaform/TraitRadioToggle.jinja b/src/valentina/webui/components/global/valentinaform/TraitRadioToggle.jinja new file mode 100644 index 00000000..a7ad6fde --- /dev/null +++ b/src/valentina/webui/components/global/valentinaform/TraitRadioToggle.jinja @@ -0,0 +1,17 @@ +{# def + form:ValentinaForm, + max_value:int = 5, + name:str, + label:str, + current_value:int = 0, +#} +{% for i in range(max_value + 1) %} + + +{% endfor %} diff --git a/src/valentina/webui/components/global/wtform/BooleanField.jinja b/src/valentina/webui/components/global/wtform/BooleanField.jinja new file mode 100644 index 00000000..de1733c4 --- /dev/null +++ b/src/valentina/webui/components/global/wtform/BooleanField.jinja @@ -0,0 +1,10 @@ +{# def + field:Field, +#} +{% set valid_css = " is-invalid" if field.errors else " is-valid" if field.raw_data else "" %} +
+ {{ field(class="form-check-input"~valid_css) }} + {{ field.label(class="form-check-label") }} + {% if field.errors %}
{{ field.errors|first }}
{% endif %} + {% if field.description %}
{{ field.description }}
{% endif %} +
diff --git a/src/valentina/webui/components/global/wtform/ColorField.jinja b/src/valentina/webui/components/global/wtform/ColorField.jinja new file mode 100644 index 00000000..4179666c --- /dev/null +++ b/src/valentina/webui/components/global/wtform/ColorField.jinja @@ -0,0 +1,23 @@ +{# def + field:Field, + join_label:bool=False, +#} +{% set label_text = field.label.text~"*" if field.flags.required else field.label.text %} +{% set valid_css = " is-invalid" if field.errors else " is-valid" if field.raw_data else "" %} +{% if join_label %} +
+
+ {{ label_text }} + {{ field(**{"class":"form-control form-control-color"~valid_css}) }} + {% if field.errors %}
{{ field.errors|first }}
{% endif %} + {% if field.description %}
{{ field.description }}
{% endif %} +
+
+{% else %} +
+ + {{ field(**{"class":"form-control form-control-color"~valid_css, "placeholder":""}) }} + {% if field.errors %}
{{ field.errors|first }}
{% endif %} + {% if field.description %}
{{ field.description }}
{% endif %} +
+{% endif %} diff --git a/src/valentina/webui/components/global/wtform/IntegerField.jinja b/src/valentina/webui/components/global/wtform/IntegerField.jinja new file mode 100644 index 00000000..a037b1cc --- /dev/null +++ b/src/valentina/webui/components/global/wtform/IntegerField.jinja @@ -0,0 +1,32 @@ +{# def + field:Field, + join_label:bool=False, +#} +{# USAGE: + This field creates an integer up/down field between min and max values using AlpinJS for the dynamic functionality. + + Min, max, and start are set by passing the render_kw dictionary to the field at WTForm creation time. Example: ender_kw={"min": 0,"max": 5, "start": 1}. Note, render_kw does not replace the need for validators such as NumberRange() +#} +{% set label_text = field.label.text~"*" if field.flags.required else field.label.text %} +{% set valid_css = " is-invalid" if field.errors else " is-valid" if field.raw_data else "" %} +{% set min_val = field.render_kw.get('min', 0) %} +{% set max_val = field.render_kw.get('max', 100) %} +{% set start_val = field.render_kw.get('start', 0) %} +
+ {% if not join_label %}{% endif %} +
+ {% if join_label %}{% endif %} + + {{ field(**{"class":"form-control"~valid_css, "x-model":"currentVal.toFixed(decimalPoints)","readonly":""}) }} + + {% if field.errors %}
{{ field.errors|first }}
{% endif %} +
+
diff --git a/src/valentina/webui/components/global/wtform/RadioField.jinja b/src/valentina/webui/components/global/wtform/RadioField.jinja new file mode 100644 index 00000000..ca9645ee --- /dev/null +++ b/src/valentina/webui/components/global/wtform/RadioField.jinja @@ -0,0 +1,17 @@ +{# def + field:Field, +#} +{% set valid_css = " is-invalid" if field.errors else " is-valid" if field.raw_data else "" %} +
+ {{ field.label(class="form-label fw-medium") }} + {% for subfield in field %} +
+ {{ subfield(class="form-check-input"~valid_css) }} + {{ subfield.label(class="form-check-label") }} + {% if loop.last %} + {% if field.errors %}
{{ field.errors|first }}
{% endif %} + {% if field.description %}
{{ field.description }}
{% endif %} + {% endif %} +
+ {% endfor %} +
diff --git a/src/valentina/webui/components/global/wtform/RangeField.jinja b/src/valentina/webui/components/global/wtform/RangeField.jinja new file mode 100644 index 00000000..d8622509 --- /dev/null +++ b/src/valentina/webui/components/global/wtform/RangeField.jinja @@ -0,0 +1,31 @@ +{# def + field:Field, + join_label:bool=False, + floating_label:bool=False, +#} +{% set label_text = field.label.text~"*" if field.flags.required else field.label.text %} +{% set valid_css = " is-invalid" if field.errors else " is-valid" if field.raw_data else "" %} +{% if join_label %} +
+
+ {{ label_text }} + {{ field(**{"class":"form-range"~valid_css}) }} + {% if field.errors %}
{{ field.errors|first }}
{% endif %} + {% if field.description %}
{{ field.description }}
{% endif %} +
+
+{% elif floating_label %} +
+ {{ field(**{"class":"form-range"~valid_css, "placeholder":""}) }} + + {% if field.errors %}
{{ field.errors|first }}
{% endif %} + {% if field.description %}
{{ field.description }}
{% endif %} +
+{% else %} +
+ + {{ field(**{"class":"form-range"~valid_css, "placeholder":""}) }} + {% if field.errors %}
{{ field.errors|first }}
{% endif %} + {% if field.description %}
{{ field.description }}
{% endif %} +
+{% endif %} diff --git a/src/valentina/webui/components/global/wtform/SelectField.jinja b/src/valentina/webui/components/global/wtform/SelectField.jinja new file mode 100644 index 00000000..f046a0e5 --- /dev/null +++ b/src/valentina/webui/components/global/wtform/SelectField.jinja @@ -0,0 +1,23 @@ +{# def + field:Field, + join_label:bool=False, +#} +{% set label_text = field.label.text~"" if field.flags.required else field.label.text %} +{% set valid_css = " is-invalid" if field.errors else " is-valid" if field.raw_data else "" %} +{% if join_label %} +
+
+ {{ label_text }} + {{ field(**{"class":"form-select"~valid_css}) }} + {% if field.errors %}
{{ field.errors|first }}
{% endif %} + {% if field.description %}
{{ field.description }}
{% endif %} +
+
+{% else %} +
+ + {{ field(**{"class":"form-select"~valid_css, "placeholder":""}) }} + {% if field.errors %}
{{ field.errors|first }}
{% endif %} + {% if field.description %}
{{ field.description }}
{% endif %} +
+{% endif %} diff --git a/src/valentina/webui/components/global/wtform/StringField.jinja b/src/valentina/webui/components/global/wtform/StringField.jinja new file mode 100644 index 00000000..b3bd64f1 --- /dev/null +++ b/src/valentina/webui/components/global/wtform/StringField.jinja @@ -0,0 +1,31 @@ +{# def + field:Field, + join_label:bool=False, + floating_label:bool=False, +#} +{% set label_text = field.label.text~'' if field.flags.required else field.label.text %} +{% set valid_css = " is-invalid" if field.errors else " is-valid" if field.raw_data else "" %} +{% if join_label %} +
+
+ {{ label_text }} + {{ field(**{"class":"form-control"~valid_css}) }} + {% if field.errors %}
{{ field.errors|first }}
{% endif %} +
+ {% if field.description %}
{{ field.description }}
{% endif %} +
+{% elif floating_label %} +
+ {{ field(**{"class":"form-control"~valid_css, "placeholder":""}) }} + + {% if field.errors %}
{{ field.errors|first }}
{% endif %} + {% if field.description %}
{{ field.description }}
{% endif %} +
+{% else %} +
+ + {{ field(**{"class":"form-control"~valid_css, "placeholder":""}) }} + {% if field.errors %}
{{ field.errors|first }}
{% endif %} + {% if field.description %}
{{ field.description }}
{% endif %} +
+{% endif %} diff --git a/src/valentina/webui/components/global/wtform/SubmitField.jinja b/src/valentina/webui/components/global/wtform/SubmitField.jinja new file mode 100644 index 00000000..e1bdda0f --- /dev/null +++ b/src/valentina/webui/components/global/wtform/SubmitField.jinja @@ -0,0 +1,7 @@ +{# def + field:Field, +#} +{{ field(class="btn btn-success") }} + diff --git a/src/valentina/webui/components/global/wtform/partials/JoinLabelText.jinja b/src/valentina/webui/components/global/wtform/partials/JoinLabelText.jinja new file mode 100644 index 00000000..cf91152d --- /dev/null +++ b/src/valentina/webui/components/global/wtform/partials/JoinLabelText.jinja @@ -0,0 +1 @@ +
{{ content.rjust(17, ' ') | title }}
diff --git a/src/valentina/webui/components/homepage/Card.jinja b/src/valentina/webui/components/homepage/Card.jinja new file mode 100644 index 00000000..d997300e --- /dev/null +++ b/src/valentina/webui/components/homepage/Card.jinja @@ -0,0 +1,19 @@ +{# def + title: str, + icon:str=None, +#} +{# USAGE: + Icon: The name of a font-awesome icon to display in the card title. + Content: All paragraphs should have class "card-text" to style them consistently +#} +
+
+
+

+ {% if icon %}{% endif %} + {{ title }} +

+ {{ content }} +
+
+
diff --git a/src/valentina/webui/routes/__init__.py b/src/valentina/webui/routes/__init__.py new file mode 100644 index 00000000..9bf972d5 --- /dev/null +++ b/src/valentina/webui/routes/__init__.py @@ -0,0 +1 @@ +"""Routes for the webui module.""" diff --git a/src/valentina/webui/routes/home.py b/src/valentina/webui/routes/home.py new file mode 100644 index 00000000..c6360440 --- /dev/null +++ b/src/valentina/webui/routes/home.py @@ -0,0 +1,38 @@ +"""Routes for the home page.""" + +from flask_discord import requires_authorization +from quart import Blueprint, redirect, request, send_from_directory, session, url_for +from quart.wrappers.response import Response as QuartResponse +from werkzeug.wrappers.response import Response + +from valentina.webui import catalog, discord_oauth, static_dir +from valentina.webui.utils.helpers import update_session +from valentina.webui.views import HomepageView + +bp = Blueprint("homepage", __name__) +bp.add_url_rule("/", view_func=HomepageView.as_view("homepage"), methods=["GET"]) + + +@bp.route("/select-guild", methods=["GET", "POST"]) +@requires_authorization +async def select_guild() -> str | Response: + """Select a guild to play in.""" + if request.method == "POST": + form = await request.form + + session["GUILD_ID"] = int(form["guild_id"]) + del session["matched_guilds"] + await update_session() + + return redirect(url_for("homepage.homepage")) + + user = discord_oauth.fetch_user() + + return catalog.render("guild_select", user=user, matched_guilds=session["matched_guilds"]) + + +# @bp.route("/sitemap.xml") +@bp.route("/robots.txt") +async def static_from_root() -> QuartResponse: + """Serve a static file from the root directory.""" + return await send_from_directory(static_dir, request.path[1:]) diff --git a/src/valentina/webui/routes/oauth.py b/src/valentina/webui/routes/oauth.py new file mode 100644 index 00000000..958b8801 --- /dev/null +++ b/src/valentina/webui/routes/oauth.py @@ -0,0 +1,56 @@ +"""Route for Discord OAuth2 authentication.""" + +from typing import Any + +from quart import Blueprint, abort, redirect, session, url_for + +from valentina.models import User +from valentina.webui import discord_oauth +from valentina.webui.utils import update_session + +bp = Blueprint("oauth", __name__) + + +@bp.route("/login") +async def login() -> Any: + """Login route.""" + return discord_oauth.create_session() + + +@bp.route("/logout") +async def logout() -> Any: + """Login route.""" + session.clear() + discord_oauth.revoke() + return redirect(url_for("homepage.homepage")) + + +@bp.route("/callback") +async def callback() -> Any: + """Callback route.""" + discord_oauth.callback() + + user = discord_oauth.fetch_user() + session["USER_ID"] = user.id + + db_user = await User.get(user.id) + + # Deny access to users who aren't playing in a guild + # TODO: Add a custom page for this + if not db_user: + abort(403) + + matched_guilds = [ + {"id": x.id, "name": x.name} for x in user.fetch_guilds() if int(x.id) in db_user.guilds + ] + + if not matched_guilds: + abort(403) + + if len(matched_guilds) == 1: + session["GUILD_ID"] = matched_guilds[0]["id"] + await update_session() + return redirect(url_for("index")) + + session["matched_guilds"] = matched_guilds + return redirect(url_for("homepage.select_guild")) diff --git a/src/valentina/webui/static/components/bootstrap_initialize.js b/src/valentina/webui/static/components/bootstrap_initialize.js new file mode 100644 index 00000000..ddbd7bab --- /dev/null +++ b/src/valentina/webui/static/components/bootstrap_initialize.js @@ -0,0 +1,73 @@ +/* global bootstrap: false */ + +(() => { + 'use strict' + + // Tooltip and popover demos + document.querySelectorAll('.has_tooltips') + .forEach(tooltip => { + new bootstrap.Tooltip(tooltip, { + selector: '[data-bs-toggle="tooltip"]' + }) + }) + + document.querySelectorAll('[data-bs-toggle="popover"]') + .forEach(popover => { + new bootstrap.Popover(popover) + }) + + document.querySelectorAll('.toast') + .forEach(toastNode => { + const toast = new bootstrap.Toast(toastNode, { + autohide: false + }) + + toast.show() + }) + + // Disable empty links and submit buttons + document.querySelectorAll('[href="#"], [type="submit"]') + .forEach(link => { + link.addEventListener('click', event => { + event.preventDefault() + }) + }) + + function setActiveItem() { + const { hash } = window.location + + if (hash === '') { + return + } + + const link = document.querySelector(`.bd-aside a[href="${hash}"]`) + + if (!link) { + return + } + + const active = document.querySelector('.bd-aside .active') + const parent = link.parentNode.parentNode.previousElementSibling + + link.classList.add('active') + + if (parent.classList.contains('collapsed')) { + parent.click() + } + + if (!active) { + return + } + + const expanded = active.parentNode.parentNode.previousElementSibling + + active.classList.remove('active') + + if (expanded && parent !== expanded) { + expanded.click() + } + } + + setActiveItem() + window.addEventListener('hashchange', setActiveItem) + })() diff --git a/src/valentina/webui/static/robots.txt b/src/valentina/webui/static/robots.txt new file mode 100644 index 00000000..3cca4cd4 --- /dev/null +++ b/src/valentina/webui/static/robots.txt @@ -0,0 +1,3 @@ +# TODO: Allow setting this via a config option +User-agent: * +Disallow: / diff --git a/src/valentina/webui/static/spinners/bars.svg b/src/valentina/webui/static/spinners/bars.svg new file mode 100644 index 00000000..6e4f8455 --- /dev/null +++ b/src/valentina/webui/static/spinners/bars.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/valentina/webui/static/spinners/dots.svg b/src/valentina/webui/static/spinners/dots.svg new file mode 100644 index 00000000..2d23ed79 --- /dev/null +++ b/src/valentina/webui/static/spinners/dots.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/valentina/webui/static/spinners/pulse.svg b/src/valentina/webui/static/spinners/pulse.svg new file mode 100644 index 00000000..253e36b2 --- /dev/null +++ b/src/valentina/webui/static/spinners/pulse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/valentina/webui/static/spinners/ring.svg b/src/valentina/webui/static/spinners/ring.svg new file mode 100644 index 00000000..11c2641d --- /dev/null +++ b/src/valentina/webui/static/spinners/ring.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/valentina/webui/static/valentina.css b/src/valentina/webui/static/valentina.css new file mode 100644 index 00000000..2c38338b --- /dev/null +++ b/src/valentina/webui/static/valentina.css @@ -0,0 +1,77 @@ +form pre { + margin-bottom:0; + font-weight:700; +} + +.htmx-indicator{ + display:none; +} +.htmx-request .htmx-indicator{ + display:inline; +} +.htmx-request.htmx-indicator{ + display:inline; +} + +/* apply img-hover class to img tag */ +.img-hover { + -webkit-filter: grayscale(0) blur(0); + filter: grayscale(0) blur(0); + -webkit-transition: .4s ease-in-out; + transition: .4s ease-in-out; +} + +.img-hover:hover { + -webkit-filter: grayscale(10%) blur(1px); + filter: grayscale(10%) blur(1px); +} + + +/* Add the `fade-in` class to any content for a fade-in effect */ +.fade-in { + opacity: 1; + animation-name: fadeInOpacity; + animation-iteration-count: 1; + animation-timing-function: ease-in; + animation-duration: 0.6s; +} + +@keyframes fadeInOpacity { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +/* Add text in middle of horizontal rule */ +.separator { + display: flex; + align-items: center; + text-align: center; + font-size: 1.75rem; + font-weight: 500; + line-height: 1.2; + color: var(--bs-heading-color); + /* margin-block-start: 1em; + margin-block-end: 1em; */ + margin: 1rem 0; + text-transform: uppercase; +} + +.separator::before, +.separator::after { + content: ''; + flex: 1; + border-bottom: 1px solid #333; + opacity: .25; +} + +.separator:not(:empty)::before { + margin-right: .25em; +} + +.separator:not(:empty)::after { + margin-left: .25em; +} diff --git a/src/valentina/webui/static/valentina_avatar.png b/src/valentina/webui/static/valentina_avatar.png new file mode 100644 index 00000000..80dc5bb0 Binary files /dev/null and b/src/valentina/webui/static/valentina_avatar.png differ diff --git a/src/valentina/webui/templates/campaign.jinja b/src/valentina/webui/templates/campaign.jinja new file mode 100644 index 00000000..7da4e14f --- /dev/null +++ b/src/valentina/webui/templates/campaign.jinja @@ -0,0 +1,11 @@ +{# def + campaign: Campaign +#} + +{{ campaign.name }} +{% include "campaign/tabs.html" %} +
+ {# Include the default jinjax template include here for initial page load #} + +
+
diff --git a/src/valentina/webui/templates/campaign/Books.jinja b/src/valentina/webui/templates/campaign/Books.jinja new file mode 100644 index 00000000..e6c37b62 --- /dev/null +++ b/src/valentina/webui/templates/campaign/Books.jinja @@ -0,0 +1,22 @@ +{# def + campaign:Campaign, + books:List[CampaignBook], +#} +

Books & Chapters

+ +{% for book in books | sort(attribute="number") %} + + {{ book.description_long | from_markdown | safe }} + {% if book.chapters %} +

Chapters

+
    + {% for chapter in book.chapters %} +
  1. + {{ chapter.name }} {{ chapter.description_long | from_markdown | safe }} +
  2. + {% endfor %} +
+ {% endif %} +
+{% endfor %} +
diff --git a/src/valentina/webui/templates/campaign/Characters.jinja b/src/valentina/webui/templates/campaign/Characters.jinja new file mode 100644 index 00000000..cf13b740 --- /dev/null +++ b/src/valentina/webui/templates/campaign/Characters.jinja @@ -0,0 +1,23 @@ +{# def + campaign: Campaign, + characters: List[Character] +#} +

Characters & NPCs

+

Player Characters

+ +{% if campaign.npcs %} +

NPCs

+
    + {% for npc in campaign.npcs | sort(attribute="full_name") %} +
  • + {{ npc.name | escape }} {{ npc.description | from_markdown | safe }} +
  • + {% endfor %} +
+{% endif %} diff --git a/src/valentina/webui/templates/campaign/Overview.jinja b/src/valentina/webui/templates/campaign/Overview.jinja new file mode 100644 index 00000000..030fe5b5 --- /dev/null +++ b/src/valentina/webui/templates/campaign/Overview.jinja @@ -0,0 +1,21 @@ +{# def + campaign: Campaign +#} +
+
+ + + + + + + + + +
Danger{{ campaign.danger }}
Desperation{{ campaign.desperation }}
+
+
+

Description

+ {{ (campaign.description | from_markdown | safe) if campaign.description else "

No Description

" }} +
+
diff --git a/src/valentina/webui/templates/campaign/Statistics.jinja b/src/valentina/webui/templates/campaign/Statistics.jinja new file mode 100644 index 00000000..5231a5de --- /dev/null +++ b/src/valentina/webui/templates/campaign/Statistics.jinja @@ -0,0 +1,5 @@ +{# def + campaign:Campaign, + statistics: dict[str, str] +#} + diff --git a/src/valentina/webui/templates/campaign/tabs.html b/src/valentina/webui/templates/campaign/tabs.html new file mode 100644 index 00000000..a64d933a --- /dev/null +++ b/src/valentina/webui/templates/campaign/tabs.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/valentina/webui/templates/character.jinja b/src/valentina/webui/templates/character.jinja new file mode 100644 index 00000000..d4c06e69 --- /dev/null +++ b/src/valentina/webui/templates/character.jinja @@ -0,0 +1,17 @@ +{# def + character:Character, + traits:list[CharacterTrait]=[], + success_msg:str='', +#} + +{% if success_msg %}{% endif %} +{% set link = url_for('character.character_edit', character_id=character.id) %} +
+ {{ character.full_name }} +
+{% include "character/tabs.html" %} +
+ {# Include the default jinjax template include here for initial page load #} + +
+
diff --git a/src/valentina/webui/templates/character/Inventory.jinja b/src/valentina/webui/templates/character/Inventory.jinja new file mode 100644 index 00000000..c6fe4f67 --- /dev/null +++ b/src/valentina/webui/templates/character/Inventory.jinja @@ -0,0 +1,28 @@ +{# def + character:Character, + inventory:list[CharacterInventoryItem]=[] +#} +{# TODO: Add and edit items #} +

Inventory

+
+ {% if not inventory %} +
+

No items in inventory

+
+ {% else %} + {% for key, values in inventory.items() %} +
+

{{ key | capitalize }}

+
+
+ {% for item in values %} +
{{ item.name | from_markdown | safe }}
+
+ {{ item.description | from_markdown | safe }} +
+ {% endfor %} +
+
+ {% endfor %} + {% endif %} +
diff --git a/src/valentina/webui/templates/character/Sheet.jinja b/src/valentina/webui/templates/character/Sheet.jinja new file mode 100644 index 00000000..bd91d493 --- /dev/null +++ b/src/valentina/webui/templates/character/Sheet.jinja @@ -0,0 +1,31 @@ +{# def + character: Character, + traits: list[CharacterTrait] +#} +{# js bootstrap_initialize.js #} +
+ {% for key, value in character.sheet_section_top_items().items() %} +
+

{{ key }}

+

{{ value }}

+
+ {% endfor %} +
+{% for sheet_section, trait_category in traits.items() %} +
{{ sheet_section }}
+
+ {% for category, traits in trait_category.items() %} +
+
+

{{ category | title }}

+
+
+ {% for trait in traits %} +
{{ trait.name | capitalize }}:
+
{{ trait.dots }}
+ {% endfor %} +
+
+ {% endfor %} +
+{% endfor %} diff --git a/src/valentina/webui/templates/character/images.jinja b/src/valentina/webui/templates/character/images.jinja new file mode 100644 index 00000000..21f6c4d7 --- /dev/null +++ b/src/valentina/webui/templates/character/images.jinja @@ -0,0 +1,19 @@ +{# def + character:Character, + images:list[images]=[] +#} +{% if not images %} +

No images

+{% else %} +
+ {% for img_url in images %} +
+ + {{ loop.index }} + +
+ {% endfor %} +
+{% endif %} diff --git a/src/valentina/webui/templates/character/profile.jinja b/src/valentina/webui/templates/character/profile.jinja new file mode 100644 index 00000000..da85dd96 --- /dev/null +++ b/src/valentina/webui/templates/character/profile.jinja @@ -0,0 +1,26 @@ +{# def + character: Character +#} +{% if character.bio %} +

Biography

+ {{ character.bio | from_markdown | safe }} +{% endif %} +{% if character.notes %} +
+

Notes

+
    + {% for note in character.notes %}
  • {{ note.text | from_markdown | safe }}
  • {% endfor %} +
+{% endif %} +{% if character.sheet_sections %} +
+

Sheet Sections

+
+ {% for section in character.sheet_sections | sort(attribute="title") %} +
+

{{ section.title | escape }}

+ {{ section.content | from_markdown | safe }} +
+ {% endfor %} +
+{% endif %} diff --git a/src/valentina/webui/templates/character/statistics.jinja b/src/valentina/webui/templates/character/statistics.jinja new file mode 100644 index 00000000..ea090528 --- /dev/null +++ b/src/valentina/webui/templates/character/statistics.jinja @@ -0,0 +1,5 @@ +{# def + character:Character, + statistics: dict[str, str] +#} + diff --git a/src/valentina/webui/templates/character/tabs.html b/src/valentina/webui/templates/character/tabs.html new file mode 100644 index 00000000..3480f438 --- /dev/null +++ b/src/valentina/webui/templates/character/tabs.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/valentina/webui/templates/character_create_full.jinja b/src/valentina/webui/templates/character_create_full.jinja new file mode 100644 index 00000000..4dd81eec --- /dev/null +++ b/src/valentina/webui/templates/character_create_full.jinja @@ -0,0 +1,5 @@ + +Create Character from Sheet +
{% include "character_create_full/Start.jinja" %}
+
diff --git a/src/valentina/webui/templates/character_create_full/Start.jinja b/src/valentina/webui/templates/character_create_full/Start.jinja new file mode 100644 index 00000000..05377421 --- /dev/null +++ b/src/valentina/webui/templates/character_create_full/Start.jinja @@ -0,0 +1,11 @@ +
+

Get started

+

+ This wizard will guide you through the process of entering a new character into the system. It is recommended that you have already created your character on paper before starting this process. This will make it easier to enter the information into the system. +

+ +
diff --git a/src/valentina/webui/templates/character_create_full/Step1.jinja b/src/valentina/webui/templates/character_create_full/Step1.jinja new file mode 100644 index 00000000..00e28066 --- /dev/null +++ b/src/valentina/webui/templates/character_create_full/Step1.jinja @@ -0,0 +1,9 @@ +{# def + form:QuartForm, + join_label:bool = False, + floating_label:bool = False, + post_url:str = "", +#} +
+ +
diff --git a/src/valentina/webui/templates/character_create_full/Step2.jinja b/src/valentina/webui/templates/character_create_full/Step2.jinja new file mode 100644 index 00000000..00e28066 --- /dev/null +++ b/src/valentina/webui/templates/character_create_full/Step2.jinja @@ -0,0 +1,9 @@ +{# def + form:QuartForm, + join_label:bool = False, + floating_label:bool = False, + post_url:str = "", +#} +
+ +
diff --git a/src/valentina/webui/templates/character_create_full/Step3.jinja b/src/valentina/webui/templates/character_create_full/Step3.jinja new file mode 100644 index 00000000..59ced788 --- /dev/null +++ b/src/valentina/webui/templates/character_create_full/Step3.jinja @@ -0,0 +1,40 @@ +{# def + form:QuartForm, + sheet_traits:dict[dict[dict[str,str]]], + post_url:str, +#} +
+
+ {% for sheet_section, trait_category in sheet_traits.items() if trait_category %} +
+
+

{{ sheet_section }}

+
+
+ {% for category, traits in trait_category.items() if traits %} +
+
+

{{ category | title }}

+
+
+ {% for name, max_value in traits.items() %} +
{{ name | capitalize }}:
+
+ {% set field_name = category~'_'~name~'_'~max_value %} + +
+ {% endfor %} +
+
+ {% endfor %} +
+ {% endfor %} +
+ + +
+
diff --git a/src/valentina/webui/templates/character_create_full/WTForm.jinja b/src/valentina/webui/templates/character_create_full/WTForm.jinja new file mode 100644 index 00000000..b4029955 --- /dev/null +++ b/src/valentina/webui/templates/character_create_full/WTForm.jinja @@ -0,0 +1,61 @@ +{# def + form:QuartForm, + join_label:bool = False, + floating_label:bool = False, + progress:int = None, + post_url:str = "", +#} +{# Determine the encoding type based on the presence of file fields #} +{% set encoding = "multipart/form-data" if form.__iter__() | selectattr("type", "in", ["FileField","MultipleFileField"]) | list else "application/x-www-form-urlencoded" %} +
+ {% if progress %} +
+
{{ progress }}%
+
+ {% endif %} +

{{ form.title }}

+
+ {% if form.form_errors %} + + {% endif %} + {% for field in form %} + {% set valid_css = " is-invalid" if field.errors else " is-valid" if field.raw_data else "" %} + {% if field.type == "SubmitField" %} + + {% elif field.type in ["CSRFTokenField","HiddenField"] %} + {{ field() }} + {% elif field.type == "BooleanField" %} + + {% elif field.type == "RadioField" %} + + {% elif field.type == "ColorField" %} + + {% elif field.type == "IntegerField" %} + + {% elif field.type in ["SelectField", "SelectMultipleField"] %} + + {% elif field.type in ["IntegerRangeField", "DecimalRangeField"] %} + + {% else %} + {# All other field types use text inputs #} + + {% endif %} + {% endfor %} + +
diff --git a/src/valentina/webui/templates/character_edit.jinja b/src/valentina/webui/templates/character_edit.jinja new file mode 100644 index 00000000..87f66b68 --- /dev/null +++ b/src/valentina/webui/templates/character_edit.jinja @@ -0,0 +1,19 @@ +{# def + character:Character, + form:QuartForm, + join_label:bool = False, + floating_label:bool = False, + post_url:str = "", +#} + +{% set link = url_for('character.character_edit', character_id=character.id) %} +
+ Edit {{ character.full_name }} +
+
+
+ +
+
+
diff --git a/src/valentina/webui/templates/character_edit/WTForm.jinja b/src/valentina/webui/templates/character_edit/WTForm.jinja new file mode 100644 index 00000000..aa932cac --- /dev/null +++ b/src/valentina/webui/templates/character_edit/WTForm.jinja @@ -0,0 +1,53 @@ +{# def + form:QuartForm, + join_label:bool = False, + floating_label:bool = False, + post_url:str = "", + character:Character, +#} +
+ {% if form.form_errors %} + + {% endif %} + {% for field in form %} + {% set valid_css = " is-invalid" if field.errors else " is-valid" if field.raw_data else "" %} + {% if field.type == "SubmitField" %} + + {% elif field.type in ["CSRFTokenField","HiddenField"] %} + {{ field() }} + {% elif field.type == "BooleanField" %} + + {% elif field.type == "RadioField" %} + + {% elif field.type == "ColorField" %} + + {% elif field.type == "IntegerField" %} + + {% elif field.type in ["SelectField", "SelectMultipleField"] %} + + {% elif field.type in ["IntegerRangeField", "DecimalRangeField"] %} + + {% else %} + {# All other field types use text inputs #} + + {% endif %} + {% endfor %} + + Cancel + diff --git a/src/valentina/webui/templates/error.html b/src/valentina/webui/templates/error.html new file mode 100644 index 00000000..7696e5b9 --- /dev/null +++ b/src/valentina/webui/templates/error.html @@ -0,0 +1,20 @@ + + + + + + Error {{ status_code }} + + + +
+

{{ status_code }}

+

{{ detail }}

+ Go Back Home +
+ + diff --git a/src/valentina/webui/templates/gameplay.jinja b/src/valentina/webui/templates/gameplay.jinja new file mode 100644 index 00000000..9c22b7bb --- /dev/null +++ b/src/valentina/webui/templates/gameplay.jinja @@ -0,0 +1,15 @@ +{# def + character:Character = None, + campaign:Campaign = None, + dice_sizes:list[int], + form:ValentinaForm, +#} + +Gameplay +
+
+ +
+
+
+
diff --git a/src/valentina/webui/templates/gameplay/FormHeader.jinja b/src/valentina/webui/templates/gameplay/FormHeader.jinja new file mode 100644 index 00000000..2b63fbc1 --- /dev/null +++ b/src/valentina/webui/templates/gameplay/FormHeader.jinja @@ -0,0 +1,69 @@ +{# def + character:Character = None, + campaign:Campaign = None, +#} +{% if character %} + {% set character_id_as_string = character.id | string() %} +{% else %} + {% set character_id_as_string = "" %} +{% endif %} +{% if campaign %} + {% set campaign_id_as_string = campaign.id | string() %} +{% else %} + {% set campaign_id_as_string = "" %} +{% endif %} +
+
+ + {% if session["USER_CHARACTERS"] | length > 1 %} + + {% elif session["USER_CHARACTERS"] | length == 1 %} + {% for character_name, character_id in session['USER_CHARACTERS'].items() %} + {% if loop.first %} + {{ character_name }} + + {% endif %} + {% endfor %} + {% else %} + Please create a character + {% endif %} +
+
+ + {% if session["GUILD_CAMPAIGNS"] | length > 1 %} + + {% elif session["GUILD_CAMPAIGNS"] | length == 1 %} + {% for campaign_name, campaign_id in session['GUILD_CAMPAIGNS'].items() %} + {% if loop.first %} + {{ campaign_name }} + + {% endif %} + {% endfor %} + {% else %} + {# TODO: link to create a campaign #} + Please create a Campaign + {% endif %} +
+
diff --git a/src/valentina/webui/templates/gameplay/FormTabMacros.jinja b/src/valentina/webui/templates/gameplay/FormTabMacros.jinja new file mode 100644 index 00000000..fc8fff85 --- /dev/null +++ b/src/valentina/webui/templates/gameplay/FormTabMacros.jinja @@ -0,0 +1,38 @@ +{# def + macros:list[UserMacro] = [], + campaign:Campaign = None, + form:ValentinaForm, +#} +
+ + +
+
+
+ + +
+
+
+
+
+ +
+ {% if campaign %} +
+ +
+ {% endif %} +
+ +
diff --git a/src/valentina/webui/templates/gameplay/FormTabThrow.jinja b/src/valentina/webui/templates/gameplay/FormTabThrow.jinja new file mode 100644 index 00000000..fc4ea077 --- /dev/null +++ b/src/valentina/webui/templates/gameplay/FormTabThrow.jinja @@ -0,0 +1,47 @@ +{# def + character:Character = None, + campaign:Campaign = None, + dice_sizes:list[int], + form:ValentinaForm, +#} +{% if not campaign and not character %} + +{% elif not campaign %} + +{% elif not character %} + +{% else %} +
+ +
+
+ +
+ {# #} + +
+
+
+ +
+
+ +
+
+ {% if campaign %} +
+
+ +
+
+ {% endif %} + +
+{% endif %} diff --git a/src/valentina/webui/templates/gameplay/FormTabTraits.jinja b/src/valentina/webui/templates/gameplay/FormTabTraits.jinja new file mode 100644 index 00000000..3d852680 --- /dev/null +++ b/src/valentina/webui/templates/gameplay/FormTabTraits.jinja @@ -0,0 +1,52 @@ +{# def + traits:List[CharacterTrait] = [], + campaign:Campaign = None, + form:ValentinaForm, +#} +
+ + +
+
+ +
+ {# #} + +
+
+
+ +
+ +
+
+
+
+
+ +
+ {% if campaign %} +
+ +
+ {% endif %} +
+ +
diff --git a/src/valentina/webui/templates/gameplay/FormWrapper.jinja b/src/valentina/webui/templates/gameplay/FormWrapper.jinja new file mode 100644 index 00000000..b6161657 --- /dev/null +++ b/src/valentina/webui/templates/gameplay/FormWrapper.jinja @@ -0,0 +1,13 @@ +{# def + character:Character = None, + campaign:Campaign = None, + dice_sizes:list[int], + form:ValentinaForm, +#} +
+ + {% include "gameplay/tabs.html" %} +
+ +
+
diff --git a/src/valentina/webui/templates/gameplay/RollResult.jinja b/src/valentina/webui/templates/gameplay/RollResult.jinja new file mode 100644 index 00000000..a6c8261a --- /dev/null +++ b/src/valentina/webui/templates/gameplay/RollResult.jinja @@ -0,0 +1,43 @@ +{# def + character:Character = None, + campaign:Campaign=None, + roll:Diceroll, + result_image_url:str = "", + result_div_class:str = "", + rolled_traits:dict[str,int] = {}, +#} +
+
+

+ You rolled {{ roll.pool }}{{ roll.dice_type.name | lower }} +

+

{{ roll.roll_result_humanized | upper }}

+

{{ roll.num_successes_humanized | title }}

+
Rolled Dice
+

+ {{ roll.dice_as_emoji_images }} + {% if roll.desperation_roll %}+ {{ roll.desperation_dice_as_emoji_images }}{% endif %} +

+ {% if roll.desperation_roll and roll.desperation_botches > 0 %} + {# TODO: Add modal for selecting overreach or entering despair #} +

You botched one of your desperation rolls. You must enter despair or overreach and increase danger by 1.

+ {% endif %} + {% if rolled_traits %} +
Traits Rolled
+
+ {% for key, value in rolled_traits.items() %} +
+

+ {{ key }} +
+ ({{ value }} dice) +

+
+ {% endfor %} +
+ {% endif %} +
+
+ Result Image +
+
diff --git a/src/valentina/webui/templates/gameplay/tabs.html b/src/valentina/webui/templates/gameplay/tabs.html new file mode 100644 index 00000000..fd35bcda --- /dev/null +++ b/src/valentina/webui/templates/gameplay/tabs.html @@ -0,0 +1,5 @@ + + + + + diff --git a/src/valentina/webui/templates/guild_select.jinja b/src/valentina/webui/templates/guild_select.jinja new file mode 100644 index 00000000..d508ef32 --- /dev/null +++ b/src/valentina/webui/templates/guild_select.jinja @@ -0,0 +1,16 @@ +{# def matched_guilds, user #} + +
+

Select A Guild

+

Valentina can only interact with a single guild at a time. Please select the guild you would like to play on.

+
+
+ {% for guild in session["matched_guilds"] | sort(attribute="name" ) %} + + {% endfor %} +
+
+
+
diff --git a/src/valentina/webui/templates/homepage.jinja b/src/valentina/webui/templates/homepage.jinja new file mode 100644 index 00000000..e93a585c --- /dev/null +++ b/src/valentina/webui/templates/homepage.jinja @@ -0,0 +1,90 @@ +{# def + homepage_description:str, +#} + +{% if "USER_NAME" not in session or not session["USER_NAME"] %} +
+
+
+ Bootstrap Themes +
+
+

Valentina Noir

+

{{ homepage_description }}

+
+ Log In +
+
+
+
+{% else %} + {% set card_style = "card h-100 border border-2 shadow bg-light-subtle" %} + Welcome {{ session["USER_NAME"] }} +
+ +
+
+

View your characters

+
+
+ +
+ {# djlint:off J018 #} + Go +
+
+
+

Or create a new character

+
+ Create +
+
+
+ +

View the guild's campaigns

+
+
+ +
+ {# djlint:off J018 #} + Go +
+
+ +

Roll dice for your characters.

+ Go +
+
+{% endif %} +
diff --git a/src/valentina/webui/utils/__init__.py b/src/valentina/webui/utils/__init__.py new file mode 100644 index 00000000..f28e438c --- /dev/null +++ b/src/valentina/webui/utils/__init__.py @@ -0,0 +1,19 @@ +"""Helper functions for the webui.""" + +from .helpers import ( + fetch_active_campaign, + fetch_active_character, + fetch_campaigns, + fetch_user, + fetch_user_characters, + update_session, +) + +__all__ = [ + "fetch_active_campaign", + "fetch_active_character", + "fetch_campaigns", + "fetch_user_characters", + "fetch_user", + "update_session", +] diff --git a/src/valentina/webui/utils/discord.py b/src/valentina/webui/utils/discord.py new file mode 100644 index 00000000..b5aa6093 --- /dev/null +++ b/src/valentina/webui/utils/discord.py @@ -0,0 +1,109 @@ +"""Utilities to allow the webui to interact with Discord.""" + +import inspect +from datetime import UTC, datetime +from typing import Literal + +from flask_discord.models import User as FlaskDiscordUser +from loguru import logger + +from valentina.constants import EmbedColor +from valentina.models import User +from valentina.webui import discord_oauth + +from .helpers import fetch_guild, fetch_user + + +def log_to_logfile(msg: str, level: str = "INFO", user: User = None) -> None: # pragma: no cover + """Log the command to the console and log file.""" + username = f"@{user.name}" if user else "" + + if inspect.stack()[1].function == "log_message" and inspect.stack()[2].function in { + "post_to_audit_log", + "post_to_error_log", + }: + name1 = inspect.stack()[3].filename.split("/")[-3].split(".")[0] + name2 = inspect.stack()[3].filename.split("/")[-2].split(".")[0] + name3 = inspect.stack()[3].filename.split("/")[-1].split(".")[0] + new_name = f"{name1}.{name2}.{name3}" + elif inspect.stack()[1].function == "log_message": + name1 = inspect.stack()[2].filename.split("/")[-3].split(".")[0] + name2 = inspect.stack()[2].filename.split("/")[-2].split(".")[0] + name3 = inspect.stack()[2].filename.split("/")[-1].split(".")[0] + new_name = f"{name1}.{name2}.{name3}" + else: + name1 = inspect.stack()[1].filename.split("/")[-3].split(".")[0] + name2 = inspect.stack()[1].filename.split("/")[-2].split(".")[0] + name3 = inspect.stack()[1].filename.split("/")[-1].split(".")[0] + new_name = f"{name1}.{name2}.{name3}" + + logger.patch(lambda r: r.update(name=new_name)).log( # type: ignore [call-arg] + level.upper(), f"{msg} [{username}]" + ) + + +async def log_message( + log_type: Literal["audit", "error"], + msg: str, + level: str = "INFO", + view: str = "", +) -> None: + """Log the message to the console, log file, and Discord.""" + user = await fetch_user() + log_to_logfile(msg, level, user) + + guild = await fetch_guild() + if not guild: + return None + + channel = guild.channels.error_log if log_type == "error" else guild.channels.audit_log + if not channel: + return None + + footer = "" + footer += f"User: @{user.name}" if user and user.name else "" + footer += " | " if user and user.name and view else "" + footer += f"WebUI: {view}" if view else "" + + return discord_oauth.bot_request( + f"/channels/{channel}/messages", + "POST", + json={ + "embeds": [ + { + "color": EmbedColor[level.upper()].value, + "title": msg, + "description": "", + "footer": {"text": f"{footer}"}, + "timestamp": str(datetime.now(UTC)), + } + ], + }, + ) + + +async def post_to_audit_log(msg: str, level: str = "INFO", view: str = "") -> None: + """Send a message to the audit log channel for a guild.""" + await log_message("audit", msg, level, view) + + +async def post_to_error_log(msg: str, level: str = "ERROR", view: str = "") -> None: + """Send a message to the error log channel for a guild.""" + await log_message("error", msg, level, view) + + +async def send_user_dm(user: FlaskDiscordUser, message: str) -> dict | str: + """Send private message message in Discord to a user. + + Args: + user (FlaskDiscordUser): The user to send the message to. + message (str): The message to send. + """ + dm_channel = discord_oauth.bot_request( + "/users/@me/channels", "POST", json={"recipient_id": user.id} + ) + return discord_oauth.bot_request( + f"/channels/{dm_channel['id']}/messages", + "POST", + json={"content": message}, + ) diff --git a/src/valentina/webui/utils/errors.py b/src/valentina/webui/utils/errors.py new file mode 100644 index 00000000..17ed66aa --- /dev/null +++ b/src/valentina/webui/utils/errors.py @@ -0,0 +1,25 @@ +"""Custom errors for the webui.""" + +from flask_discord import Unauthorized +from quart import Quart, redirect, render_template, url_for +from werkzeug.exceptions import HTTPException +from werkzeug.wrappers.response import Response + + +def register_error_handlers(app: Quart) -> None: + """Register error handlers for the app.""" + + @app.errorhandler(Unauthorized) + async def redirect_unauthorized(e: type[Exception] | int) -> Response: # noqa: ARG001 + """Redirect unauthorized users to the login page.""" + return redirect(url_for("oauth.login")) + + @app.errorhandler(HTTPException) + async def error_handler(exc: HTTPException) -> str: + """Use a custom error handler for HTTP exceptions.""" + return await render_template( + "error.html", + detail=exc.description, + status_code=exc.code, + page_title=f"{exc.code} Error", + ) diff --git a/src/valentina/webui/utils/helpers.py b/src/valentina/webui/utils/helpers.py new file mode 100644 index 00000000..8f471279 --- /dev/null +++ b/src/valentina/webui/utils/helpers.py @@ -0,0 +1,178 @@ +"""Helpers for the webui.""" + +from loguru import logger +from quart import session + +from valentina.models import Campaign, Character, Guild, User +from valentina.utils import ValentinaConfig, console + + +async def fetch_active_campaign( + campaign_id: str = "", fetch_links: bool = False +) -> Campaign | None: + """Update and return the active campaign from the session.""" + if len(session["GUILD_CAMPAIGNS"]) == 0: + return None + + if len(session["GUILD_CAMPAIGNS"]) == 1: + return await Campaign.get( + next(iter(session["GUILD_CAMPAIGNS"].values())), fetch_links=fetch_links + ) + + existing_campaign_id = session.get("ACTIVE_CAMPAIGN_ID", None) + + if not campaign_id: + if existing_campaign_id: + return await Campaign.get(existing_campaign_id, fetch_links=fetch_links) + + return None + + if existing_campaign_id == campaign_id: + return await Campaign.get(campaign_id, fetch_links=fetch_links) + + session["ACTIVE_CAMPAIGN_ID"] = campaign_id + return await Campaign.get(campaign_id, fetch_links=fetch_links) + + +async def fetch_active_character( + character_id: str = "", fetch_links: bool = False +) -> Character | None: + """Update and return the active character from the session.""" + if len(session["USER_CHARACTERS"]) == 0: + return None + + if len(session["USER_CHARACTERS"]) == 1: + return await Character.get( + next(iter(session["USER_CHARACTERS"].values())), fetch_links=fetch_links + ) + + existing_character_id = session.get("ACTIVE_CHARACTER_ID", None) + + if not character_id: + if existing_character_id: + return await Character.get(existing_character_id, fetch_links=fetch_links) + + return None + + if existing_character_id == character_id: + return await Character.get(character_id, fetch_links=fetch_links) + + session["ACTIVE_CHARACTER_ID"] = character_id + return await Character.get(character_id, fetch_links=fetch_links) + + +async def fetch_guild(fetch_links: bool = False) -> Guild: + """Fetch the database Guild based on Discord guild_id from the session. Updates the session with the guild name. + + Args: + session (SessionMixin): The session to fetch the guild from. + fetch_links (bool): Whether to fetch the database linked objects. + """ + # Guard clause to prevent mangled session data + if not session.get("GUILD_ID", None): + session.clear() + return None + + guild = await Guild.get(session["GUILD_ID"], fetch_links=fetch_links) + + if session.get("GUILD_NAME", None) != guild.name: + session["GUILD_NAME"] = guild.name + + return guild + + +async def fetch_user(fetch_links: bool = False) -> User: + """Fetch the database User based on Discord user_id from the session. + + Args: + fetch_links (bool): Whether to fetch the database linked objects. + session (SessionMixin): The session to fetch the user from. + """ + # Guard clause to prevent mangled session data + if not session.get("USER_ID", None): + session.clear() + return None + + user = await User.get(session["USER_ID"], fetch_links=fetch_links) + + if session.get("USER_NAME", None) != user.name: + logger.warning("Updating session with user name") + session["USER_NAME"] = user.name + + if session.get("USER_AVATAR_URL", None) != user.avatar_url: + logger.warning("Updating session with user avatar") + session["USER_AVATAR_URL"] = user.avatar_url + + return user + + +async def fetch_user_characters(fetch_links: bool = True) -> list[Character]: + """Fetch the user's characters and return them as a list. Updates the session with a dictionary of character names and ids. + + Args: + fetch_links (bool): Whether to fetch the database linked objects. + session (SessionMixin): The session to fetch the characters from. + """ + # Guard clause to prevent mangled session data + if not session.get("USER_ID", None) or not session.get("GUILD_ID", None): + session.clear() + return [] + + characters = await Character.find( + Character.user_owner == session["USER_ID"], + Character.guild == session["GUILD_ID"], + Character.type_player == True, # noqa: E712 + fetch_links=fetch_links, + ).to_list() + + character_dict = dict(sorted({x.name: str(x.id) for x in characters}.items())) + if session.get("USER_CHARACTERS", None) != character_dict: + logger.warning("Updating session with characters") + session["USER_CHARACTERS"] = character_dict + + return characters + + +async def fetch_campaigns(fetch_links: bool = True) -> list[Campaign]: + """Fetch the guild's campaign and return them as a list. Updates the session with a dictionary of campaign names and ids. + + Args: + fetch_links (bool): Whether to fetch the database linked objects. + session (SessionMixin): The session to fetch the characters from. + """ + # Guard clause to prevent mangled session data + if not session.get("GUILD_ID", None): + session.clear() + return [] + + campaigns = await Campaign.find( + Campaign.guild == session["GUILD_ID"], + Campaign.is_deleted == False, # noqa: E712 + fetch_links=fetch_links, + ).to_list() + + campaigns_dict = dict(sorted({x.name: str(x.id) for x in campaigns}.items())) + if session.get("GUILD_CAMPAIGNS", None) != campaigns_dict: + logger.warning("Updating session with campaigns") + session["GUILD_CAMPAIGNS"] = campaigns_dict + + return campaigns + + +async def update_session() -> None: + """Make updates to the session based on the user's current state. + + Args: + session (SessionMixin): The session to update. + """ + logger.debug("Updating session") + await fetch_guild(fetch_links=False) + await fetch_user(fetch_links=False) + await fetch_user_characters(fetch_links=False) + await fetch_campaigns(fetch_links=False) + + if ValentinaConfig().webui_log_level.upper() in ["DEBUG", "TRACE"]: + console.rule("Session") + for key, value in session.items(): + console.log(f"{key}={value}") + console.rule() diff --git a/src/valentina/webui/utils/jinja_filters.py b/src/valentina/webui/utils/jinja_filters.py new file mode 100644 index 00000000..190fad75 --- /dev/null +++ b/src/valentina/webui/utils/jinja_filters.py @@ -0,0 +1,18 @@ +"""Custom Jinja2 filters for Valentina web UI.""" + +from markdown2 import markdown +from markupsafe import escape +from quart import Quart + + +def from_markdown(value: str) -> str: + """Convert a Markdown string to HTML.""" + value = escape(value) + return markdown(value) + + +def register_filters(app: Quart) -> Quart: + """Register custom Jinja2 filters for the app.""" + app.jinja_env.filters["from_markdown"] = from_markdown + + return app diff --git a/src/valentina/webui/utils/jinjax.py b/src/valentina/webui/utils/jinjax.py new file mode 100644 index 00000000..1aee8480 --- /dev/null +++ b/src/valentina/webui/utils/jinjax.py @@ -0,0 +1,21 @@ +"""Configure Jinjax for Valentina.""" + +from pathlib import Path + +import jinjax +from quart import Quart + +from .jinja_filters import from_markdown + + +def register_jinjax_catalog(app: Quart) -> jinjax.Catalog: + """Register the JinJax catalog with the app.""" + catalog = jinjax.Catalog(jinja_env=app.jinja_env) + catalog.add_folder(Path(__file__).parent.parent / "components") + catalog.add_folder(Path(__file__).parent.parent / "templates") + catalog.jinja_env.filters.update({"from_markdown": from_markdown}) + catalog.jinja_env.trim_blocks = True + catalog.jinja_env.lstrip_blocks = True + app.jinja_env.globals["catalog"] = catalog + + return catalog diff --git a/src/valentina/webui/views/__init__.py b/src/valentina/webui/views/__init__.py new file mode 100644 index 00000000..16576e35 --- /dev/null +++ b/src/valentina/webui/views/__init__.py @@ -0,0 +1,25 @@ +"""View models for the Valentina web UI.""" + +from .campaign_view import CampaignView +from .character_create_full import ( + CreateCharacterStart, + CreateCharacterStep1, + CreateCharacterStep2, + CreateCharacterStep3, +) +from .character_view import CharacterEdit, CharacterView +from .gameplay import DiceRollView, GameplayView +from .homepage import HomepageView + +__all__ = [ + "CampaignView", + "CharacterView", + "CreateCharacterStart", + "CreateCharacterStep1", + "CreateCharacterStep2", + "CreateCharacterStep3", + "CharacterEdit", + "DiceRollView", + "GameplayView", + "HomepageView", +] diff --git a/src/valentina/webui/views/campaign_view.py b/src/valentina/webui/views/campaign_view.py new file mode 100644 index 00000000..fd49597e --- /dev/null +++ b/src/valentina/webui/views/campaign_view.py @@ -0,0 +1,58 @@ +"""Campaign view.""" + +from typing import ClassVar + +from flask_discord import requires_authorization +from quart import abort, request, session +from quart.views import MethodView + +from valentina.models import Campaign, Statistics +from valentina.webui import catalog + + +class CampaignView(MethodView): + """View to handle campaign operations.""" + + decorators: ClassVar = [requires_authorization] + + def __init__(self) -> None: + self.session = session # Assuming session is defined globally or passed in some way + + async def handle_tabs(self, campaign: Campaign) -> str: + """Handle HTMX tabs for the campaign view.""" + if request.args.get("tab") == "overview": + return catalog.render("campaign.Overview", campaign=campaign) + + if request.args.get("tab") == "books": + return catalog.render( + "campaign.Books", campaign=campaign, books=await campaign.fetch_books() + ) + + if request.args.get("tab") == "characters": + return catalog.render( + "campaign.Characters", + campaign=campaign, + characters=await campaign.fetch_characters(), + ) + + if request.args.get("tab") == "statistics": + stats_engine = Statistics(guild_id=session["GUILD_ID"]) + return catalog.render( + "campaign.Statistics", + campaign=campaign, + statistics=await stats_engine.campaign_statistics(campaign, as_json=True), + ) + + return abort(404) + + @requires_authorization + async def get(self, campaign_id: str = "") -> str: + """Handle GET requests.""" + campaign = await Campaign.get(campaign_id, fetch_links=True) + if not campaign: + abort(401) + + if request.headers.get("HX-Request"): + return await self.handle_tabs(campaign) + + return catalog.render("campaign", campaign=campaign) diff --git a/src/valentina/webui/views/character_create_full.py b/src/valentina/webui/views/character_create_full.py new file mode 100644 index 00000000..4e26792e --- /dev/null +++ b/src/valentina/webui/views/character_create_full.py @@ -0,0 +1,360 @@ +"""Form for creating a full character.""" + +from datetime import UTC, datetime, timedelta +from typing import ClassVar + +from beanie import WriteRules +from flask_discord import requires_authorization +from loguru import logger +from quart import abort, redirect, request, session, url_for +from quart.views import MethodView +from quart_wtf import QuartForm +from werkzeug.wrappers.response import Response + +from valentina.constants import CharSheetSection, TraitCategory +from valentina.models import Character, CharacterTrait +from valentina.utils.helpers import get_max_trait_value +from valentina.webui import catalog +from valentina.webui.utils.discord import post_to_audit_log, post_to_error_log +from valentina.webui.utils.helpers import update_session +from valentina.webui.WTForms.character_create_full import ( + CharacterCreateFullStep1, + HunterClassSpecifics, + VampireClassSpecifics, + WerewolfClassSpecifics, +) + +from .valentina_forms import ValentinaForm + + +class FormSessionManager: + """Manage session data for the character creation forms. Creates a dict of data which can be read from and written to across different forms in the character creation process.""" + + def __init__(self) -> None: + self.key = "CharacterCreateFullData" + + def _clear_if_expired(self) -> None: + """Clear the session data if it has expired.""" + if self.key not in session: + return + + expires = session[self.key].get("expires", None) + now = datetime.now(UTC) + if expires and expires < now: + logger.debug("Form data expired, clearing from session") + self.clear_data() + + def write_data(self, data: dict) -> None: + """Write data to the session.""" + self._clear_if_expired() + + if self.key not in session: + session[self.key] = {} + + if not session[self.key].get("expires"): + now = datetime.now(UTC) + now_plus_10 = now + timedelta(minutes=10) + session[self.key]["expires"] = now_plus_10 + + session[self.key].update(data) + + def read_data(self) -> dict: + """Read data from the session.""" + self._clear_if_expired() + return session.get(self.key, {}) + + def clear_data(self) -> None: + """Clear the data from the session.""" + session.pop(self.key, None) + + +class CreateCharacterStart(MethodView): + """Create a character step 1.""" + + decorators: ClassVar = [requires_authorization] + + async def get(self) -> str: + """Process initial page load.""" + return catalog.render("character_create_full") + + +class CreateCharacterStep1(MethodView): + """Create a character step 1. Loads HTMX partials for the first form.""" + + decorators: ClassVar = [requires_authorization] + + def __init__(self) -> None: + self.is_htmx = bool(request.headers.get("Hx-Request", False)) + self.session_data = FormSessionManager() + self.join_label = True + + async def get(self) -> str: + """Process initial page load.""" + form = await CharacterCreateFullStep1().create_form() + + # If form data for this step is already in the session, populate the form with it. Usefed for going "back" in the form process. + if form_data := self.session_data.read_data(): + form.process(data=form_data) + + return catalog.render( + "character_create_full.Step1", + form=form, + join_label=self.join_label, + post_url=url_for("character.create_full_1"), + ) + + async def post(self) -> str | Response: + """Process form responses.""" + form = await CharacterCreateFullStep1().create_form() + if await form.validate_on_submit(): + character = Character( + campaign=session.get("ACTIVE_CAMPAIGN_ID", None), + guild=session.get("GUILD_ID", None), + name_first=form.data.get("firstname") if form.data.get("firstname") else None, + name_last=form.data.get("lastname") if form.data.get("lastname") else None, + name_nick=form.data.get("nickname") if form.data.get("nickname") else None, + char_class_name=form.data.get("char_class"), + type_player=True, + user_creator=session.get("USER_ID", None), + user_owner=session.get("USER_ID", None), + demeanor=form.data.get("demeanor") if form.data.get("demeanor") else None, + nature=form.data.get("nature") if form.data.get("nature") else None, + dob=form.data.get("dob") if form.data.get("dob") else None, + ) + new_char = await character.save() + form.data["char_id"] = str(new_char.id) + + # Write the form data to the session + self.session_data.write_data(form.data) + + # Redirect to next step + char_class = form.data.get("char_class").lower() + match char_class: + case "vampire" | "werewolf" | "hunter": + route = "character.create_full_2" + case _: + route = "character.create_full_3" + + return redirect(url_for(route, character_id=str(new_char.id), char_class=char_class)) + + # If POST request does not validate, return errors + return catalog.render( + "character_create_full.Step1", + form=form, + join_label=self.join_label, + post_url=url_for("character.create_full_1"), + ) + + +class CreateCharacterStep2(MethodView): + """Create a character step 2. Loads HTMX partials for class specific items.""" + + decorators: ClassVar = [requires_authorization] + + def __init__(self) -> None: + self.is_htmx = bool(request.headers.get("Hx-Request", False)) + self.session_data = FormSessionManager() + self.join_label = True + + def _get_class_specific_form(self, char_class: str) -> QuartForm | None: + """Return the form for the selected class specific items.""" + if char_class: + match char_class.lower(): + case "vampire": + return VampireClassSpecifics() + case "hunter": + return HunterClassSpecifics() + case "werewolf": + return WerewolfClassSpecifics() + + return None + + async def get(self, character_id: str, char_class: str) -> str: + """Process initial page load.""" + if not character_id or not char_class: + await post_to_error_log( + msg="No character ID or char_class provided to CreateCharacterStep2", + view=self.__class__.__name__, + ) + abort(400) + + class_form = self._get_class_specific_form(char_class) + form = await class_form.create_form() + + # If form data for this step is already in the session, populate the form with it. Used for going "back" in the form process. + if form_data := self.session_data.read_data(): + form.process(data=form_data) + + return catalog.render( + "character_create_full.Step2", + form=form, + join_label=self.join_label, + post_url=url_for( + "character.create_full_2", character_id=character_id, char_class=char_class + ), + ) + + async def post(self, character_id: str, char_class: str) -> str | Response: + """Process form responses.""" + if not character_id or not char_class: + await post_to_error_log( + msg="No character ID or char_class provided to CreateCharacterStep2", + view=self.__class__.__name__, + ) + abort(400) + + class_form = self._get_class_specific_form(char_class) + form = await class_form.create_form() + + if await form.validate_on_submit(): + if not character_id: + await post_to_error_log( + msg="No character ID provided to CreateCharacterStep2", + view=self.__class__.__name__, + ) + abort(401) + + character = await Character.get(character_id) + + character.clan_name = form.data.get("clan_name") if form.data.get("clan_name") else None + character.sire = form.data.get("sire") if form.data.get("sire") else None + character.generation = ( + form.data.get("generation") if form.data.get("generation") else None + ) + character.breed = form.data.get("breed") if form.data.get("breed") else None + character.auspice = form.data.get("auspice") if form.data.get("auspice") else None + character.tribe = form.data.get("tribe") if form.data.get("tribe") else None + character.creed_name = form.data.get("creed") if form.data.get("creed") else None + await character.save() + + # Write the form data to the session + self.session_data.write_data(form.data) + + # Redirect to next step + return redirect(url_for("character.create_full_3", character_id=character_id)) + + # If form data for this step is already in the session, populate the form with it. Used for going "back" in the form process. + if form_data := self.session_data.read_data(): + form.process(data=form_data) + + # If POST request does not validate, return errors + return catalog.render( + "character_create_full.Step2", + form=form, + join_label=self.join_label, + post_url=url_for( + "character.create_full_2", character_id=character_id, char_class=char_class + ), + ) + + +class CreateCharacterStep3(MethodView): + """Create a character step 3. Loads HTMX partials for the third form - traits.""" + + decorators: ClassVar = [requires_authorization] + + def __init__(self) -> None: + self.is_htmx = bool(request.headers.get("Hx-Request", False)) + self.session_data = FormSessionManager() + self.join_label = True + self.char_class = request.args.get("char_class", None) + self.form = ValentinaForm( + hx_validate=False, + join_labels=True, + title="Character Traits", + description="Enter the traits for your character.", + ) + + async def _fetch_trait_names(self, category: TraitCategory, character: Character) -> list[str]: + """Fetch the trait names for the selected category.""" + return list(category.value.COMMON) + list( + getattr(category.value, character.char_class_name) + ) + + async def _fetch_sheet_traits( + self, character: Character + ) -> dict[str, dict[str, dict[str, int]]]: + """Fetch the traits for the character sheet.""" + sheet_traits: dict[str, dict[str, dict[str, int]]] = {} + for sheet_section in sorted(CharSheetSection, key=lambda x: x.value["order"]): + sheet_traits[sheet_section.name] = {} + + trait_categories = sorted( + [x for x in TraitCategory if x.value.section == sheet_section], + key=lambda x: x.value.order, + ) + for category in trait_categories: + if not sheet_traits[sheet_section.name].get(category.name): + sheet_traits[sheet_section.name][category.name] = {} + + traits = await self._fetch_trait_names(category=category, character=character) + for trait_name in traits: + sheet_traits[sheet_section.name][category.name][trait_name] = ( + get_max_trait_value(trait_name, category.name) + ) + + return sheet_traits + + async def get(self, character_id: str) -> str: + """Process initial page load.""" + if not character_id: + await post_to_error_log( + msg="No character ID provided to CreateCharacterStep3", + view=self.__class__.__name__, + ) + abort(400) + + character = await Character.get(character_id, fetch_links=True) + sheet_traits = await self._fetch_sheet_traits(character) + + return catalog.render( + "character_create_full.Step3", + form=self.form, + sheet_traits=sheet_traits, + post_url=url_for("character.create_full_3", character_id=character_id), + ) + + async def post(self, character_id: str) -> str | Response: + """Process form responses.""" + if not character_id: + await post_to_error_log( + msg="No character ID provided to CreateCharacterStep3", + view=self.__class__.__name__, + ) + abort(400) + + character = await Character.get(character_id, fetch_links=True) + form = await request.form + + traits_to_add = [] + for k, value in form.items(): + category, name, max_value = k.split("_") + traits_to_add.append( + CharacterTrait( + name=name, + value=int(value), + category_name=category, + character=str(character_id), + max_value=int(max_value), + ) + ) + + character.traits = traits_to_add + await character.save(link_rule=WriteRules.WRITE) + self.session_data.clear_data() + + # Log the result + await post_to_audit_log( + msg=f"New character created: {character.full_name}", + view=self.__class__.__name__, + ) + + # Rebuild the session with the new character data + await update_session() + return redirect( + url_for( + "character.character_view", + character_id=character_id, + success_msg="Character created successfully!", + ) + ) diff --git a/src/valentina/webui/views/character_view.py b/src/valentina/webui/views/character_view.py new file mode 100644 index 00000000..c99c0f57 --- /dev/null +++ b/src/valentina/webui/views/character_view.py @@ -0,0 +1,307 @@ +"""View character.""" + +from typing import ClassVar + +from flask_discord import requires_authorization +from loguru import logger +from markupsafe import escape +from quart import abort, render_template, request, session, url_for +from quart.views import MethodView +from quart_wtf import QuartForm +from werkzeug.wrappers.response import Response + +from valentina.constants import CharSheetSection, InventoryItemType, TraitCategory +from valentina.models import AWSService, Character, CharacterTrait, InventoryItem, Statistics +from valentina.webui import catalog +from valentina.webui.utils.discord import post_to_audit_log +from valentina.webui.utils.helpers import update_session +from valentina.webui.WTForms import fields +from valentina.webui.WTForms.character_edit import EmptyForm + + +class CharacterView(MethodView): + """View to handle character operations.""" + + decorators: ClassVar = [requires_authorization] + + def __init__(self) -> None: + self.session = session # Assuming session is defined globally or passed in some way + + async def get_character_object(self, character_id: str) -> Character: + """Get a character db object by ID. + + Args: + character_id (str): The character ID to fetch. + + Returns: + Character: The character object. + """ + try: + character = await Character.get(character_id, fetch_links=True) + except ValueError: + abort(406) + + if not character: + abort(404) + + return character + + async def get_character_sheet_traits( + self, character: Character + ) -> dict[str, dict[str, list[CharacterTrait]]]: + """Returns all character traits grouped by character sheet section and category.""" + character_traits = await CharacterTrait.find( + CharacterTrait.character == str(character.id) + ).to_list() + + sheet_traits: dict[str, dict[str, list[CharacterTrait]]] = {} + + for section in sorted(CharSheetSection, key=lambda x: x.value["order"]): + if section != CharSheetSection.NONE: + sheet_traits[section.name] = {} + + # Sort by trait category + for cat in sorted( + [x for x in TraitCategory if x.value.section == section], + key=lambda x: x.value.order, + ): + for x in character_traits: + if x.category_name == cat.name and not ( + x.value == 0 and not cat.value.show_zero + ): + try: + sheet_traits[section.name][x.category_name].append(x) + except KeyError: + sheet_traits[section.name][x.category_name] = [x] + + return sheet_traits + + async def get_character_inventory(self, character: Character) -> dict: + """Get the character's inventory.""" + inventory: dict[str, list[InventoryItem]] = {} + for x in InventoryItemType: + inventory[x.name] = [] + + for item in await InventoryItem.find( + InventoryItem.character == str(character.id) + ).to_list(): + inventory[item.type].append(item) + + # remove all empty dictionary entries + return {k: v for k, v in inventory.items() if v} + + async def process_form_data(self, character: Character) -> None: + """Process form data and update character attributes.""" + form = await request.form + + # Iterate over all form fields and update character attributes if they exist and are not "None" + for key, value in form.items(): + if hasattr(character, key): + v = value if value != "" else None + if getattr(character, key) != v: + setattr(character, key, escape(v) if v else None) + + if not hasattr(character, key): + logger.warning(f"Character attribute {key} not found.") + + # TODO: Implement channel renaming + + await character.save() + + async def get_character_image_urls(self, character: Character) -> list[str]: + """Get image URLs for a character.""" + aws_svc = AWSService() + + return [aws_svc.get_url(x) for x in character.images] + + async def handle_tabs(self, character: Character) -> str: + """Handle htmx tab requests.""" + if request.args.get("tab") == "sheet": + return catalog.render( + "character.Sheet", + character=character, + traits=await self.get_character_sheet_traits(character), + ) + + if request.args.get("tab") == "inventory": + return catalog.render( + "character.Inventory", + character=character, + inventory=await self.get_character_inventory(character), + ) + + if request.args.get("tab") == "profile": + return catalog.render("character.profile", character=character) + + if request.args.get("tab") == "images": + return catalog.render( + "character.Images", + character=character, + images=await self.get_character_image_urls(character), + ) + + if request.args.get("tab") == "statistics": + stats_engine = Statistics(guild_id=session["GUILD_ID"]) + + return catalog.render( + "character.Statistics", + character=character, + statistics=await stats_engine.character_statistics(character, as_json=True), + ) + + return abort(404) + + async def get(self, character_id: str = "") -> str: + """Handle GET requests.""" + success_msg = request.args.get("success_msg") + character = await self.get_character_object(character_id) + + if request.headers.get("HX-Request"): + return await self.handle_tabs(character) + + return catalog.render( + "character", + character=character, + traits=await self.get_character_sheet_traits(character), + success_msg=success_msg, + ) + + async def post(self, character_id: str = "") -> str: + """Handle POST requests.""" + character = await self.get_character_object(character_id) + traits = await self.get_character_sheet_traits(character) + inventory = await self.get_character_inventory(character) + stats_engine = Statistics(guild_id=session["GUILD_ID"]) + statistics = await stats_engine.character_statistics(character, as_json=True) + + await self.process_form_data(character) + + return await render_template( + "character.html", + character=character, + traits=traits, + inventory_item_types=inventory, + args=request.args, + statistics=statistics, + ) + + +class CharacterEdit(MethodView): + """View to handle character field edits. Serves individual field forms for editing character attributes.""" + + decorators: ClassVar = [requires_authorization] + + def __init__(self) -> None: + self.join_label = True + self.floating_label = False + + async def _build_form(self, character: Character) -> QuartForm: + """Build the character edit form.""" + data_from_db = { + "character_id": character.id, + "name_first": character.name_first if character.name_first else "", + "name_last": character.name_last if character.name_last else "", + "name_nick": character.name_nick if character.name_nick else "", + "demeanor": character.demeanor if character.demeanor else "", + "nature": character.nature if character.nature else "", + "dob": character.dob if character.dob else "", + "sire": character.sire if character.sire else "", + "generation": character.generation if character.generation else "", + "tribe": character.tribe if character.tribe else "", + "auspice": character.auspice if character.auspice else "", + "breed": character.breed if character.breed else "", + } + + EmptyForm.character_id = fields.character_id + EmptyForm.name_first = fields.name_first + EmptyForm.name_last = fields.name_last + EmptyForm.name_nick = fields.name_nick + EmptyForm.demeanor = fields.demeanor + EmptyForm.nature = fields.nature + EmptyForm.dob = fields.dob + + if character.char_class_name.lower() == "vampire": + EmptyForm.sire = fields.sire + EmptyForm.generation = fields.generation + + if character.char_class_name.lower() == "werewolf": + EmptyForm.tribe = fields.tribe + EmptyForm.auspice = fields.auspice + EmptyForm.breed = fields.breed + + return await EmptyForm().create_form(data=data_from_db) + + async def get(self, character_id: str) -> str: + """Handle GET requests.""" + character = await Character.get(character_id) + if not character: + abort(404) + + form = await self._build_form(character) + + return catalog.render( + "character_edit", + character=character, + form=form, + join_label=self.join_label, + floating_label=self.floating_label, + ) + + async def post(self, character_id: str) -> str | Response: + """Handle POST requests.""" + character = await Character.get(character_id) + if not character: + abort(404) + + form = await self._build_form(character) + if await form.validate_on_submit(): + form_data = { + k: v if v else None + for k, v in form.data.items() + if k not in {"submit", "character_id", "csrf_token"} + } + + # Iterate over all form fields and update character attributes if they exist and are not "None" + has_updates = False + for key in form_data: + if (not form_data[key] and getattr(character, key)) or ( + form_data[key] and form_data[key] != getattr(character, key) + ): + # dob field from form is datetime.date, but dob field from character is datetime.datetime. convert the character dob to date() for comparison + if ( + key == "dob" + and getattr(character, key) + and form_data[key] == getattr(character, key).date() + ): + continue + + has_updates = True + setattr(character, key, form_data[key]) + + if has_updates: + await character.save() + await post_to_audit_log( + msg=f"Character {character.name} edited", + view=self.__class__.__name__, + ) + + # Rebuild the session with the new character data + await update_session() + + # Redirect to the character view via htmx-redirect + response = Response() + response.headers["hx-redirect"] = url_for( + "character.character_view", + character_id=character_id, + success_msg="Character updated!" if has_updates else "No changes made.", + ) + return response + + # If POST request does not validate, return errors + return catalog.render( + "character_edit.WTForm", + form=form, + join_label=self.join_label, + floating_label=self.floating_label, + character=character, + ) diff --git a/src/valentina/webui/views/gameplay.py b/src/valentina/webui/views/gameplay.py new file mode 100644 index 00000000..d52a3abc --- /dev/null +++ b/src/valentina/webui/views/gameplay.py @@ -0,0 +1,224 @@ +"""Gameplay views for dicerolling.""" + +import json +from typing import ClassVar + +from flask_discord import requires_authorization +from quart import abort, request, session +from quart.views import MethodView + +from valentina.constants import DiceType, RollResultType +from valentina.models import CharacterTrait, DiceRoll +from valentina.webui import catalog +from valentina.webui.utils import fetch_active_campaign, fetch_active_character, fetch_user + +from .valentina_forms import ValentinaForm + +gameplay_form = ValentinaForm() + + +class DiceRollView(MethodView): + """View to handle dice roll operations. POST a diceroll form to this view to return a snippet containing diceroll results.""" + + decorators: ClassVar = [requires_authorization] + + def init(self) -> None: + """Initialize the view.""" + self.session = session + + def get_result_div_classes(self, result_type: RollResultType) -> str: + """Return any classes to be added to the div.""" + if result_type in {RollResultType.CRITICAL, RollResultType.SUCCESS}: + return "bg-success-subtle border border-success border-2" + + if result_type == RollResultType.FAILURE: + return "bg-warning-subtle border border-warning border-2" + + if result_type == RollResultType.BOTCH: + return "bg-danger-subtle border border-danger border-2" + + return "border border-2" + + async def process_traits_form(self, form: dict) -> tuple[int, dict[str, int]]: + """Process the trait rolling form. + + Args: + form (dict): The form data. + + Returns: + tuple[int, dic[str,int]]: The pool and a dict of trait names and values. + """ + trait1 = json.loads(form.get("trait1", {})) + trait2 = json.loads(form.get("trait2", {})) + + pool = trait1.get("value", 0) + trait2.get("value", 0) + rolled_traits: dict[str, int] = {} + if trait1.get("name", None): + rolled_traits[trait1.get("name")] = trait1.get("value", 0) + if trait2.get("name", None): + rolled_traits[trait2.get("name")] = trait2.get("value", 0) + + return pool, rolled_traits + + async def process_macros_form(self, form: dict) -> tuple[int, dict[str, int]]: + """Process the macro rolling form. + + Args: + form (dict): The form data. + + Returns: + tuple[int, dic[str,int]]: The pool and a dict of trait names and values. + """ + character = await fetch_active_character(fetch_links=True) + + macro = json.loads(form.get("macro", {})) + + trait1 = await character.fetch_trait_by_name(macro.get("trait1")) + trait2 = await character.fetch_trait_by_name(macro.get("trait2")) + + rolled_traits: dict[str, int] = {} + if trait1: + rolled_traits[trait1.name] = trait1.value + if trait2: + rolled_traits[trait2.name] = trait2.value + + return trait1.value + trait2.value, rolled_traits + + async def post(self) -> str: + """Process the diceroll form and return the correct snippet.""" + form = await request.form + + # Process forms + match form.get("tab", None): + case "traits": + pool, rolled_traits = await self.process_traits_form(form=form) + case "macros": + pool, rolled_traits = await self.process_macros_form(form=form) + case _: + pool = int(form.get("pool", 1)) + rolled_traits = {} + + character = await fetch_active_character(fetch_links=True) + campaign = await fetch_active_campaign() + + roll = DiceRoll( + difficulty=int(form.get("difficulty", 1)), + dice_size=int(form.get("dice_size", 10)), + pool=pool, + desperation_pool=int(form.get("desperation_dice", 0)), + guild_id=session["GUILD_ID"], + author_id=session["USER_ID"], + author_name=session["USER_NAME"], + character=character, + campaign=campaign, + ) + + await roll.log_roll(traits=list(rolled_traits)) + + return catalog.render( + "gameplay.RollResult", + character=character, + campaign=campaign, + roll=roll, + rolled_traits=rolled_traits, + result_image_url=await roll.thumbnail_url(), + result_div_class=self.get_result_div_classes(roll.result_type), + ) + + +class GameplayView(MethodView): + """View to handle character operations.""" + + decorators: ClassVar = [requires_authorization] + + def __init__(self) -> None: + self.session = session # Assuming session is defined globally or passed in some way + self.dice_size_values = [member.value for member in DiceType] + + async def handle_form_tabs(self) -> str: + """Tab switcher for the gameplay template.""" + campaign = await fetch_active_campaign(fetch_links=True) + character = await fetch_active_character(fetch_links=True) + error_msg = None + + if not character and not campaign: + error_msg = "Select a character and a campaign" + elif not character: + error_msg = "Select a character" + elif not campaign: + error_msg = "Select a campaign" + + if error_msg: + return f'' + + if request.args.get("tab") == "throw": + return catalog.render( + "gameplay.FormTabThrow", + character=character, + campaign=campaign, + dice_sizes=self.dice_size_values, + form=gameplay_form, + ) + + if request.args.get("tab") == "traits": + traits = ( + await CharacterTrait.find(CharacterTrait.character == str(character.id)) + .sort(+CharacterTrait.name) + .to_list() + ) + + return catalog.render( + "gameplay.FormTabTraits", + traits=traits, + campaign=campaign, + form=gameplay_form, + ) + + if request.args.get("tab") == "macros": + user = await fetch_user() + return catalog.render( + "gameplay.FormTabMacros", + macros=user.macros, + campaign=campaign, + form=gameplay_form, + ) + + return abort(404) + + async def get(self) -> str: + """Handle GET requests. Changes to the form made prior to dice-rolling are handled as GET requests. + + The gameplay template is split into three jinjax partials for usage with HTMX for form processing. + + FormWrapper - The outside wrapper containing all elements of the left column of the page + FormHeader - The controller for selecting characters and campaigns + FormTab... - The diceroll tabs, each with their own name corresponding to the tab name + """ + # Handle changes from the form header where character and campaign are selected + if request.headers.get("HX-Request") and ( + request.args.get("character_id", None) or request.args.get("campaign_id", None) + ): + return catalog.render( + "gameplay.FormWrapper", + character=await fetch_active_character( + character_id=request.args.get("character_id", None) + ), + campaign=await fetch_active_campaign( + campaign_id=request.args.get("campaign_id", None) + ), + dice_sizes=self.dice_size_values, + form=gameplay_form, + ) + + # Handle tab switches + if request.headers.get("HX-Request") and request.args.get("tab", None): + return await self.handle_form_tabs() + + # If not an HTMX request, return the entire page + return catalog.render( + "gameplay", + character=await fetch_active_character(), + campaign=await fetch_active_campaign(), + dice_sizes=self.dice_size_values, + form=gameplay_form, + ) diff --git a/src/valentina/webui/views/homepage.py b/src/valentina/webui/views/homepage.py new file mode 100644 index 00000000..a8eb3ce0 --- /dev/null +++ b/src/valentina/webui/views/homepage.py @@ -0,0 +1,25 @@ +"""Homepage views.""" + +import random + +from quart import session +from quart.views import MethodView + +from valentina.constants import BOT_DESCRIPTIONS +from valentina.webui import catalog, discord_oauth +from valentina.webui.utils import update_session + +homepage_description = f"Valentina, your {random.choice(['honored', 'admired', 'distinguished', 'celebrated', 'hallowed', 'prestigious', 'acclaimed', 'favorite', 'friendly neighborhood', 'prized', 'treasured', 'number one', 'esteemed', 'venerated', 'revered', 'feared'])} {random.choice(BOT_DESCRIPTIONS)}, is ready for you!\n" + + +class HomepageView(MethodView): + """View to handle homepage operations.""" + + async def get(self) -> str: + """Handle GET requests.""" + await update_session() + + if not discord_oauth.authorized or not session["USER_ID"]: + return catalog.render("homepage", homepage_description=homepage_description) + + return catalog.render("homepage", homepage_description=homepage_description) diff --git a/src/valentina/webui/views/valentina_forms.py b/src/valentina/webui/views/valentina_forms.py new file mode 100644 index 00000000..a0d0d9e4 --- /dev/null +++ b/src/valentina/webui/views/valentina_forms.py @@ -0,0 +1,43 @@ +"""Model for HTML forms.""" + +from dataclasses import dataclass + + +@dataclass() +class ValentinaForm: + """Form class to contain custom form elements.""" + + title: str = "" + description: str = "" + hx_validate: bool = False + hx_url: str = "" + join_labels: bool = False + floating_labels: bool = False + + def hx_command_outer(self, name: str = "") -> str: + """The parent element HTMX command to be placed within the
tag surrounding the form item. + + Args: + name (str): The element name + + Returns: + str: The HTMX command + """ + if self.hx_validate: + return f'hx-target="this" hx-swap="outerHTML" hx-indicator="#indicator-{name}"' + + return "" + + def hx_command_input(self, name: str = "") -> str: + """The HTMX command for the input element to be placed within the tag. + + Args: + name (str): The element name + + Returns: + str: The HTMX command + """ + if self.hx_validate: + return f'hx-post="{self.hx_url}" hx-indicator="#indicator-{name}" hx-headers=\'{{"HX-form-field": "{name}"}}\'' + + return "" diff --git a/tests/models/test_dicerolls.py b/tests/models/test_dicerolls.py index 3ed49c35..a68e350e 100644 --- a/tests/models/test_dicerolls.py +++ b/tests/models/test_dicerolls.py @@ -8,6 +8,21 @@ from valentina.utils import errors +@pytest.mark.no_db() +@pytest.mark.parametrize( + ("guild_id", "author_id", "author_name"), [(None, 1, "name"), (1, None, "name"), (1, 1, None)] +) +def test_fail_without_init_data(guild_id, author_id, author_name) -> None: + """Ensure that Roll fails without the required data. + + GIVEN a call to Roll + WHEN the required data is not provided + THEN raise an exception + """ + with pytest.raises(errors.ValidationError): + DiceRoll(guild_id=guild_id, author_id=author_id, author_name=author_name, pool=1) + + @pytest.mark.no_db() @pytest.mark.parametrize( ( @@ -29,7 +44,7 @@ def test_rolling_dice(mock_ctx1, pool: int, dice_size: int) -> None: THEN assert that the correct number of dice are rolled with the correct dice type. """ for _ in range(100): - roll = DiceRoll(mock_ctx1, pool, dice_size=dice_size, difficulty=1) + roll = DiceRoll(pool=pool, ctx=mock_ctx1, dice_size=dice_size, difficulty=1) assert len(roll.roll) == pool assert all(1 <= die <= dice_size for die in roll.roll) @@ -80,7 +95,7 @@ def test_roll_successes( """ mocker.patch.object(DiceRoll, "roll", roll) - roll = DiceRoll(mock_ctx1, pool=3, difficulty=6) + roll = DiceRoll(pool=3, ctx=mock_ctx1, difficulty=6) assert roll.botches == botches assert roll.criticals == criticals assert roll.failures == failures @@ -93,7 +108,7 @@ def test_roll_successes( async def test_not_d10(mock_ctx1): """Ensure that customizations for non-d10 dice are applied correctly.""" # GIVEN a roll with a non-d10 dice - roll = DiceRoll(mock_ctx1, pool=3, dice_size=6, difficulty=6) + roll = DiceRoll(ctx=mock_ctx1, pool=3, dice_size=6, difficulty=6) assert roll.result_type == RollResultType.OTHER @@ -106,28 +121,28 @@ def test_roll_exceptions(mock_ctx1): THEN raise the appropriate exception """ with pytest.raises(errors.ValidationError, match="Pool cannot be less than 0."): - DiceRoll(mock_ctx1, pool=-1) + DiceRoll(ctx=mock_ctx1, pool=-1) with pytest.raises( errors.ValidationError, match="Difficulty cannot exceed the size of the dice." ): - DiceRoll(mock_ctx1, difficulty=11, pool=1) + DiceRoll(ctx=mock_ctx1, difficulty=11, pool=1) with pytest.raises(errors.ValidationError, match="Pool cannot exceed 100."): - DiceRoll(mock_ctx1, pool=101) + DiceRoll(ctx=mock_ctx1, pool=101) with pytest.raises(errors.ValidationError, match="Difficulty cannot be less than 0."): - DiceRoll(mock_ctx1, difficulty=-1, pool=1) + DiceRoll(ctx=mock_ctx1, difficulty=-1, pool=1) with pytest.raises(errors.ValidationError, match="Invalid dice size"): - DiceRoll(mock_ctx1, difficulty=6, pool=6, dice_size=3) + DiceRoll(ctx=mock_ctx1, difficulty=6, pool=6, dice_size=3) @pytest.mark.drop_db() async def test_log_roll(mock_ctx1): """Test diceroll logging to the database.""" # GIVEN a diceroll object and a list of two traits - d = DiceRoll(mock_ctx1, pool=3, dice_size=10, difficulty=6) + d = DiceRoll(ctx=mock_ctx1, pool=3, dice_size=10, difficulty=6) traits = ["test_trait1", "test_trait2"] # WHEN the log_roll method is called diff --git a/tests/models/test_probability.py b/tests/models/test_probability.py index 69fb0038..d5fe7585 100644 --- a/tests/models/test_probability.py +++ b/tests/models/test_probability.py @@ -15,7 +15,7 @@ async def test_calculate_no_db(mock_ctx1): """Test the calculate method.""" # GIVEN an empty RollProbability collection # WHEN calculating the probability of a roll - p = Probability(mock_ctx1, pool=5, difficulty=6, dice_size=10) + p = Probability(ctx=mock_ctx1, pool=5, difficulty=6, dice_size=10) result = await p._calculate() # THEN confirm the probability is correct and the result is saved to the database @@ -60,7 +60,7 @@ async def test_calculate_with_db(mock_ctx1): await r.insert() # WHEN calculating the probability of a roll - p = Probability(mock_ctx1, pool=5, difficulty=6, dice_size=10) + p = Probability(ctx=mock_ctx1, pool=5, difficulty=6, dice_size=10) result = await p._calculate() # THEN confirm the probability is correct and the result pulled from the database @@ -91,23 +91,25 @@ async def test_get_description(mock_ctx1): ) # WHEN getting the description - p = Probability(mock_ctx1, pool=5, difficulty=6, dice_size=10) + p = Probability(ctx=mock_ctx1, pool=5, difficulty=6, dice_size=10) result = p._get_description(results=obj) # THEN confirm the description is correct - assert result == Regex(r"## Overall success probability: \d{2}\.\d{2}% 👍", re.I) + assert result == Regex(r"## Overall success probability: \d{2}\.\d{2}% 👍", re.IGNORECASE) assert "Rolling `5d10` against difficulty `6`" in result async def test_get_embed(mock_ctx1): """Test the get_embed method.""" # GIVEN a probability instance - p = Probability(mock_ctx1, pool=5, difficulty=6, dice_size=10) + p = Probability(ctx=mock_ctx1, pool=5, difficulty=6, dice_size=10) # WHEN getting the embed embed = await p.get_embed() # THEN confirm the embed is correct assert isinstance(embed, discord.Embed) - assert embed.description == Regex(r"## Overall success probability: \d{2}\.\d{2}% 👍", re.I) + assert embed.description == Regex( + r"## Overall success probability: \d{2}\.\d{2}% 👍", re.IGNORECASE + ) assert "Rolling `5d10` against difficulty `6`" in embed.description diff --git a/tests/models/test_statistics.py b/tests/models/test_statistics.py index 456262f0..e3f6f556 100644 --- a/tests/models/test_statistics.py +++ b/tests/models/test_statistics.py @@ -261,3 +261,61 @@ async def test_campaign_statistics_results(mock_ctx1, character_factory, campaig assert isinstance(result, discord.Embed) assert "Successful Rolls ........ 1 (33.33%)" in result.description assert "Definitions:" not in result.description + + +@pytest.mark.drop_db() +async def test_guild_statistics_results_json(mock_ctx1): + """Test pulling guild statistics as a json object.""" + # GIVEN a guild with statistics + stat1 = RollStatistic( + user=1, + guild=1, + character="1", + result=RollResultType.SUCCESS, + pool=1, + difficulty=1, + ) + stat2 = RollStatistic( + user=1, + guild=1, + character="1", + result=RollResultType.BOTCH, + pool=1, + difficulty=3, + ) + stat3 = RollStatistic( + user=2, + guild=2, + character="2", + result=RollResultType.BOTCH, + pool=1, + difficulty=1, + ) + await stat1.insert() + await stat2.insert() + await stat3.insert() + + # WHEN statistics are pulled for a guild + s = Statistics(mock_ctx1) + result = await s.guild_statistics(as_json=True) + + # THEN confirm the statistics are returned to the user + assert s.botches == 1 + assert s.successes == 1 + assert s.failures == 0 + assert s.criticals == 0 + assert s.total_rolls == 2 + assert s.average_difficulty == 2 + assert result == { + "average_difficulty": "2", + "average_pool": "1", + "botches": "1", + "botches_percentage": "50.00", + "criticals": "0", + "criticals_percentage": "0.00", + "failures": "0", + "failures_percentage": "0.00", + "successes": "1", + "successes_percentage": "50.00", + "total_rolls": "2", + }