From ee0cda03102b44f562aacbcf1b22e2fefc039050 Mon Sep 17 00:00:00 2001 From: Nathaniel Landau Date: Sun, 1 Sep 2024 15:20:22 -0400 Subject: [PATCH] refactor: cleanup env var trace and improve docstrings (#174) --- .pre-commit-config.yaml | 6 +- README.md | 2 +- poetry.lock | 860 +++++++++--------- pyproject.toml | 2 +- src/valentina/characters/add_from_sheet.py | 66 +- src/valentina/characters/chargen.py | 239 +++-- src/valentina/characters/spend_experience.py | 16 +- src/valentina/debug_webui.py | 3 - src/valentina/main.py | 15 +- src/valentina/models/__init__.py | 9 +- src/valentina/models/bot.py | 167 +++- src/valentina/models/campaign.py | 34 +- src/valentina/models/changelog.py | 176 +++- src/valentina/models/character.py | 155 +++- src/valentina/models/database.py | 27 +- src/valentina/models/dicerolls.py | 289 ++++-- src/valentina/models/guild.py | 29 +- src/valentina/models/statistics.py | 153 +++- src/valentina/models/user.py | 125 ++- src/valentina/utils/__init__.py | 3 +- src/valentina/utils/autocomplete.py | 127 ++- src/valentina/utils/config.py | 27 +- src/valentina/utils/converters.py | 87 +- src/valentina/utils/database.py | 19 +- src/valentina/utils/discord_utils.py | 41 +- src/valentina/utils/helpers.py | 73 +- src/valentina/webui/__init__.py | 72 +- src/valentina/webui/blueprints.py | 2 +- src/valentina/webui/utils/discord.py | 77 +- src/valentina/webui/utils/errors.py | 32 +- src/valentina/webui/utils/helpers.py | 98 +- src/valentina/webui/utils/jinja_filters.py | 24 +- src/valentina/webui/utils/jinjax.py | 14 +- src/valentina/webui/views/campaign_view.py | 93 +- .../webui/views/character_create_full.py | 188 +++- src/valentina/webui/views/character_view.py | 71 +- src/valentina/webui/views/gameplay.py | 43 +- tests/models/test_statistics.py | 12 +- 38 files changed, 2536 insertions(+), 940 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index baa669b8..3e240db3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -59,19 +59,19 @@ repos: entry: yamllint --strict --config-file .yamllint.yml - repo: "https://github.com/charliermarsh/ruff-pre-commit" - rev: "v0.5.7" + rev: "v0.6.3" hooks: - id: ruff exclude: tests/ - id: ruff-format - repo: "https://github.com/crate-ci/typos" - rev: v1.23.6 + rev: typos-dict-v0.11.27 hooks: - id: typos - repo: "https://github.com/djlint/djLint" - rev: v1.34.1 + rev: v1.35.2 hooks: - id: djlint args: ["--configuration", "pyproject.toml"] diff --git a/README.md b/README.md index 12a6faf6..0ad40b8b 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ Settings for Valentina are controlled by environment variables. The following is | 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_PORT | `8088` | 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. | diff --git a/poetry.lock b/poetry.lock index f0c189ce..b33ac83c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -13,98 +13,113 @@ files = [ [[package]] name = "aiohappyeyeballs" -version = "2.3.6" +version = "2.4.0" 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"}, + {file = "aiohappyeyeballs-2.4.0-py3-none-any.whl", hash = "sha256:7ce92076e249169a13c2f49320d1967425eaf1f407522d707d59cac7628d62bd"}, + {file = "aiohappyeyeballs-2.4.0.tar.gz", hash = "sha256:55a1714f084e63d49639800f95716da97a1f173d46a16dfcfda0016abb93b6b2"}, ] [[package]] name = "aiohttp" -version = "3.10.3" +version = "3.10.5" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.8" files = [ - {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"}, + {file = "aiohttp-3.10.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:18a01eba2574fb9edd5f6e5fb25f66e6ce061da5dab5db75e13fe1558142e0a3"}, + {file = "aiohttp-3.10.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:94fac7c6e77ccb1ca91e9eb4cb0ac0270b9fb9b289738654120ba8cebb1189c6"}, + {file = "aiohttp-3.10.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2f1f1c75c395991ce9c94d3e4aa96e5c59c8356a15b1c9231e783865e2772699"}, + {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f7acae3cf1a2a2361ec4c8e787eaaa86a94171d2417aae53c0cca6ca3118ff6"}, + {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:94c4381ffba9cc508b37d2e536b418d5ea9cfdc2848b9a7fea6aebad4ec6aac1"}, + {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c31ad0c0c507894e3eaa843415841995bf8de4d6b2d24c6e33099f4bc9fc0d4f"}, + {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0912b8a8fadeb32ff67a3ed44249448c20148397c1ed905d5dac185b4ca547bb"}, + {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d93400c18596b7dc4794d48a63fb361b01a0d8eb39f28800dc900c8fbdaca91"}, + {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3c5e0d764a5c9aa5a62d99728c56d455310bcc288a79cab10157b3af426f"}, + {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d742c36ed44f2798c8d3f4bc511f479b9ceef2b93f348671184139e7d708042c"}, + {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:814375093edae5f1cb31e3407997cf3eacefb9010f96df10d64829362ae2df69"}, + {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8224f98be68a84b19f48e0bdc14224b5a71339aff3a27df69989fa47d01296f3"}, + {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d9a487ef090aea982d748b1b0d74fe7c3950b109df967630a20584f9a99c0683"}, + {file = "aiohttp-3.10.5-cp310-cp310-win32.whl", hash = "sha256:d9ef084e3dc690ad50137cc05831c52b6ca428096e6deb3c43e95827f531d5ef"}, + {file = "aiohttp-3.10.5-cp310-cp310-win_amd64.whl", hash = "sha256:66bf9234e08fe561dccd62083bf67400bdbf1c67ba9efdc3dac03650e97c6088"}, + {file = "aiohttp-3.10.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8c6a4e5e40156d72a40241a25cc226051c0a8d816610097a8e8f517aeacd59a2"}, + {file = "aiohttp-3.10.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c634a3207a5445be65536d38c13791904fda0748b9eabf908d3fe86a52941cf"}, + {file = "aiohttp-3.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4aff049b5e629ef9b3e9e617fa6e2dfeda1bf87e01bcfecaf3949af9e210105e"}, + {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1942244f00baaacaa8155eca94dbd9e8cc7017deb69b75ef67c78e89fdad3c77"}, + {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e04a1f2a65ad2f93aa20f9ff9f1b672bf912413e5547f60749fa2ef8a644e061"}, + {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f2bfc0032a00405d4af2ba27f3c429e851d04fad1e5ceee4080a1c570476697"}, + {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:424ae21498790e12eb759040bbb504e5e280cab64693d14775c54269fd1d2bb7"}, + {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:975218eee0e6d24eb336d0328c768ebc5d617609affaca5dbbd6dd1984f16ed0"}, + {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4120d7fefa1e2d8fb6f650b11489710091788de554e2b6f8347c7a20ceb003f5"}, + {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b90078989ef3fc45cf9221d3859acd1108af7560c52397ff4ace8ad7052a132e"}, + {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ba5a8b74c2a8af7d862399cdedce1533642fa727def0b8c3e3e02fcb52dca1b1"}, + {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:02594361128f780eecc2a29939d9dfc870e17b45178a867bf61a11b2a4367277"}, + {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8fb4fc029e135859f533025bc82047334e24b0d489e75513144f25408ecaf058"}, + {file = "aiohttp-3.10.5-cp311-cp311-win32.whl", hash = "sha256:e1ca1ef5ba129718a8fc827b0867f6aa4e893c56eb00003b7367f8a733a9b072"}, + {file = "aiohttp-3.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:349ef8a73a7c5665cca65c88ab24abe75447e28aa3bc4c93ea5093474dfdf0ff"}, + {file = "aiohttp-3.10.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:305be5ff2081fa1d283a76113b8df7a14c10d75602a38d9f012935df20731487"}, + {file = "aiohttp-3.10.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3a1c32a19ee6bbde02f1cb189e13a71b321256cc1d431196a9f824050b160d5a"}, + {file = "aiohttp-3.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:61645818edd40cc6f455b851277a21bf420ce347baa0b86eaa41d51ef58ba23d"}, + {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c225286f2b13bab5987425558baa5cbdb2bc925b2998038fa028245ef421e75"}, + {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ba01ebc6175e1e6b7275c907a3a36be48a2d487549b656aa90c8a910d9f3178"}, + {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8eaf44ccbc4e35762683078b72bf293f476561d8b68ec8a64f98cf32811c323e"}, + {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1c43eb1ab7cbf411b8e387dc169acb31f0ca0d8c09ba63f9eac67829585b44f"}, + {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de7a5299827253023c55ea549444e058c0eb496931fa05d693b95140a947cb73"}, + {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4790f0e15f00058f7599dab2b206d3049d7ac464dc2e5eae0e93fa18aee9e7bf"}, + {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:44b324a6b8376a23e6ba25d368726ee3bc281e6ab306db80b5819999c737d820"}, + {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0d277cfb304118079e7044aad0b76685d30ecb86f83a0711fc5fb257ffe832ca"}, + {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:54d9ddea424cd19d3ff6128601a4a4d23d54a421f9b4c0fff740505813739a91"}, + {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4f1c9866ccf48a6df2b06823e6ae80573529f2af3a0992ec4fe75b1a510df8a6"}, + {file = "aiohttp-3.10.5-cp312-cp312-win32.whl", hash = "sha256:dc4826823121783dccc0871e3f405417ac116055bf184ac04c36f98b75aacd12"}, + {file = "aiohttp-3.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:22c0a23a3b3138a6bf76fc553789cb1a703836da86b0f306b6f0dc1617398abc"}, + {file = "aiohttp-3.10.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7f6b639c36734eaa80a6c152a238242bedcee9b953f23bb887e9102976343092"}, + {file = "aiohttp-3.10.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29930bc2921cef955ba39a3ff87d2c4398a0394ae217f41cb02d5c26c8b1b77"}, + {file = "aiohttp-3.10.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f489a2c9e6455d87eabf907ac0b7d230a9786be43fbe884ad184ddf9e9c1e385"}, + {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:123dd5b16b75b2962d0fff566effb7a065e33cd4538c1692fb31c3bda2bfb972"}, + {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b98e698dc34966e5976e10bbca6d26d6724e6bdea853c7c10162a3235aba6e16"}, + {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3b9162bab7e42f21243effc822652dc5bb5e8ff42a4eb62fe7782bcbcdfacf6"}, + {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1923a5c44061bffd5eebeef58cecf68096e35003907d8201a4d0d6f6e387ccaa"}, + {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d55f011da0a843c3d3df2c2cf4e537b8070a419f891c930245f05d329c4b0689"}, + {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:afe16a84498441d05e9189a15900640a2d2b5e76cf4efe8cbb088ab4f112ee57"}, + {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f8112fb501b1e0567a1251a2fd0747baae60a4ab325a871e975b7bb67e59221f"}, + {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1e72589da4c90337837fdfe2026ae1952c0f4a6e793adbbfbdd40efed7c63599"}, + {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4d46c7b4173415d8e583045fbc4daa48b40e31b19ce595b8d92cf639396c15d5"}, + {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33e6bc4bab477c772a541f76cd91e11ccb6d2efa2b8d7d7883591dfb523e5987"}, + {file = "aiohttp-3.10.5-cp313-cp313-win32.whl", hash = "sha256:c58c6837a2c2a7cf3133983e64173aec11f9c2cd8e87ec2fdc16ce727bcf1a04"}, + {file = "aiohttp-3.10.5-cp313-cp313-win_amd64.whl", hash = "sha256:38172a70005252b6893088c0f5e8a47d173df7cc2b2bd88650957eb84fcf5022"}, + {file = "aiohttp-3.10.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:f6f18898ace4bcd2d41a122916475344a87f1dfdec626ecde9ee802a711bc569"}, + {file = "aiohttp-3.10.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5ede29d91a40ba22ac1b922ef510aab871652f6c88ef60b9dcdf773c6d32ad7a"}, + {file = "aiohttp-3.10.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:673f988370f5954df96cc31fd99c7312a3af0a97f09e407399f61583f30da9bc"}, + {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58718e181c56a3c02d25b09d4115eb02aafe1a732ce5714ab70326d9776457c3"}, + {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b38b1570242fbab8d86a84128fb5b5234a2f70c2e32f3070143a6d94bc854cf"}, + {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:074d1bff0163e107e97bd48cad9f928fa5a3eb4b9d33366137ffce08a63e37fe"}, + {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd31f176429cecbc1ba499d4aba31aaccfea488f418d60376b911269d3b883c5"}, + {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7384d0b87d4635ec38db9263e6a3f1eb609e2e06087f0aa7f63b76833737b471"}, + {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8989f46f3d7ef79585e98fa991e6ded55d2f48ae56d2c9fa5e491a6e4effb589"}, + {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:c83f7a107abb89a227d6c454c613e7606c12a42b9a4ca9c5d7dad25d47c776ae"}, + {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cde98f323d6bf161041e7627a5fd763f9fd829bcfcd089804a5fdce7bb6e1b7d"}, + {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:676f94c5480d8eefd97c0c7e3953315e4d8c2b71f3b49539beb2aa676c58272f"}, + {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:2d21ac12dc943c68135ff858c3a989f2194a709e6e10b4c8977d7fcd67dfd511"}, + {file = "aiohttp-3.10.5-cp38-cp38-win32.whl", hash = "sha256:17e997105bd1a260850272bfb50e2a328e029c941c2708170d9d978d5a30ad9a"}, + {file = "aiohttp-3.10.5-cp38-cp38-win_amd64.whl", hash = "sha256:1c19de68896747a2aa6257ae4cf6ef59d73917a36a35ee9d0a6f48cff0f94db8"}, + {file = "aiohttp-3.10.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7e2fe37ac654032db1f3499fe56e77190282534810e2a8e833141a021faaab0e"}, + {file = "aiohttp-3.10.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5bf3ead3cb66ab990ee2561373b009db5bc0e857549b6c9ba84b20bc462e172"}, + {file = "aiohttp-3.10.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b2c16a919d936ca87a3c5f0e43af12a89a3ce7ccbce59a2d6784caba945b68b"}, + {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad146dae5977c4dd435eb31373b3fe9b0b1bf26858c6fc452bf6af394067e10b"}, + {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c5c6fa16412b35999320f5c9690c0f554392dc222c04e559217e0f9ae244b92"}, + {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:95c4dc6f61d610bc0ee1edc6f29d993f10febfe5b76bb470b486d90bbece6b22"}, + {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da452c2c322e9ce0cfef392e469a26d63d42860f829026a63374fde6b5c5876f"}, + {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:898715cf566ec2869d5cb4d5fb4be408964704c46c96b4be267442d265390f32"}, + {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:391cc3a9c1527e424c6865e087897e766a917f15dddb360174a70467572ac6ce"}, + {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:380f926b51b92d02a34119d072f178d80bbda334d1a7e10fa22d467a66e494db"}, + {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce91db90dbf37bb6fa0997f26574107e1b9d5ff939315247b7e615baa8ec313b"}, + {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9093a81e18c45227eebe4c16124ebf3e0d893830c6aca7cc310bfca8fe59d857"}, + {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ee40b40aa753d844162dcc80d0fe256b87cba48ca0054f64e68000453caead11"}, + {file = "aiohttp-3.10.5-cp39-cp39-win32.whl", hash = "sha256:03f2645adbe17f274444953bdea69f8327e9d278d961d85657cb0d06864814c1"}, + {file = "aiohttp-3.10.5-cp39-cp39-win_amd64.whl", hash = "sha256:d17920f18e6ee090bdd3d0bfffd769d9f2cb4c8ffde3eb203777a3895c128862"}, + {file = "aiohttp-3.10.5.tar.gz", hash = "sha256:f071854b47d39591ce9a17981c46790acb30518e2f83dfca8db2dfa091178691"}, ] [package.dependencies] @@ -242,17 +257,17 @@ files = [ [[package]] name = "boto3" -version = "1.34.162" +version = "1.35.10" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.34.162-py3-none-any.whl", hash = "sha256:d6f6096bdab35a0c0deff469563b87d184a28df7689790f7fe7be98502b7c590"}, - {file = "boto3-1.34.162.tar.gz", hash = "sha256:873f8f5d2f6f85f1018cbb0535b03cceddc7b655b61f66a0a56995238804f41f"}, + {file = "boto3-1.35.10-py3-none-any.whl", hash = "sha256:add26dd58e076dfd387013da4704716d5cff215cf14f6d4347c4b9b7fc1f0b8e"}, + {file = "boto3-1.35.10.tar.gz", hash = "sha256:189ab1e2b4cd86df56f82438d89b4040eb140c92683f1bda7cb2e62624f20ea5"}, ] [package.dependencies] -botocore = ">=1.34.162,<1.35.0" +botocore = ">=1.35.10,<1.36.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -261,13 +276,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.34.162" +version = "1.35.10" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.34.162-py3-none-any.whl", hash = "sha256:2d918b02db88d27a75b48275e6fb2506e9adaaddbec1ffa6a8a0898b34e769be"}, - {file = "botocore-1.34.162.tar.gz", hash = "sha256:adc23be4fb99ad31961236342b7cbf3c0bfc62532cd02852196032e8c0d682f3"}, + {file = "botocore-1.35.10-py3-none-any.whl", hash = "sha256:0d96d023b9b0cea99a0a428a431d011329d3a958730aee6ed6a6fec5d9bfbc03"}, + {file = "botocore-1.35.10.tar.gz", hash = "sha256:6c8a1377b6636a0d80218115e1cd41bcceba0a2f050b79c206f4cf8d002c54d7"}, ] [package.dependencies] @@ -280,24 +295,24 @@ crt = ["awscrt (==0.21.2)"] [[package]] name = "cachetools" -version = "5.4.0" +version = "5.5.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"}, + {file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"}, + {file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"}, ] [[package]] name = "certifi" -version = "2024.7.4" +version = "2024.8.30" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, - {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, ] [[package]] @@ -760,27 +775,27 @@ files = [ [[package]] name = "djlint" -version = "1.34.1" +version = "1.35.2" description = "HTML Template Linter and Formatter" optional = false -python-versions = ">=3.8.0,<4.0.0" +python-versions = "<4.0,>=3.8" files = [ - {file = "djlint-1.34.1-py3-none-any.whl", hash = "sha256:96ff1c464fb6f061130ebc88663a2ea524d7ec51f4b56221a2b3f0320a3cfce8"}, - {file = "djlint-1.34.1.tar.gz", hash = "sha256:db93fa008d19eaadb0454edf1704931d14469d48508daba2df9941111f408346"}, + {file = "djlint-1.35.2-py3-none-any.whl", hash = "sha256:4ba995bad378f2afa77c8ea56ba1c14429d9ff26a18e8ae23bc71eedb9152243"}, + {file = "djlint-1.35.2.tar.gz", hash = "sha256:318de9d4b9b0061a111f8f5164ecbacd8215f449dd4bd5a76d2a691c815ee103"}, ] [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" +click = ">=8.0.1" +colorama = ">=0.4.4" +cssbeautifier = ">=1.14.4" +html-tag-names = ">=0.1.2" +html-void-elements = ">=0.1.0" +jsbeautifier = ">=1.14.4" +json5 = ">=0.9.11" +pathspec = ">=0.12.0" +PyYAML = ">=6.0" +regex = ">=2023" +tqdm = ">=4.62.2" [[package]] name = "dnspython" @@ -1095,13 +1110,13 @@ license = ["ukkonen"] [[package]] name = "idna" -version = "3.7" +version = "3.8" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" files = [ - {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, - {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, + {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"}, + {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"}, ] [[package]] @@ -1503,38 +1518,38 @@ files = [ [[package]] name = "mypy" -version = "1.11.1" +version = "1.11.2" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {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"}, + {file = "mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a"}, + {file = "mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef"}, + {file = "mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383"}, + {file = "mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8"}, + {file = "mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7"}, + {file = "mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385"}, + {file = "mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca"}, + {file = "mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104"}, + {file = "mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4"}, + {file = "mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6"}, + {file = "mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318"}, + {file = "mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36"}, + {file = "mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987"}, + {file = "mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca"}, + {file = "mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70"}, + {file = "mypy-1.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b"}, + {file = "mypy-1.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86"}, + {file = "mypy-1.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce"}, + {file = "mypy-1.11.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1"}, + {file = "mypy-1.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b"}, + {file = "mypy-1.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6"}, + {file = "mypy-1.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70"}, + {file = "mypy-1.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d"}, + {file = "mypy-1.11.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d"}, + {file = "mypy-1.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24"}, + {file = "mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12"}, + {file = "mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79"}, ] [package.dependencies] @@ -1571,56 +1586,63 @@ files = [ [[package]] name = "numpy" -version = "2.0.1" +version = "2.1.0" description = "Fundamental package for array computing in Python" optional = false -python-versions = ">=3.9" -files = [ - {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"}, +python-versions = ">=3.10" +files = [ + {file = "numpy-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6326ab99b52fafdcdeccf602d6286191a79fe2fda0ae90573c5814cd2b0bc1b8"}, + {file = "numpy-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0937e54c09f7a9a68da6889362ddd2ff584c02d015ec92672c099b61555f8911"}, + {file = "numpy-2.1.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:30014b234f07b5fec20f4146f69e13cfb1e33ee9a18a1879a0142fbb00d47673"}, + {file = "numpy-2.1.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:899da829b362ade41e1e7eccad2cf274035e1cb36ba73034946fccd4afd8606b"}, + {file = "numpy-2.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08801848a40aea24ce16c2ecde3b756f9ad756586fb2d13210939eb69b023f5b"}, + {file = "numpy-2.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:398049e237d1aae53d82a416dade04defed1a47f87d18d5bd615b6e7d7e41d1f"}, + {file = "numpy-2.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0abb3916a35d9090088a748636b2c06dc9a6542f99cd476979fb156a18192b84"}, + {file = "numpy-2.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10e2350aea18d04832319aac0f887d5fcec1b36abd485d14f173e3e900b83e33"}, + {file = "numpy-2.1.0-cp310-cp310-win32.whl", hash = "sha256:f6b26e6c3b98adb648243670fddc8cab6ae17473f9dc58c51574af3e64d61211"}, + {file = "numpy-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:f505264735ee074250a9c78247ee8618292091d9d1fcc023290e9ac67e8f1afa"}, + {file = "numpy-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:76368c788ccb4f4782cf9c842b316140142b4cbf22ff8db82724e82fe1205dce"}, + {file = "numpy-2.1.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:f8e93a01a35be08d31ae33021e5268f157a2d60ebd643cfc15de6ab8e4722eb1"}, + {file = "numpy-2.1.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:9523f8b46485db6939bd069b28b642fec86c30909cea90ef550373787f79530e"}, + {file = "numpy-2.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54139e0eb219f52f60656d163cbe67c31ede51d13236c950145473504fa208cb"}, + {file = "numpy-2.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5ebbf9fbdabed208d4ecd2e1dfd2c0741af2f876e7ae522c2537d404ca895c3"}, + {file = "numpy-2.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:378cb4f24c7d93066ee4103204f73ed046eb88f9ad5bb2275bb9fa0f6a02bd36"}, + {file = "numpy-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8f699a709120b220dfe173f79c73cb2a2cab2c0b88dd59d7b49407d032b8ebd"}, + {file = "numpy-2.1.0-cp311-cp311-win32.whl", hash = "sha256:ffbd6faeb190aaf2b5e9024bac9622d2ee549b7ec89ef3a9373fa35313d44e0e"}, + {file = "numpy-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:0af3a5987f59d9c529c022c8c2a64805b339b7ef506509fba7d0556649b9714b"}, + {file = "numpy-2.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fe76d75b345dc045acdbc006adcb197cc680754afd6c259de60d358d60c93736"}, + {file = "numpy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f358ea9e47eb3c2d6eba121ab512dfff38a88db719c38d1e67349af210bc7529"}, + {file = "numpy-2.1.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:dd94ce596bda40a9618324547cfaaf6650b1a24f5390350142499aa4e34e53d1"}, + {file = "numpy-2.1.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:b47c551c6724960479cefd7353656498b86e7232429e3a41ab83be4da1b109e8"}, + {file = "numpy-2.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0756a179afa766ad7cb6f036de622e8a8f16ffdd55aa31f296c870b5679d745"}, + {file = "numpy-2.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24003ba8ff22ea29a8c306e61d316ac74111cebf942afbf692df65509a05f111"}, + {file = "numpy-2.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b34fa5e3b5d6dc7e0a4243fa0f81367027cb6f4a7215a17852979634b5544ee0"}, + {file = "numpy-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c4f982715e65036c34897eb598d64aef15150c447be2cfc6643ec7a11af06574"}, + {file = "numpy-2.1.0-cp312-cp312-win32.whl", hash = "sha256:c4cd94dfefbefec3f8b544f61286584292d740e6e9d4677769bc76b8f41deb02"}, + {file = "numpy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0cdef204199278f5c461a0bed6ed2e052998276e6d8ab2963d5b5c39a0500bc"}, + {file = "numpy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8ab81ccd753859ab89e67199b9da62c543850f819993761c1e94a75a814ed667"}, + {file = "numpy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:442596f01913656d579309edcd179a2a2f9977d9a14ff41d042475280fc7f34e"}, + {file = "numpy-2.1.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:848c6b5cad9898e4b9ef251b6f934fa34630371f2e916261070a4eb9092ffd33"}, + {file = "numpy-2.1.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:54c6a63e9d81efe64bfb7bcb0ec64332a87d0b87575f6009c8ba67ea6374770b"}, + {file = "numpy-2.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:652e92fc409e278abdd61e9505649e3938f6d04ce7ef1953f2ec598a50e7c195"}, + {file = "numpy-2.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ab32eb9170bf8ffcbb14f11613f4a0b108d3ffee0832457c5d4808233ba8977"}, + {file = "numpy-2.1.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:8fb49a0ba4d8f41198ae2d52118b050fd34dace4b8f3fb0ee34e23eb4ae775b1"}, + {file = "numpy-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44e44973262dc3ae79e9063a1284a73e09d01b894b534a769732ccd46c28cc62"}, + {file = "numpy-2.1.0-cp313-cp313-win32.whl", hash = "sha256:ab83adc099ec62e044b1fbb3a05499fa1e99f6d53a1dde102b2d85eff66ed324"}, + {file = "numpy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:de844aaa4815b78f6023832590d77da0e3b6805c644c33ce94a1e449f16d6ab5"}, + {file = "numpy-2.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:343e3e152bf5a087511cd325e3b7ecfd5b92d369e80e74c12cd87826e263ec06"}, + {file = "numpy-2.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f07fa2f15dabe91259828ce7d71b5ca9e2eb7c8c26baa822c825ce43552f4883"}, + {file = "numpy-2.1.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5474dad8c86ee9ba9bb776f4b99ef2d41b3b8f4e0d199d4f7304728ed34d0300"}, + {file = "numpy-2.1.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:1f817c71683fd1bb5cff1529a1d085a57f02ccd2ebc5cd2c566f9a01118e3b7d"}, + {file = "numpy-2.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a3336fbfa0d38d3deacd3fe7f3d07e13597f29c13abf4d15c3b6dc2291cbbdd"}, + {file = "numpy-2.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a894c51fd8c4e834f00ac742abad73fc485df1062f1b875661a3c1e1fb1c2f6"}, + {file = "numpy-2.1.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:9156ca1f79fc4acc226696e95bfcc2b486f165a6a59ebe22b2c1f82ab190384a"}, + {file = "numpy-2.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:624884b572dff8ca8f60fab591413f077471de64e376b17d291b19f56504b2bb"}, + {file = "numpy-2.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:15ef8b2177eeb7e37dd5ef4016f30b7659c57c2c0b57a779f1d537ff33a72c7b"}, + {file = "numpy-2.1.0-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:e5f0642cdf4636198a4990de7a71b693d824c56a757862230454629cf62e323d"}, + {file = "numpy-2.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15976718c004466406342789f31b6673776360f3b1e3c575f25302d7e789575"}, + {file = "numpy-2.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:6c1de77ded79fef664d5098a66810d4d27ca0224e9051906e634b3f7ead134c2"}, + {file = "numpy-2.1.0.tar.gz", hash = "sha256:7dc90da0081f7e1da49ec4e398ede6a8e9cc4f5ebe5f9e06b443ed889ee9aaa2"}, ] [[package]] @@ -1674,13 +1696,13 @@ files = [ [[package]] name = "pdoc" -version = "14.6.0" +version = "14.6.1" description = "API Documentation for Python Projects" optional = false python-versions = ">=3.8" files = [ - {file = "pdoc-14.6.0-py3-none-any.whl", hash = "sha256:36c42c546a317d8e3e8c0b39645f24161374de0c7066ccaae76628d721e49ba5"}, - {file = "pdoc-14.6.0.tar.gz", hash = "sha256:6e98a24c5e0ca5d188397969cf82581836eaef13f172fc3820047bfe15c61c9a"}, + {file = "pdoc-14.6.1-py3-none-any.whl", hash = "sha256:efbed433655264392c60551615a3d42b8f21e492373419756d20234c667b54bc"}, + {file = "pdoc-14.6.1.tar.gz", hash = "sha256:ee598f30d5c55dd4702086dabc412a26022acc35aa88aa382cda8ac655fead98"}, ] [package.dependencies] @@ -1970,13 +1992,13 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pygithub" -version = "2.3.0" +version = "2.4.0" description = "Use the full Github API v3" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "PyGithub-2.3.0-py3-none-any.whl", hash = "sha256:65b499728be3ce7b0cd2cd760da3b32f0f4d7bc55e5e0677617f90f6564e793e"}, - {file = "PyGithub-2.3.0.tar.gz", hash = "sha256:0148d7347a1cdeed99af905077010aef81a4dad988b0ba51d4108bf66b443f7e"}, + {file = "PyGithub-2.4.0-py3-none-any.whl", hash = "sha256:81935aa4bdc939fba98fee1cb47422c09157c56a27966476ff92775602b9ee24"}, + {file = "pygithub-2.4.0.tar.gz", hash = "sha256:6601e22627e87bac192f1e2e39c6e6f69a43152cfb8f307cee575879320b3051"}, ] [package.dependencies] @@ -2469,104 +2491,90 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)" [[package]] name = "regex" -version = "2023.12.25" +version = "2024.7.24" description = "Alternative regular expression module, to replace re." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" 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"}, + {file = "regex-2024.7.24-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b0d3f567fafa0633aee87f08b9276c7062da9616931382993c03808bb68ce"}, + {file = "regex-2024.7.24-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3426de3b91d1bc73249042742f45c2148803c111d1175b283270177fdf669024"}, + {file = "regex-2024.7.24-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f273674b445bcb6e4409bf8d1be67bc4b58e8b46fd0d560055d515b8830063cd"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23acc72f0f4e1a9e6e9843d6328177ae3074b4182167e34119ec7233dfeccf53"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65fd3d2e228cae024c411c5ccdffae4c315271eee4a8b839291f84f796b34eca"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c414cbda77dbf13c3bc88b073a1a9f375c7b0cb5e115e15d4b73ec3a2fbc6f59"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf7a89eef64b5455835f5ed30254ec19bf41f7541cd94f266ab7cbd463f00c41"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19c65b00d42804e3fbea9708f0937d157e53429a39b7c61253ff15670ff62cb5"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7a5486ca56c8869070a966321d5ab416ff0f83f30e0e2da1ab48815c8d165d46"}, + {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6f51f9556785e5a203713f5efd9c085b4a45aecd2a42573e2b5041881b588d1f"}, + {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a4997716674d36a82eab3e86f8fa77080a5d8d96a389a61ea1d0e3a94a582cf7"}, + {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c0abb5e4e8ce71a61d9446040c1e86d4e6d23f9097275c5bd49ed978755ff0fe"}, + {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:18300a1d78cf1290fa583cd8b7cde26ecb73e9f5916690cf9d42de569c89b1ce"}, + {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:416c0e4f56308f34cdb18c3f59849479dde5b19febdcd6e6fa4d04b6c31c9faa"}, + {file = "regex-2024.7.24-cp310-cp310-win32.whl", hash = "sha256:fb168b5924bef397b5ba13aabd8cf5df7d3d93f10218d7b925e360d436863f66"}, + {file = "regex-2024.7.24-cp310-cp310-win_amd64.whl", hash = "sha256:6b9fc7e9cc983e75e2518496ba1afc524227c163e43d706688a6bb9eca41617e"}, + {file = "regex-2024.7.24-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:382281306e3adaaa7b8b9ebbb3ffb43358a7bbf585fa93821300a418bb975281"}, + {file = "regex-2024.7.24-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4fdd1384619f406ad9037fe6b6eaa3de2749e2e12084abc80169e8e075377d3b"}, + {file = "regex-2024.7.24-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3d974d24edb231446f708c455fd08f94c41c1ff4f04bcf06e5f36df5ef50b95a"}, + {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2ec4419a3fe6cf8a4795752596dfe0adb4aea40d3683a132bae9c30b81e8d73"}, + {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb563dd3aea54c797adf513eeec819c4213d7dbfc311874eb4fd28d10f2ff0f2"}, + {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:45104baae8b9f67569f0f1dca5e1f1ed77a54ae1cd8b0b07aba89272710db61e"}, + {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:994448ee01864501912abf2bad9203bffc34158e80fe8bfb5b031f4f8e16da51"}, + {file = "regex-2024.7.24-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fac296f99283ac232d8125be932c5cd7644084a30748fda013028c815ba3364"}, + {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7e37e809b9303ec3a179085415cb5f418ecf65ec98cdfe34f6a078b46ef823ee"}, + {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:01b689e887f612610c869421241e075c02f2e3d1ae93a037cb14f88ab6a8934c"}, + {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f6442f0f0ff81775eaa5b05af8a0ffa1dda36e9cf6ec1e0d3d245e8564b684ce"}, + {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:871e3ab2838fbcb4e0865a6e01233975df3a15e6fce93b6f99d75cacbd9862d1"}, + {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c918b7a1e26b4ab40409820ddccc5d49871a82329640f5005f73572d5eaa9b5e"}, + {file = "regex-2024.7.24-cp311-cp311-win32.whl", hash = "sha256:2dfbb8baf8ba2c2b9aa2807f44ed272f0913eeeba002478c4577b8d29cde215c"}, + {file = "regex-2024.7.24-cp311-cp311-win_amd64.whl", hash = "sha256:538d30cd96ed7d1416d3956f94d54e426a8daf7c14527f6e0d6d425fcb4cca52"}, + {file = "regex-2024.7.24-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:fe4ebef608553aff8deb845c7f4f1d0740ff76fa672c011cc0bacb2a00fbde86"}, + {file = "regex-2024.7.24-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:74007a5b25b7a678459f06559504f1eec2f0f17bca218c9d56f6a0a12bfffdad"}, + {file = "regex-2024.7.24-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7df9ea48641da022c2a3c9c641650cd09f0cd15e8908bf931ad538f5ca7919c9"}, + {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a1141a1dcc32904c47f6846b040275c6e5de0bf73f17d7a409035d55b76f289"}, + {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80c811cfcb5c331237d9bad3bea2c391114588cf4131707e84d9493064d267f9"}, + {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7214477bf9bd195894cf24005b1e7b496f46833337b5dedb7b2a6e33f66d962c"}, + {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d55588cba7553f0b6ec33130bc3e114b355570b45785cebdc9daed8c637dd440"}, + {file = "regex-2024.7.24-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:558a57cfc32adcf19d3f791f62b5ff564922942e389e3cfdb538a23d65a6b610"}, + {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a512eed9dfd4117110b1881ba9a59b31433caed0c4101b361f768e7bcbaf93c5"}, + {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:86b17ba823ea76256b1885652e3a141a99a5c4422f4a869189db328321b73799"}, + {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5eefee9bfe23f6df09ffb6dfb23809f4d74a78acef004aa904dc7c88b9944b05"}, + {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:731fcd76bbdbf225e2eb85b7c38da9633ad3073822f5ab32379381e8c3c12e94"}, + {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eaef80eac3b4cfbdd6de53c6e108b4c534c21ae055d1dbea2de6b3b8ff3def38"}, + {file = "regex-2024.7.24-cp312-cp312-win32.whl", hash = "sha256:185e029368d6f89f36e526764cf12bf8d6f0e3a2a7737da625a76f594bdfcbfc"}, + {file = "regex-2024.7.24-cp312-cp312-win_amd64.whl", hash = "sha256:2f1baff13cc2521bea83ab2528e7a80cbe0ebb2c6f0bfad15be7da3aed443908"}, + {file = "regex-2024.7.24-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:66b4c0731a5c81921e938dcf1a88e978264e26e6ac4ec96a4d21ae0354581ae0"}, + {file = "regex-2024.7.24-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:88ecc3afd7e776967fa16c80f974cb79399ee8dc6c96423321d6f7d4b881c92b"}, + {file = "regex-2024.7.24-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64bd50cf16bcc54b274e20235bf8edbb64184a30e1e53873ff8d444e7ac656b2"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb462f0e346fcf41a901a126b50f8781e9a474d3927930f3490f38a6e73b6950"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a82465ebbc9b1c5c50738536fdfa7cab639a261a99b469c9d4c7dcbb2b3f1e57"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:68a8f8c046c6466ac61a36b65bb2395c74451df2ffb8458492ef49900efed293"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac8e84fff5d27420f3c1e879ce9929108e873667ec87e0c8eeb413a5311adfe"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba2537ef2163db9e6ccdbeb6f6424282ae4dea43177402152c67ef869cf3978b"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:43affe33137fcd679bdae93fb25924979517e011f9dea99163f80b82eadc7e53"}, + {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:c9bb87fdf2ab2370f21e4d5636e5317775e5d51ff32ebff2cf389f71b9b13750"}, + {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:945352286a541406f99b2655c973852da7911b3f4264e010218bbc1cc73168f2"}, + {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:8bc593dcce679206b60a538c302d03c29b18e3d862609317cb560e18b66d10cf"}, + {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3f3b6ca8eae6d6c75a6cff525c8530c60e909a71a15e1b731723233331de4169"}, + {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c51edc3541e11fbe83f0c4d9412ef6c79f664a3745fab261457e84465ec9d5a8"}, + {file = "regex-2024.7.24-cp38-cp38-win32.whl", hash = "sha256:d0a07763776188b4db4c9c7fb1b8c494049f84659bb387b71c73bbc07f189e96"}, + {file = "regex-2024.7.24-cp38-cp38-win_amd64.whl", hash = "sha256:8fd5afd101dcf86a270d254364e0e8dddedebe6bd1ab9d5f732f274fa00499a5"}, + {file = "regex-2024.7.24-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0ffe3f9d430cd37d8fa5632ff6fb36d5b24818c5c986893063b4e5bdb84cdf24"}, + {file = "regex-2024.7.24-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25419b70ba00a16abc90ee5fce061228206173231f004437730b67ac77323f0d"}, + {file = "regex-2024.7.24-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:33e2614a7ce627f0cdf2ad104797d1f68342d967de3695678c0cb84f530709f8"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d33a0021893ede5969876052796165bab6006559ab845fd7b515a30abdd990dc"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04ce29e2c5fedf296b1a1b0acc1724ba93a36fb14031f3abfb7abda2806c1535"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b16582783f44fbca6fcf46f61347340c787d7530d88b4d590a397a47583f31dd"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:836d3cc225b3e8a943d0b02633fb2f28a66e281290302a79df0e1eaa984ff7c1"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:438d9f0f4bc64e8dea78274caa5af971ceff0f8771e1a2333620969936ba10be"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:973335b1624859cb0e52f96062a28aa18f3a5fc77a96e4a3d6d76e29811a0e6e"}, + {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c5e69fd3eb0b409432b537fe3c6f44ac089c458ab6b78dcec14478422879ec5f"}, + {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fbf8c2f00904eaf63ff37718eb13acf8e178cb940520e47b2f05027f5bb34ce3"}, + {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ae2757ace61bc4061b69af19e4689fa4416e1a04840f33b441034202b5cd02d4"}, + {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:44fc61b99035fd9b3b9453f1713234e5a7c92a04f3577252b45feefe1b327759"}, + {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:84c312cdf839e8b579f504afcd7b65f35d60b6285d892b19adea16355e8343c9"}, + {file = "regex-2024.7.24-cp39-cp39-win32.whl", hash = "sha256:ca5b2028c2f7af4e13fb9fc29b28d0ce767c38c7facdf64f6c2cd040413055f1"}, + {file = "regex-2024.7.24-cp39-cp39-win_amd64.whl", hash = "sha256:7c479f5ae937ec9985ecaf42e2e10631551d909f203e31308c12d703922742f9"}, + {file = "regex-2024.7.24.tar.gz", hash = "sha256:9cfd009eed1a46b27c14039ad5bbc5e71b6367c5b2e6d5f5da0ea91600817506"}, ] [[package]] @@ -2610,13 +2618,13 @@ rsa = ["oauthlib[signedtoken] (>=3.0.0)"] [[package]] name = "rich" -version = "13.7.1" +version = "13.8.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.7.0" files = [ - {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, - {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, + {file = "rich-13.8.0-py3-none-any.whl", hash = "sha256:2e85306a063b9492dffc86278197a60cbece75bcb766022f3436f567cae11bdc"}, + {file = "rich-13.8.0.tar.gz", hash = "sha256:a5ac1f1cd448ade0d59cc3356f7db7a7ccda2c8cbae9c7a90c28ff463d3e91f4"}, ] [package.dependencies] @@ -2628,29 +2636,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.6.1" +version = "0.6.3" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {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"}, + {file = "ruff-0.6.3-py3-none-linux_armv6l.whl", hash = "sha256:97f58fda4e309382ad30ede7f30e2791d70dd29ea17f41970119f55bdb7a45c3"}, + {file = "ruff-0.6.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3b061e49b5cf3a297b4d1c27ac5587954ccb4ff601160d3d6b2f70b1622194dc"}, + {file = "ruff-0.6.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:34e2824a13bb8c668c71c1760a6ac7d795ccbd8d38ff4a0d8471fdb15de910b1"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bddfbb8d63c460f4b4128b6a506e7052bad4d6f3ff607ebbb41b0aa19c2770d1"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ced3eeb44df75353e08ab3b6a9e113b5f3f996bea48d4f7c027bc528ba87b672"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47021dff5445d549be954eb275156dfd7c37222acc1e8014311badcb9b4ec8c1"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d7bd20dc07cebd68cc8bc7b3f5ada6d637f42d947c85264f94b0d1cd9d87384"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:500f166d03fc6d0e61c8e40a3ff853fa8a43d938f5d14c183c612df1b0d6c58a"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42844ff678f9b976366b262fa2d1d1a3fe76f6e145bd92c84e27d172e3c34500"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70452a10eb2d66549de8e75f89ae82462159855e983ddff91bc0bce6511d0470"}, + {file = "ruff-0.6.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:65a533235ed55f767d1fc62193a21cbf9e3329cf26d427b800fdeacfb77d296f"}, + {file = "ruff-0.6.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2e2c23cef30dc3cbe9cc5d04f2899e7f5e478c40d2e0a633513ad081f7361b5"}, + {file = "ruff-0.6.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d8a136aa7d228975a6aee3dd8bea9b28e2b43e9444aa678fb62aeb1956ff2351"}, + {file = "ruff-0.6.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f92fe93bc72e262b7b3f2bba9879897e2d58a989b4714ba6a5a7273e842ad2f8"}, + {file = "ruff-0.6.3-py3-none-win32.whl", hash = "sha256:7a62d3b5b0d7f9143d94893f8ba43aa5a5c51a0ffc4a401aa97a81ed76930521"}, + {file = "ruff-0.6.3-py3-none-win_amd64.whl", hash = "sha256:746af39356fee2b89aada06c7376e1aa274a23493d7016059c3a72e3b296befb"}, + {file = "ruff-0.6.3-py3-none-win_arm64.whl", hash = "sha256:14a9528a8b70ccc7a847637c29e56fd1f9183a9db743bbc5b8e0c4ad60592a82"}, + {file = "ruff-0.6.3.tar.gz", hash = "sha256:183b99e9edd1ef63be34a3b51fee0a9f4ab95add123dbf89a71f7b1f0c991983"}, ] [[package]] @@ -2803,13 +2811,13 @@ test = ["coverage[toml] (>=7)", "mypy (>=1.2.0)", "pytest (>=7)"] [[package]] name = "typer" -version = "0.12.3" +version = "0.12.5" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false python-versions = ">=3.7" files = [ - {file = "typer-0.12.3-py3-none-any.whl", hash = "sha256:070d7ca53f785acbccba8e7d28b08dcd88f79f1fbda035ade0aecec71ca5c914"}, - {file = "typer-0.12.3.tar.gz", hash = "sha256:49e73131481d804288ef62598d97a1ceef3058905aa536a1134f90891ba35482"}, + {file = "typer-0.12.5-py3-none-any.whl", hash = "sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b"}, + {file = "typer-0.12.5.tar.gz", hash = "sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722"}, ] [package.dependencies] @@ -2831,13 +2839,13 @@ files = [ [[package]] name = "types-python-dateutil" -version = "2.9.0.20240316" +version = "2.9.0.20240821" description = "Typing stubs for python-dateutil" optional = false python-versions = ">=3.8" files = [ - {file = "types-python-dateutil-2.9.0.20240316.tar.gz", hash = "sha256:5d2f2e240b86905e40944dd787db6da9263f0deabef1076ddaed797351ec0202"}, - {file = "types_python_dateutil-2.9.0.20240316-py3-none-any.whl", hash = "sha256:6b8cb66d960771ce5ff974e9dd45e38facb81718cc1e208b10b1baccbfdbee3b"}, + {file = "types-python-dateutil-2.9.0.20240821.tar.gz", hash = "sha256:9649d1dcb6fef1046fb18bebe9ea2aa0028b160918518c34589a46045f6ebd98"}, + {file = "types_python_dateutil-2.9.0.20240821-py3-none-any.whl", hash = "sha256:f5889fcb4e63ed4aaa379b44f93c32593d50b9a94c9a60a0c854d8cc3511cd57"}, ] [[package]] @@ -2853,21 +2861,21 @@ files = [ [[package]] name = "typos" -version = "1.23.6" +version = "1.24.3" description = "Source Code Spelling Correction" optional = false python-versions = ">=3.7" files = [ - {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"}, + {file = "typos-1.24.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c555fa83634586f0449e9c1419992ee712209411685c0922f9d5f2adb24374cf"}, + {file = "typos-1.24.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ea45a4b7e95ec58aab0fad0e4b0babfd6baceb8f4a14ab5fe16cc793b2088d9f"}, + {file = "typos-1.24.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b740701332606171d4fedb857a738af657fc79abd1d7e2452c557dd6c6d5f6c"}, + {file = "typos-1.24.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:afeef74053575d83e2c1224659193542eacc3dcc385138b0ec8f2700623531a7"}, + {file = "typos-1.24.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bd84a725bc3a200600f3f516f07f7a9fb5248b6df8346379411bd206bc2c9c4"}, + {file = "typos-1.24.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:caff99b67f4ef9926291854da09553be9b565c46fed9b777a1581c833cf8c8e7"}, + {file = "typos-1.24.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d6ceb4afc88706e165e2c8e9ab12b56bab92a88ec153885cc44d48961087f6e8"}, + {file = "typos-1.24.3-py3-none-win32.whl", hash = "sha256:4d7465ca1ddf9f49f8fb0a657b70829f6a305997c53a6503ec5b2a9097c644f7"}, + {file = "typos-1.24.3-py3-none-win_amd64.whl", hash = "sha256:21b7a194a7dff9b5f07e4bb2e86ae60558fed26c64eff637a2a839f42be4f933"}, + {file = "typos-1.24.3.tar.gz", hash = "sha256:99667e46924a2a2977c7c0c2f3ee02407d6a134c723851fc1d7f08860e94b2de"}, ] [[package]] @@ -2920,13 +2928,13 @@ files = [ [[package]] name = "werkzeug" -version = "3.0.3" +version = "3.0.4" 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"}, + {file = "werkzeug-3.0.4-py3-none-any.whl", hash = "sha256:02c9eb92b7d6c06f31a782811505d2157837cea66aaede3e217c7c27c039476c"}, + {file = "werkzeug-3.0.4.tar.gz", hash = "sha256:34f2371506b250df4d4f84bfe7b0921e4762525762bbd936614909fe25cd7306"}, ] [package.dependencies] @@ -3093,101 +3101,103 @@ dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "sphinx"] [[package]] name = "yarl" -version = "1.9.4" +version = "1.9.6" description = "Yet another URL library" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"}, - {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"}, - {file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"}, - {file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"}, - {file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"}, - {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"}, - {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"}, - {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"}, - {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"}, - {file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"}, - {file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"}, - {file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"}, - {file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"}, - {file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"}, - {file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"}, - {file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"}, - {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"}, - {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"}, + {file = "yarl-1.9.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4300c792fa8a9bd2b3649b8f7a7b184128552c799d1593b8e866c5784aacf064"}, + {file = "yarl-1.9.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3777804ae06edfc354c757419e89eb3d640ff04d6477aed76fe0afe72e6e6e48"}, + {file = "yarl-1.9.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d602e77ff2bf949064e88cb6c41f1d7fe4698ddfec7ccdb628d419886136d437"}, + {file = "yarl-1.9.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c14f5d3220575a0392bd06028342e0527c3a873c72d87879418ff32919a6f11"}, + {file = "yarl-1.9.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92c65fa0bb904a1d24a5f6f9929feaa1573f93a1da5e3843136d28161c5d2cfd"}, + {file = "yarl-1.9.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cfa9b9ce66e9939e1dafdb9d951cedcebf4e3bec999c9bc84ba16d246f6fd8f"}, + {file = "yarl-1.9.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a73607a01744ff6631f6b7a2e78c73ea24f025c1808f5c246957b92d8da56362"}, + {file = "yarl-1.9.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ce5ee549d3f8236327be68e4a3bda15b57137077d535dcc3dc4a521e8999536"}, + {file = "yarl-1.9.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:990a69a85f5dc9ceeeff76dce4f53ff8eea758f127ad5c7ed07af4ec406d0712"}, + {file = "yarl-1.9.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6a194c57d3254579c830e3ab9a3a828f5bf4fed62b1fcc662446e7d6683d84e3"}, + {file = "yarl-1.9.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:25645b4dd57c71bd90d090eb84b962d1977ba9b1633de8726b5acd2e17637fbf"}, + {file = "yarl-1.9.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:f3c9c928cbd159b8f89aded601e6844d926be33434fdde7cee2b843a4364aa02"}, + {file = "yarl-1.9.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15041b0b0a245a718551af98047accae093b7aa8f2fc085a6fdf85244c6a2034"}, + {file = "yarl-1.9.6-cp310-cp310-win32.whl", hash = "sha256:40eb9f092b9d576c3ca97dc405538914bcc96be1bd2099c8dc4bf2fe78b54c03"}, + {file = "yarl-1.9.6-cp310-cp310-win_amd64.whl", hash = "sha256:da47fb9ba6d18a3f63365da141399ad56a2b00d0432bbd0e0b2ee534acfef430"}, + {file = "yarl-1.9.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fee93d050e834fe12ec65e3fa762a24be622f19d7d4dd1d16ea99daab5568a3a"}, + {file = "yarl-1.9.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad189fe2c7e0b38cd42d9053c90ab5edc85c9169b6c495c2415b9c74e88cca9e"}, + {file = "yarl-1.9.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cbf3bb663138621aad571446bab804cbd4f69bdebcb952d555d0ba4bd19e4bb6"}, + {file = "yarl-1.9.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb9ed9849918bf4b1262a2b323844c0751dab5151481a9f2a1d1a7794506b692"}, + {file = "yarl-1.9.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5805cf7060eee1c928731eae8d2999b1dbcc158a1fdd53bc88819258b8cdf4d9"}, + {file = "yarl-1.9.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19fd2614cc11ca01460e9198ffb80da58ada3404e92c23dd352bd7b1a93a6400"}, + {file = "yarl-1.9.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f22fd081fa74590f2de817fb0f8bd3329baa96f2f3baad53210c8864bbbe6d7"}, + {file = "yarl-1.9.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9da6010b1f7f1a6a0804190ed895f3425c03230ff9bbb0edde961221e33e2447"}, + {file = "yarl-1.9.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9c5a5425ea221ed548bc0b7db089b6e69f0abcb9b318d2f81e252ce1a87cb432"}, + {file = "yarl-1.9.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c79acad3d6805b0701ffd4789dc14020cdb1977c436e3b81ba7a507e497f7146"}, + {file = "yarl-1.9.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:61f9e9c51717651319ea1f3bfd838979ad13e0a086abae1055b72e21a35c86ee"}, + {file = "yarl-1.9.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6caebfbade5e4f19d6c2caf0f3cb56c788ff22aaf6c93d8e4c57d6e6457c5002"}, + {file = "yarl-1.9.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e1df9ce83a30e3580cba87a050e2a4635a806d96633ecd99d1e6f3b329a855be"}, + {file = "yarl-1.9.6-cp311-cp311-win32.whl", hash = "sha256:72556c7273b3c1f9e2eaf3f4caa2de597ab2aaec06d87c7a5840522838660316"}, + {file = "yarl-1.9.6-cp311-cp311-win_amd64.whl", hash = "sha256:ef6563bfa47b8f51e37ec4cd867690c4da5be075daa63e7348a72c486f71b056"}, + {file = "yarl-1.9.6-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:495c251bf439d3d40ee99eb25e2d8af2fb100b4727a1ca82624273fcb0146680"}, + {file = "yarl-1.9.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:89b3b220eca621a4155f3affb16e203e2c9d5de894a864fec29e6674018f2622"}, + {file = "yarl-1.9.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4275e619a95c61a793c47fcd5f1fe9aa88273bdd56e09594bfb7d0784dd4ffdd"}, + {file = "yarl-1.9.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b84baedf1ea3986940d1e88e9f26688a06635c250b9ce14ca4f64355ab33caf"}, + {file = "yarl-1.9.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:494dbe74318ed7190e3a189973600a50b8d3b2027f6915fc3265d5e0dc465077"}, + {file = "yarl-1.9.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ecb0c3f5fa89dfbb926dd743c6c28868b85ff22570f1a82f772515bf535ad54"}, + {file = "yarl-1.9.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70751d8bb5dacce02b808f7bb71545d658e5a21e4b312f0a20f6de4d5e007211"}, + {file = "yarl-1.9.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc6a25684df320e49b17eb34bc7638cc3dac5d70f4ff3b15ec25f29156a304ea"}, + {file = "yarl-1.9.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d466e1b28338910c2d7e54a22a0542d7a4f989c7a4824f4b40d2f2a14d97b65c"}, + {file = "yarl-1.9.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:50a024dc446ef0b0bc96176c6bfd6b9825e2d306a3e2be8fad09406e7b3630e7"}, + {file = "yarl-1.9.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7e00e3e33a51ff8059bdec30336b3910fdfc121e2d1ed0c51d28a4198411eafc"}, + {file = "yarl-1.9.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d6609652c8fbe903be56782fb99a2e481a7ef5382900988baca9312da72dfede"}, + {file = "yarl-1.9.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad199a15f616abfecb1e8eec053e1193c75c21fca136b5a401123c3e78ee989d"}, + {file = "yarl-1.9.6-cp312-cp312-win32.whl", hash = "sha256:0d41d541080730548f6c03932bfe7b08aff964a172fa5623695970ceac349cc2"}, + {file = "yarl-1.9.6-cp312-cp312-win_amd64.whl", hash = "sha256:9a88b543175dc9884d919eab40bd3d39c99b6dedaa17910f6f431d0c3495d212"}, + {file = "yarl-1.9.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a314f4f481d881f698148260036752e56934b59692f717258a65f61e342d392c"}, + {file = "yarl-1.9.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:95053ac3b499a5638718ed33c2e7d2724ef3d4e9c0f0e286675506ab026aedd7"}, + {file = "yarl-1.9.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:076ce8699521f6e5acd6c69fb6de96d7f78d9ec6384b162cbf1039159997e7a6"}, + {file = "yarl-1.9.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0999c76aa302455f64d59ec8fa34718fed0427fd069c790cfcc6940d6b17a49"}, + {file = "yarl-1.9.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2865c25ab6ab5a93bda6278920e3918b57a3b12b4b07c7207060a5787908c57a"}, + {file = "yarl-1.9.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:939779e9f5802305038779651b0062be3eec63bbdc1b9e7c3ea8dfde58a74663"}, + {file = "yarl-1.9.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4aebcedf28b02a4d575aeb67c3dc4b6b0533b72231298cf2b0fd7e3060decfe5"}, + {file = "yarl-1.9.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8e91970222df517b6de5346c74a246919a5d3f4a8fd4117b0b1dd9d935eb648"}, + {file = "yarl-1.9.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:443bb24327e3b2a65a646f79d45acbf883b46c7ebc8ea5fbb6057e124a2ffb84"}, + {file = "yarl-1.9.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b033089070f63cfbd06f9f3926c56ed2f4abac8bd389e18a086c56603674ae69"}, + {file = "yarl-1.9.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:852e213f2fe6a1605c87dfd9bea69a43090cb47daf49991bbeb35ad4a21c87bb"}, + {file = "yarl-1.9.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:683abc326e3cca63ec3bd0785b44fe39237822737b99453956817214b5eca3d4"}, + {file = "yarl-1.9.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ccea3d444291487f0e044f92a3bd72c2ad2880dde6409ecb7f12c5571022ed0c"}, + {file = "yarl-1.9.6-cp313-cp313-win32.whl", hash = "sha256:dac4e5afad0707beed2d5554cf1003ba0c4ce83578e254a5bac8aa03df9fe2c6"}, + {file = "yarl-1.9.6-cp313-cp313-win_amd64.whl", hash = "sha256:e333ed4bbf317a2424d865ec4836d1f3560e7beee8e0fd0ba44110d9e9174d9f"}, + {file = "yarl-1.9.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b75692302951aaa8cdd3284b0d28089c1fcc183e75b78723e9288921dd00cab4"}, + {file = "yarl-1.9.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9de7f08fd02b51068b8e801d26af567fbe4cc7f380638106d4e38b7c8b6349a1"}, + {file = "yarl-1.9.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d795a65cbeb0b39487ba6b36265d44f9c7bb0930bb40b26e14964bcff3d6bbc8"}, + {file = "yarl-1.9.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37f8bb65b50f07118243e68080836a57477d12da3b47c0b5112feb9cb5d8c1f"}, + {file = "yarl-1.9.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:080f9ad6076f717d45d8689f3c3ca3f880aeb5c8667ac936d98d9d285a97a549"}, + {file = "yarl-1.9.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8a93ef52dbd604ff2e039f310cb405c8e59b91e38198e5ab62ba38169fe4c798"}, + {file = "yarl-1.9.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dfb4e7e53e3dd9a4f32818abfe4575e7cc6f5fa002a859529108ed1c657e169"}, + {file = "yarl-1.9.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:77a34663752872cece9cf11c495fec50687f56ba4ab999952c3cd4869a6acc39"}, + {file = "yarl-1.9.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:515f396bf29538bdbb817cad03ebc453f903dc9edae03a835d70088042425553"}, + {file = "yarl-1.9.6-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:be4df89374aef89ba7200866e47bde89af67edc2bdbe01cae39e9ab7309365f6"}, + {file = "yarl-1.9.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e66e5b4c5884d67e892e0dd0cf79f2af6c4b9078841ebb2b2e7a75c3df16b71d"}, + {file = "yarl-1.9.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:c1aa72ea579d8615f4f135f55c7f5c1710f9e743fa2576fe903ad75de0777e66"}, + {file = "yarl-1.9.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f52b1caa08842101b24662507326eb9899471e3f56c28706355f8097a2fad8df"}, + {file = "yarl-1.9.6-cp38-cp38-win32.whl", hash = "sha256:5688e48908f68dd1175a5a775bd562ba431a3ab010593915558f7d143d63cf48"}, + {file = "yarl-1.9.6-cp38-cp38-win_amd64.whl", hash = "sha256:acf846ad1dcd40b549651c984e9ceccd134a3f3f9938a51c77cda75cebc7b46a"}, + {file = "yarl-1.9.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4318397b84b417daab204b88929387087846ef1a182b74f7c7565f4c5bf14ec2"}, + {file = "yarl-1.9.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:839bce5479c5d2fade4a2619e6232060b56595b08a99397ea38b0e0bef15f59c"}, + {file = "yarl-1.9.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:58e6d67537b6d93c8bf4102917c337c3bd39bb70f0910b5ca6ff1d102721069c"}, + {file = "yarl-1.9.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f5272514166287d89bdb1215da3ccc7f31bce6b481425add8e3d11bdad4e1dc"}, + {file = "yarl-1.9.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea4b3e4f24e3d60f3361dec53db31bc167dba1f9eb7f861377fa4d681e7afdb4"}, + {file = "yarl-1.9.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6ce7a8edd1813118fa79dc5d3aa12cc4735265bac958e173013506fec49b0d78"}, + {file = "yarl-1.9.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a24034845530aba47ad087c0190789af142dbc7bca38583e567be2da4ba5b90"}, + {file = "yarl-1.9.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:053d5ab41e31c6f86038ba1dca5dc8d1658d0fb105adf2d32606d7727904436b"}, + {file = "yarl-1.9.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1fa7e4c9118d513a62c493f985e9045a0de072ee4d23a5035e8b77ad30992dbe"}, + {file = "yarl-1.9.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f24ce88a0b3f8e59bd27c45633b77699b9a9dc6d23045c3b83f2334e8aa799e8"}, + {file = "yarl-1.9.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:0a987726abdf4ae961a084e79a03fe9e46fbf419c63d17cf0280cdecc1670b5b"}, + {file = "yarl-1.9.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:7abc6a65e8a5909f725efe59f05e26b8eb941a8b475525eaf0ace9c6254fc729"}, + {file = "yarl-1.9.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:69982ee7dce2073fffc50731cb1ae927a715c29d27b4f22b8c2edb56714da8d1"}, + {file = "yarl-1.9.6-cp39-cp39-win32.whl", hash = "sha256:283d7649a2805c64eabd246f763321149a370dc696bfd3f575453cb75506e959"}, + {file = "yarl-1.9.6-cp39-cp39-win_amd64.whl", hash = "sha256:1e0c621ff807414b8a0b964251714e0038a355de6d2a2c67d6bf1db09c3bf38e"}, + {file = "yarl-1.9.6-py3-none-any.whl", hash = "sha256:d34a4c6fde4d49aab493214228d0e03f7e5a717f6da4fe65b879a3af3c22ad7b"}, + {file = "yarl-1.9.6.tar.gz", hash = "sha256:0bdc6a7b59efa0c34c90ef3da864f0c53e81a4640fbc461bfde9f1b0c64c3c81"}, ] [package.dependencies] @@ -3197,4 +3207,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.13" -content-hash = "04ddc91403dc15f010809e03514ca40312369eb05cd391b55043eefd4e3819f5" +content-hash = "1e848a2813a01a291f235d51ecf5273fc1337c810e35c90368240896fb33dbdd" diff --git a/pyproject.toml b/pyproject.toml index 02a14bb6..a3af6ed9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ pdoc = "^14.4.0" poethepoet = "^0.26.1" pre-commit = "^3.7.1" - ruff = "^0.6.0" + ruff = "^0.6.3" shellcheck-py = "^0.9.0.6" types-aiofiles = "^23.2.0.0" typos = "^1.23.2" diff --git a/src/valentina/characters/add_from_sheet.py b/src/valentina/characters/add_from_sheet.py index 9421402f..a5ae1ebe 100644 --- a/src/valentina/characters/add_from_sheet.py +++ b/src/valentina/characters/add_from_sheet.py @@ -2,7 +2,7 @@ import asyncio import uuid -from typing import Any +from collections.abc import Callable import discord from beanie import WriteRules @@ -74,7 +74,14 @@ async def button_pressed(self, interaction) -> None: # type: ignore [no-untyped class AddFromSheetWizard: - """A character generation wizard that walks the user through setting a value for each trait. This is used for entering a character that has already been created from a physical character sheet.""" + """A character generation wizard for entering pre-existing characters. + + This wizard guides the user through the process of setting values for each trait + of a character that has already been created on a physical character sheet. + It provides an interactive interface to input trait values systematically, + ensuring all necessary information is captured for the digital representation + of the character. + """ def __init__( self, @@ -151,7 +158,19 @@ async def __view_callback( async def __finalize_character( self, ) -> None: - """Add the character to the database and inform the user they are done.""" + """Finalize character creation and notify the user. + + Add the character to the database, create associated traits, link to a campaign + if applicable, update the user's character list, create a character channel + if in a campaign, and send a confirmation message to the user. + + This method handles the final steps of character creation after all traits + have been input by the user. + + Raises: + discord.errors.HTTPException: If there's an error creating the character's channel. + beanie.exceptions.DocumentSaveError: If there's an error saving the character or user data. + """ # Add the character to the database await self.character.insert() @@ -210,7 +229,22 @@ async def __finalize_character( async def __send_messages( self, *, interaction: discord.Interaction | None = None, message: str | None = None ) -> None: - """Query a trait.""" + """Send messages to query trait information during character creation. + + This method handles the process of sending messages to the user to gather + trait information for character creation. It prepares and sends an embed + with the current trait being queried, and sets up the view for user interaction. + + Args: + interaction (discord.Interaction | None): The interaction object if this + method is called in response to a user interaction. Defaults to None. + message (str | None): An optional message to include in the embed description. + Defaults to None. + + Raises: + discord.errors.HTTPException: If there's an error sending the message. + discord.errors.Forbidden: If the bot doesn't have permission to send messages. + """ trait_name, trait_category = self.trait_list[0] description = "This wizard will guide you through the character creation process.\n\n" @@ -248,14 +282,32 @@ async def __send_messages( await interaction.response.edit_message(embed=embed, view=self.view) async def __timeout(self) -> None: - """Inform the user they took too long.""" + """Inform the user that their character generation session has timed out due to inactivity. + + This method is called when the user fails to respond within the allotted time during + the character creation process. It sends a message to the user, cancels the character + generation, and logs the timeout event. + + Raises: + discord.errors.HTTPException: If there's an error sending the message. + discord.errors.Forbidden: If the bot doesn't have permission to send messages. + """ errmsg = f"Due to inactivity, your character generation on **{self.ctx.guild.name}** has been canceled." await self.edit_message(content=errmsg, embed=None, view=None) logger.info("CHARGEN: Timed out") @property - def edit_message(self) -> Any: - """Get the proper edit method for editing our message outside of an interaction.""" + def edit_message(self) -> Callable: + """Get the appropriate edit method for modifying messages outside of an interaction. + + Returns: + Callable: The edit method to use. If self.msg exists, returns self.msg.edit, + otherwise returns self.ctx.respond. + + This property determines the correct method to use for editing messages + in different contexts, allowing for flexible message manipulation + throughout the character creation process. + """ if self.msg: return self.msg.edit return self.ctx.respond diff --git a/src/valentina/characters/chargen.py b/src/valentina/characters/chargen.py index 98370086..86700ba2 100644 --- a/src/valentina/characters/chargen.py +++ b/src/valentina/characters/chargen.py @@ -130,11 +130,15 @@ async def interaction_check(self, interaction: discord.Interaction) -> bool: class BeginCancelCharGenButtons(discord.ui.View): # pragma: no cover """Manage buttons for initiating or canceling the character generation process. + This view provides buttons for users to either start rolling characters or cancel the process. + Args: - author (Union[discord.User, discord.Member, None]): The author of the interaction. + author (discord.User | discord.Member | None): The author of the interaction. + If provided, only this user can interact with the buttons. Attributes: - roll (bool): Whether to roll for characters. + roll (bool | None): Indicates whether to roll for characters. + Set to True if the roll button is clicked, False if cancelled, None otherwise. """ def __init__(self, author: discord.User | discord.Member | None = None): @@ -188,14 +192,17 @@ async def interaction_check(self, interaction: discord.Interaction) -> bool: class UpdateCharacterButtons(discord.ui.View): # pragma: no cover """Manage buttons for updating a character's attributes. + This view provides interactive buttons for various character update operations, + such as renaming the character or reallocating attribute dots. + Args: ctx (ValentinaContext): The context of the Discord application. character (Character): The character to update. - author (Union[discord.User, discord.Member, None]): The author of the interaction. + author (discord.User | discord.Member | None): The author of the interaction. Attributes: - updated (bool): Whether the character has been updated. - done (bool): Whether the update process is done. + updated (bool): Indicates whether the character has been updated. + done (bool): Indicates whether the update process is complete. """ def __init__( @@ -343,7 +350,26 @@ async def interaction_check(self, interaction: discord.Interaction) -> bool: class RNGCharGen: - """Randomly generate different parts of a character.""" + """Randomly generate different parts of a character. + + This class provides methods to randomly generate various aspects of a character, + including attributes, abilities, and other traits based on the specified + experience level and campaign settings. + + Args: + ctx (ValentinaContext): The context of the Discord application. + user (User): The user for whom the character is being generated. + experience_level (RNGCharLevel, optional): The experience level for character generation. + Defaults to a random level if not specified. + campaign (Campaign, optional): The campaign associated with the character. + Defaults to None. + + Attributes: + ctx (ValentinaContext): The context of the Discord application. + user (User): The user for whom the character is being generated. + experience_level (RNGCharLevel): The experience level for character generation. + campaign (Campaign): The campaign associated with the character, if any. + """ def __init__( self, @@ -361,11 +387,16 @@ def __init__( def _redistribute_trait_values( traits: list[CharacterTrait], concept: CharacterConcept ) -> list[CharacterTrait]: - """Redistribute trait values based on the concept. This method ensures that specific traits associated with a concept have high values for the specified traits. + """Redistribute trait values based on the character concept. + + Ensure that specific traits associated with a concept have high values by + redistributing points from less important traits. The redistribution process + continues until all concept-specific traits have a value of at least 3, or + until no more redistribution is possible. Args: - traits (list[CharacterTrait]): The traits to redistribute. - concept (CharacterConcept): The concept to use for redistribution. + traits (list[CharacterTrait]): The list of traits to redistribute. + concept (CharacterConcept): The character concept to use for redistribution. Returns: list[CharacterTrait]: The updated list of CharacterTrait objects after redistribution. @@ -398,17 +429,17 @@ def _redistribute_trait_values( return traits def _adjust_value_based_on_level(self, value: int) -> int: - """Adjust the discipline value based on the character's level. + """Adjust the discipline value based on the character's experience level. - For advanced and elite levels, the value is incremented by 1. - For new characters, the value is capped at 3. - The final value is constrained between 1 and 5. + Modify the given discipline value according to the character's experience level. + Increment the value for advanced and elite levels, cap it for new and intermediate + characters, and ensure the final value is within the valid range. Args: value (int): The initial discipline value. Returns: - int: The adjusted discipline value. + int: The adjusted discipline value, constrained between 1 and 5. """ # Increment value for advanced and elite levels if self.experience_level in {RNGCharLevel.ADVANCED, RNGCharLevel.ELITE}: @@ -438,20 +469,27 @@ async def generate_base_character( nationality: str = "us", nickname_is_class: bool = False, ) -> Character: - """Generate's the base character based on random values. + """Generate a base character with random attributes. - A base character consists of a combination of the following: + Generate a base character with randomly selected attributes including class, + concept, clan (for vampires), creed (for hunters), and name. Traits and + customizations are not included in this base generation. - - A random class - - A random concept - - A random clan (if applicable) - - A random creed (if applicable) - - A random name - - Traits and customizations are to be added later. + Args: + char_class (CharClass | None): Specific character class. If None, randomly selected. + concept (CharacterConcept | None): Specific character concept. If None, randomly selected. + clan (VampireClan | None): Specific vampire clan. If None, randomly selected for vampires. + creed (HunterCreed | None): Specific hunter creed. If None, randomly selected for hunters. + player_character (bool): Whether the character is a player character. + storyteller_character (bool): Whether the character is a storyteller character. + developer_character (bool): Whether the character is a developer character. + chargen_character (bool): Whether the character is generated through character generation. + gender (Literal["male", "female"] | None): Gender for name generation. + nationality (str): Nationality for name generation. Defaults to "us". + nickname_is_class (bool): Whether to use the class name as a nickname. Returns: - Character: The generated character. + Character: The generated base character. """ # Grab random name name_first, name_last = await fetch_random_name(gender=gender, country=nationality) @@ -513,7 +551,25 @@ async def generate_full_character( ) -> Character: """Generate a full character with random values. - This method generates a full character with random values for all traits and abilities and adds it to the database. This is primarily used for Storytellers to quickly create NPCs. + Generate a complete character with randomized values for all traits and abilities, + and add it to the database. This method is primarily used by Storytellers for + quick NPC creation. + + Args: + char_class (CharClass | None): The character's class. If None, a random class is chosen. + concept (CharacterConcept | None): The character's concept. If None, a random concept is chosen. + clan (VampireClan | None): The character's clan (for vampires). If None, a random clan is chosen. + creed (HunterCreed | None): The character's creed (for hunters). If None, a random creed is chosen. + player_character (bool): Whether this is a player character. Defaults to False. + storyteller_character (bool): Whether this is a storyteller character. Defaults to False. + developer_character (bool): Whether this is a developer character. Defaults to False. + chargen_character (bool): Whether this character was created through character generation. Defaults to False. + gender (Literal["male", "female"] | None): The character's gender. If None, a random gender is chosen. + nationality (str): The character's nationality. Defaults to "us". + nickname_is_class (bool): Whether to use the character's class as their nickname. Defaults to False. + + Returns: + Character: The fully generated character object. """ filtered_locals = {k: v for k, v in locals().items() if k != "self"} @@ -530,8 +586,15 @@ async def generate_full_character( async def random_attributes(self, character: Character) -> Character: """Randomly generate attributes for the character. + Generate and assign random attribute values for the given character based on their + concept, class, and experience level. This method handles the distribution of + attribute dots across physical, social, and mental categories. + Args: character (Character): The character for which to generate attributes. + + Returns: + Character: The updated character object with randomly generated attributes. """ logger.debug(f"Generate attribute values for {character.name}") @@ -594,8 +657,14 @@ async def random_attributes(self, character: Character) -> Character: async def random_abilities(self, character: Character) -> Character: """Randomly generate abilities for the character. + This method creates and assigns random ability values to the given character, + taking into account the character's concept and experience level. + Args: character (Character): The character for which to generate abilities. + + Returns: + Character: The updated character with randomly generated abilities. """ logger.debug(f"Generate ability values for {character.name}") @@ -656,11 +725,15 @@ async def random_abilities(self, character: Character) -> Character: async def random_disciplines(self, character: Character) -> Character: """Randomly generate disciplines for the character. + Generate and assign random discipline values for a given character based on their clan + and experience level. This method handles the logic for determining which disciplines + to assign and their values. + Args: character (Character): The character for which to generate disciplines. Returns: - Character: The updated character. + Character: The character with updated disciplines. """ logger.debug(f"Generate discipline values for {character.name}") @@ -713,11 +786,15 @@ async def random_disciplines(self, character: Character) -> Character: async def random_virtues(self, character: Character) -> Character: """Randomly generate virtues for the character. + Generate and assign random virtue values for the given character based on their + character class and the current experience level. The method calculates the total + number of dots to distribute among virtues and assigns them randomly. + Args: character (Character): The character for which to generate virtues. Returns: - Character: The updated character. + Character: The updated character with newly generated virtue traits. """ logger.debug(f"Generate virtue values for {character.name}") @@ -754,13 +831,17 @@ async def random_virtues(self, character: Character) -> Character: return character async def random_backgrounds(self, character: Character) -> Character: - """Randomly generate backgrounds for the character. + """Generate random backgrounds for the character. + + Generate and assign random background values for the given character based on their + character class and the current experience level. The method calculates the total + number of dots to distribute among backgrounds and assigns them randomly. Args: character (Character): The character for which to generate backgrounds. Returns: - Character: The updated character. + Character: The updated character with newly generated background traits. """ logger.debug(f"Generate background values for {character.name}") @@ -801,11 +882,16 @@ async def random_backgrounds(self, character: Character) -> Character: async def random_willpower(self, character: Character) -> Character: """Randomly generate willpower for the character. + Generate and assign willpower trait for the character based on their existing + Self-Control and Courage traits. If applicable, also generate and assign + a Humanity trait based on the character's Conscience trait. + Args: character (Character): The character for which to generate willpower. Returns: - Character: The updated character. + Character: The updated character with newly generated willpower + and potentially humanity traits. """ logger.debug(f"Generate willpower values for {character.name}") @@ -849,11 +935,15 @@ async def random_willpower(self, character: Character) -> Character: async def random_hunter_traits(self, character: Character) -> Character: """Randomly generate hunter traits for the character. + Generate and assign hunter-specific traits such as Willpower, Conviction, + and Edges based on the character's creed and experience level. If the + character doesn't have a creed, a random one is assigned. + Args: character (Character): The character for which to generate hunter traits. Returns: - Character: The updated character. + Character: The updated character with newly generated hunter traits. """ if character.char_class != CharClass.HUNTER: return character @@ -916,11 +1006,15 @@ async def random_hunter_traits(self, character: Character) -> Character: async def concept_special_abilities(self, character: Character) -> Character: """Assign special abilities based on the character's concept. + This method assigns special abilities to a character based on their concept, + but only if the character is a Mortal. For non-Mortal characters, it returns + the character unchanged. + Args: - character (Character): The character for which to assign special abilities. + character (Character): The character to assign special abilities to. Returns: - Character: The updated character. + Character: The updated character with assigned special abilities. """ if character.char_class != CharClass.MORTAL: return character @@ -955,14 +1049,8 @@ async def concept_special_abilities(self, character: Character) -> Character: class CharGenWizard: # pragma: no cover """Guide the user through a step-by-step character generation process. - Args: - ctx (ValentinaContext): The context of the Discord application. - campaign (Campaign): The campaign for which the character is being created. - user (GuildUser): The user who is creating the character. - hidden (bool, optional): Whether the interaction is hidden. Defaults to True. - - Attributes: - paginator (pages.Paginator): Container for the paginator. + This class manages the interactive process of creating a new character, + handling user inputs and generating character attributes. """ # TODO: Allow the user to select their special ability when a choice is available @@ -990,7 +1078,20 @@ def __init__( @staticmethod def _special_ability_char_sheet_text(character: Character) -> str: - """Generate the special abilities text for the character sheet.""" + """Generate the special abilities text for the character sheet. + + Generate and format the special abilities text for a character's sheet, + specifically for mortal characters. For non-mortal characters, return None. + + Args: + character (Character): The character object for which to generate + the special abilities text. + + Returns: + str | None: A formatted string containing the character's concept, + description, and special abilities if the character is a mortal. + Returns None for non-mortal characters. + """ # Extract concept information for mortals if character.char_class.name == CharClass.MORTAL.name: concept_info = CharacterConcept[character.concept_name].value @@ -1020,14 +1121,16 @@ async def _generate_character_sheet_embed( ) -> discord.Embed: """Create an embed for the character sheet. + Generate and return a Discord embed representing a character sheet. + Args: character (Character): The character for which to create the embed. - title (str | None, optional): The title of the embed. Defaults to None. - prefix (str | None, optional): The prefix for the description. Defaults to None. - suffix (str | None, optional): The suffix for the description. Defaults to None. + title (str | None): The title of the embed. If None, uses the character's name. + prefix (str | None): Additional text to prepend to the embed description. + suffix (str | None): Additional text to append to the embed description. Returns: - discord.Embed: The created embed. + discord.Embed: The created embed containing the character sheet information. """ # Create the embed return await sheet_embed( @@ -1042,11 +1145,16 @@ async def _generate_character_sheet_embed( async def _cancel_character_generation( self, msg: str | None = None, characters: list[Character] = [] ) -> None: - """Cancel the character generation process. + """Cancel the character generation process and clean up resources. + + This method handles the cancellation of character generation, deleting any partially + created characters and displaying a cancellation message to the user. Args: - msg (str, optional): Message to display. Defaults to None. - characters (list[Character], optional): The characters to delete. Defaults to []. + msg (str | None): Custom message to display upon cancellation. If None, a default + message is used. + characters (list[Character]): List of Character objects to be deleted. These are + typically partially created characters that need to be removed from the database. """ if not msg: msg = "No character was created." @@ -1065,8 +1173,12 @@ async def _cancel_character_generation( async def start(self, restart: bool = False) -> None: """Initiate the character generation wizard. + Start or restart the character generation process, presenting the user with + instructional embeds and options to begin or cancel character creation. + Args: - restart (bool, optional): Whether to restart the wizard. Defaults to False. + restart (bool): If True, restart the wizard with existing paginator. + If False, create a new paginator. Defaults to False. """ logger.debug("Starting the character generation wizard.") @@ -1136,12 +1248,15 @@ async def start(self, restart: bool = False) -> None: async def present_character_choices(self) -> None: """Guide the user through the character selection process. - This method generates three random characters for the user to choose from. - It then presents these characters using a paginator. The user can either - select a character, reroll for new characters, or cancel the process. + Generate three random characters and present them to the user for selection. + Display character details using a paginator, allowing the user to review + and choose a character, reroll for new options, or cancel the process. + + This method handles the core logic of character generation and selection, + including trait assignment and presentation of character options. Returns: - None: This method returns nothing. + None """ logger.debug("Starting the character selection process") @@ -1252,11 +1367,11 @@ async def present_character_choices(self) -> None: async def finalize_character_selection(self, character: Character) -> None: """Review and finalize the selected character. - This method presents the user with an updated character sheet. - The user can either finalize the character or make additional changes. + Present the user with an updated character sheet and allow them to finalize + the character or make additional changes. Args: - character (Character): The selected character to review. + character (Character): The selected character to review and finalize. """ logger.debug(f"CHARGENL Update the character: {character.full_name}") @@ -1291,13 +1406,17 @@ async def finalize_character_selection(self, character: Character) -> None: await self.campaign.sort_channels(self.ctx) async def spend_freebie_points(self, character: Character) -> Character: - """Spend freebie points. + """Spend freebie points on a character. + + Present the user with an interface to allocate freebie points to various + character traits. This method handles the process of spending freebie points, + updating the character sheet, and finalizing the character creation. Args: - character (Character): The character for which to spend freebie points. + character (Character): The character to spend freebie points on. Returns: - Character: The created character. + Character: The updated character after spending freebie points. """ logger.debug(f"Spending freebie points for {character.name}") diff --git a/src/valentina/characters/spend_experience.py b/src/valentina/characters/spend_experience.py index 54a28943..cb4869e1 100644 --- a/src/valentina/characters/spend_experience.py +++ b/src/valentina/characters/spend_experience.py @@ -19,7 +19,21 @@ class SpendFreebiePoints(discord.ui.View): - """Guide the user through spending freebie points.""" + """Guide the user through the process of spending freebie points on character traits. + + This class provides a wizard-like interface for users to allocate their + character's freebie points to various traits. It handles the selection of + trait categories, individual traits, and the allocation of points. + + Attributes: + ctx (ValentinaContext): The context of the Discord application. + character (Character): The character on which freebie points are being spent. + trait_category (TraitCategory): The selected category of traits. + trait (CharacterTrait): The specific trait selected for point allocation. + msg (discord.WebhookMessage): The message used for user interaction. + cancelled (bool): Flag indicating if the wizard process was cancelled. + + """ # TODO: Add merits/flaws/backgrounds or other areas not on sheet diff --git a/src/valentina/debug_webui.py b/src/valentina/debug_webui.py index be7a7c9c..a22e3e73 100644 --- a/src/valentina/debug_webui.py +++ b/src/valentina/debug_webui.py @@ -9,9 +9,6 @@ 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.""" diff --git a/src/valentina/main.py b/src/valentina/main.py index 087e6376..be2fd98b 100644 --- a/src/valentina/main.py +++ b/src/valentina/main.py @@ -10,7 +10,7 @@ from loguru import logger from valentina.models.bot import Valentina -from valentina.utils import ValentinaConfig, console, instantiate_logger +from valentina.utils import ValentinaConfig, debug_environment_variables, instantiate_logger from valentina.utils.database import test_db_connection from .__version__ import __version__ @@ -34,16 +34,9 @@ def main( ), ) -> None: """Run Valentina.""" - if os.environ["VALENTINA_TRACE"]: - console.rule("Env Vars") - for key, value in os.environ.items(): - if key not in {"PS1", "LS_COLORS", "PATH"}: - console.log(f"{key}: {value}") - - console.rule("ValentinaConfig") - settings_object = ValentinaConfig().model_dump(mode="python") - for key, value in settings_object.items(): - console.log(f"{key}: {value}") + # Print environment variables and ValentinaConfig settings if VALENTINA_TRACE is set + if os.environ.get("VALENTINA_TRACE"): + debug_environment_variables() # Instantiate the logger instantiate_logger() diff --git a/src/valentina/models/__init__.py b/src/valentina/models/__init__.py index 422beaa3..1e8cfdfb 100644 --- a/src/valentina/models/__init__.py +++ b/src/valentina/models/__init__.py @@ -1,4 +1,11 @@ -"""Models for Valentina.""" +"""Define and export models for the Valentina application. + +The models defined here represent different entities in the application, such as +campaigns, characters, guilds, and users. They are used for data persistence, +business logic, and interaction with the database. + +Import this module to access all Valentina models in other parts of the application. +""" from .campaign import ( Campaign, diff --git a/src/valentina/models/bot.py b/src/valentina/models/bot.py index bee4a77d..18f13cfd 100644 --- a/src/valentina/models/bot.py +++ b/src/valentina/models/bot.py @@ -39,10 +39,21 @@ # Subclass discord.ApplicationContext to create custom application context class ValentinaContext(discord.ApplicationContext): - """A custom application context for Valentina.""" + """Extend discord.ApplicationContext with Valentina-specific functionality. + + Provide custom methods and properties for handling Valentina's command context. + Implement logging capabilities and embed creation for consistent message formatting. + """ def log_command(self, msg: str, level: LogLevel = LogLevel.INFO) -> None: # pragma: no cover - """Log the command to the console and log file.""" + """Log the executed command with contextual information. + + Log the command details to both console and log file, including the author, + command name, and channel where it was executed. Determine the appropriate + log level and construct a detailed log message with the command's context. + Use introspection to identify the calling function and create a hierarchical + logger name for better traceability. + """ author = f"@{self.author.display_name}" if hasattr(self, "author") else None command = f"'/{self.command.qualified_name}'" if hasattr(self, "command") else None channel = f"#{self.channel.name}" if hasattr(self, "channel") else None @@ -73,13 +84,18 @@ def log_command(self, msg: str, level: LogLevel = LogLevel.INFO) -> None: # pra ) def _message_to_embed(self, message: str) -> discord.Embed: # pragma: no cover - """Convert a string message to a discord embed. + """Convert a string message to a Discord embed. + + Create a Discord embed object from the given message string. Set the embed's + color based on the command category, add a timestamp, and include footer + information about the command, user, and channel. The embed's title is set + to the input message. Args: - message (str): The message to be converted. + message (str): The message to be used as the embed's title. Returns: - discord.Embed: The created embed. + discord.Embed: A fully formatted Discord embed object. """ # Set color based on command if hasattr(self, "command") and ( @@ -118,17 +134,18 @@ def _message_to_embed(self, message: str) -> discord.Embed: # pragma: no cover async def post_to_error_log( self, message: str | discord.Embed, error: Exception ) -> None: # pragma: no cover - """Send an error message or embed to the guild's error log channel. + """Post an error message or embed to the guild's error log channel. - If the error log channel exists, convert the input message to an embed if it's a string and send it to the guild's error log channel. + Convert the input message to an embed if it's a string. Attempt to send the + error information to the guild's designated error log channel. If the message + is too long, send a truncated version with basic error details. Args: - ctx (discord.ApplicationContext): The context for the discord command. - message (str|discord.Embed): The error message or embed to send to the channel. - error (Exception): The exception that triggered the error log message. + message (str | discord.Embed): The error message or embed to send. + error (Exception): The exception that triggered the error log. Raises: - discord.DiscordException: If the error message could not be sent to the channel. + discord.DiscordException: If the error message cannot be sent to the channel. """ # Get the database guild object and error log channel guild = await Guild.get(self.guild.id) @@ -149,16 +166,15 @@ async def post_to_error_log( await error_log_channel.send(embed=embed) async def post_to_audit_log(self, message: str | discord.Embed) -> None: # pragma: no cover - """Send a message to the audit log channel for a guild. + """Send a message to the guild's audit log channel. - If a string is passed in, an embed will be created from it. If an embed is passed in, it will be sent as is. + Convert the input message to an embed if it's a string, otherwise send the provided embed. Log the message content to the command log. Attempt to send the message to the guild's designated audit log channel. Args: - ctx (discord.ApplicationContext): The context in which the command was invoked. - message (str|discord.Embed): The message to be sent to the log channel. + message (str | discord.Embed): The message or embed to send to the audit log. Raises: - discord.DiscordException: If the message could not be sent. + errors.MessageTooLongError: If the message exceeds Discord's character limit. """ # Get the database guild object and error log channel guild = await Guild.get(self.guild.id) @@ -179,13 +195,17 @@ async def post_to_audit_log(self, message: str | discord.Embed) -> None: # prag raise errors.MessageTooLongError from e async def can_kill_character(self, character: Character) -> bool: - """Check if the user can kill the character. + """Determine if the user has permission to kill the specified character. + + Check the user's permissions against the guild's settings to decide if they + can kill the given character. Consider the user's role, guild permissions, + and character ownership when making this determination. Args: - character (Character): The character to check. + character (Character): The character to potentially kill. Returns: - bool: True if the user can kill the character, False otherwise. + bool: True if the user has permission to kill the character, False otherwise. """ # Always allow administrators to kill characters if isinstance(self.author, discord.Member) and self.author.guild_permissions.administrator: @@ -216,13 +236,19 @@ async def can_kill_character(self, character: Character) -> bool: return True async def can_manage_traits(self, character: Character) -> bool: - """Check if the user can manage traits for the character. + """Determine if the user has permission to manage traits for the specified character. + + Check the user's permissions against the guild's settings to decide if they + can manage traits for the given character. Consider the user's role, guild + permissions, character ownership, and time since character creation when + making this determination. Args: - character (Character): The character to check. + character (Character): The character whose traits may be managed. Returns: - bool: True if the user can manage traits for the character, False otherwise. + bool: True if the user has permission to manage the character's traits, + False otherwise. """ # Always allow administrators to manage traits if isinstance(self.author, discord.Member) and self.author.guild_permissions.administrator: @@ -264,13 +290,19 @@ async def can_manage_traits(self, character: Character) -> bool: return True async def can_grant_xp(self, user: User) -> bool: - """Check if the user can grant xp to the user. + """Determine if the current user has permission to grant XP to the specified user. + + Check the guild's XP granting permission settings and the current user's roles + to determine if they are allowed to grant XP. This method considers various + scenarios such as unrestricted access, player-only restrictions, and + storyteller-only permissions. Args: - user (User): The user to check. + user (User): The target user to whom XP might be granted. Returns: - bool: True if the user can grant xp to the user, False otherwise. + bool: True if the current user has permission to grant XP to the specified user, + False otherwise. """ # Always allow administrators to manage traits if isinstance(self.author, discord.Member) and self.author.guild_permissions.administrator: @@ -301,10 +333,15 @@ async def can_grant_xp(self, user: User) -> bool: return True async def can_manage_campaign(self) -> bool: - """Check if the user can manage the campaign. + """Determine if the current user has permission to manage the campaign. + + Check the guild's campaign management permission settings and the current user's roles + to determine if they are allowed to manage the campaign. Consider various scenarios + such as unrestricted access and storyteller-only permissions. Always allow + administrators to manage campaigns. Returns: - bool: True if the user can manage the campaign, False otherwise. + bool: True if the user has permission to manage the campaign, False otherwise. """ # Always allow administrators to manage traits if isinstance(self.author, discord.Member) and self.author.guild_permissions.administrator: @@ -338,20 +375,20 @@ async def channel_update_or_add( category: discord.CategoryChannel | None = None, permissions_user_post: discord.User | None = None, ) -> discord.TextChannel: # pragma: no cover - """Create or update a channel in the guild. + """Create or update a channel in the guild with specified permissions and attributes. - Either create a new text channel in the guild or update an existing one based on the name. Set permissions for default role, player role, and storyteller role. If a member is a bot, set permissions to manage. + Create a new text channel or update an existing one based on the provided name. Set permissions for default role, player role, and storyteller role. Automatically grant manage permissions to bot members. If specified, set posting permissions for a specific user. Args: - permissions (tuple[ChannelPermission, ChannelPermission, ChannelPermission]): The permissions for the channel. - channel (discord.TextChannel, optional): The channel to update. Defaults to None. - name (str, optional): The name of the channel. Defaults to None. - topic (str, optional): The topic of the channel. Defaults to None. - category (discord.CategoryChannel, optional): The category of the channel. Defaults to None. - permissions_user_post (discord.User, optional): The user to set permissions for posting. Defaults to None. + permissions (tuple[ChannelPermission, ChannelPermission, ChannelPermission]): Permissions for default role, player role, and storyteller role respectively. + channel (discord.TextChannel, optional): Existing channel to update. Defaults to None. + name (str, optional): Name for the channel. Defaults to None. + topic (str, optional): Topic description for the channel. Defaults to None. + category (discord.CategoryChannel, optional): Category to place the channel in. Defaults to None. + permissions_user_post (discord.User, optional): User to grant posting permissions. Defaults to None. Returns: - discord.TextChannel: The created or updated text channel. + discord.TextChannel: The newly created or updated text channel. """ # Fetch roles player_role = discord.utils.get(self.guild.roles, name="Player") @@ -413,7 +450,13 @@ async def channel_update_or_add( class Valentina(commands.Bot): - """Subclass discord.Bot.""" + """Extend the discord.Bot class to create a custom bot implementation. + + Enhance the base discord.Bot with additional functionality + specific to the Valentina bot. Include custom attributes, methods, + and event handlers to manage bot state, load cogs, initialize the database, + and handle server connections. + """ def __init__(self, parent_dir: Path, version: str, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) @@ -433,7 +476,12 @@ def __init__(self, parent_dir: Path, version: str, *args: Any, **kwargs: Any): logger.debug(f"COGS: Loaded {len(self.cogs)} cogs") async def on_connect(self) -> None: - """Perform early setup.""" + """Perform early setup tasks when the bot connects to Discord. + + Initialize the MongoDB database connection, retrying if necessary. + Log connection details and bot information upon successful connection. + Synchronize commands with Discord. + """ # Initialize the mongodb database while True: try: @@ -458,7 +506,14 @@ async def on_connect(self) -> None: logger.info("CONNECT: Commands synced") async def post_changelog_to_guild(self, guild: discord.Guild) -> None: - """Update the changelog.""" + """Post the latest changelog updates to the specified guild. + + Retrieve the most recent version from global properties and compare it with the + guild's last posted changelog version. If updates are available, fetch the + changelog, post it to the designated channel, and update the guild's changelog + version in the database. Handle potential errors during the process and log + relevant information. + """ db_global_properties = await GlobalProperty.find_one() # Post Changelog to the #changelog channel, if set @@ -502,7 +557,13 @@ async def post_changelog_to_guild(self, guild: discord.Guild) -> None: @staticmethod async def _provision_guild(guild: discord.Guild) -> None: - """Provision a guild on connect.""" + """Provision a guild upon connection to Discord. + + Set up the necessary database entries, roles, and configurations for a newly + connected guild. Update existing guild information if already present. Process + guild members, ensuring their information is current in the database. Perform + any required data migrations or updates for existing campaigns. + """ logger.info(f"CONNECT: Provision {guild.name} ({guild.id})") # Add/Update the guild in the database @@ -557,7 +618,17 @@ async def _provision_guild(guild: discord.Guild) -> None: logger.info(f"CONNECT: Playing on {guild.name} ({guild.id})") async def on_ready(self) -> None: - """Override on_ready. Additional functionality is in the on_ready listener in event_listener.py.""" + """Override the on_ready method to initialize essential bot tasks. + + Perform core setup operations when the bot becomes ready. Wait for full + connection, set the bot's presence, initialize the database, and provision + connected guilds. Set the start time for uptime calculations and manage + version tracking in the database. Initiate the web server if enabled in + the configuration. + + Additional functionality is implemented in the on_ready listener within + event_listener.py. + """ await self.wait_until_ready() while not self.connected: logger.warning("CONNECT: Waiting for connection...") @@ -604,5 +675,17 @@ async def on_ready(self) -> None: async def get_application_context( # type: ignore self, interaction: discord.Interaction, cls=ValentinaContext ) -> discord.ApplicationContext: - """Override the get_application_context method to use my custom context.""" + """Override the get_application_context method to use a custom context. + + Return a ValentinaContext instance instead of the default ApplicationContext. + This allows for custom functionality and attributes specific to the Valentina + bot to be available in all command interactions. + + Args: + interaction (discord.Interaction): The interaction object from Discord. + cls (Type[ValentinaContext], optional): The context class to use. Defaults to ValentinaContext. + + Returns: + ValentinaContext: A custom application context for Valentina bot interactions. + """ return await super().get_application_context(interaction, cls=cls) diff --git a/src/valentina/models/campaign.py b/src/valentina/models/campaign.py index 20a8d5aa..1916e509 100644 --- a/src/valentina/models/campaign.py +++ b/src/valentina/models/campaign.py @@ -299,11 +299,18 @@ async def fetch_campaign_category_channels( ) -> tuple[discord.CategoryChannel, list[discord.TextChannel]]: """Fetch the campaign category channels in the guild. + Retrieve the category channel and its associated text channels for the current campaign + from the guild. Iterate through all categories in the guild to find the one matching + the campaign's category ID. + Args: ctx (ValentinaContext): The context object containing guild information. Returns: - tuple[discord.CategoryChannel, list[discord.TextChannel]]: A tuple containing the campaign category channel and a list of text channels within that category. If the category is not found, returns (None, []). + tuple[discord.CategoryChannel, list[discord.TextChannel]]: A tuple containing: + - The campaign category channel (discord.CategoryChannel or None if not found) + - A list of text channels within that category (empty list if category not found) + """ for category, channels in ctx.guild.by_category(): if category and category.id == self.channel_campaign_category: @@ -315,14 +322,14 @@ async def fetch_campaign_category_channels( def _custom_channel_sort(channel: discord.TextChannel) -> tuple[int, str]: # pragma: no cover """Generate a custom sorting key for campaign channels. - This method prioritizes channels based on their channel names. + Prioritize channels based on their names, assigning a numeric value + for sorting order. Args: - channel (discord.TextChannel): The channel to generate the sort key for. + channel (discord.TextChannel): The Discord text channel to generate the sort key for. Returns: - tuple[int, str]: A tuple indicating the sort priority and the channel name. - + tuple[int, str]: A tuple containing the sort priority (int) and the channel name (str). """ if channel.name.startswith(Emoji.SPARKLES.value): return (0, channel.name) @@ -342,10 +349,11 @@ def _custom_channel_sort(channel: discord.TextChannel) -> tuple[int, str]: # pr return (5, channel.name) async def create_channels(self, ctx: "ValentinaContext") -> None: # pragma: no cover - """Create and organize the campaign channels in the guild. + """Create and organize campaign channels in the guild. - This method ensures the campaign category and its channels are correctly created and named. - If the campaign category already exists, it renames it if necessary. Then, it creates the necessary channels for books and characters, confirming their existence and respecting the rate limits. + Create a campaign category if it doesn't exist, or rename it if necessary. + Ensure all required channels (books, characters, etc.) are created and properly named. + Respect Discord rate limits during channel creation and modification. Args: ctx (ValentinaContext): The context object containing guild information. @@ -396,10 +404,11 @@ async def create_channels(self, ctx: "ValentinaContext") -> None: # pragma: no logger.info(f"All channels confirmed for campaign '{self.name}' in '{ctx.guild.name}'") async def delete_channels(self, ctx: "ValentinaContext") -> None: # pragma: no cover - """Delete the channels associated with the campaign. + """Delete all channels associated with the campaign. - This method removes all channels related to the campaign, including book channels, - character channels, storyteller channel, general channel, and the campaign category channel. + Remove book channels, character channels, storyteller channel, general channel, + and the campaign category channel. Update the campaign object to reflect the + deleted channels. Args: ctx (ValentinaContext): The context object containing guild information. @@ -439,7 +448,8 @@ async def delete_channels(self, ctx: "ValentinaContext") -> None: # pragma: no async def fetch_characters(self) -> list[Character]: """Fetch all player characters in the campaign. - This method retrieves a list of all player characters associated with the campaign. + Retrieve a list of all player characters associated with the current campaign. + Filter characters to include only those marked as player characters. Returns: list[Character]: A list of Character objects representing player characters in the campaign. diff --git a/src/valentina/models/changelog.py b/src/valentina/models/changelog.py index 51a71101..49776dbc 100644 --- a/src/valentina/models/changelog.py +++ b/src/valentina/models/changelog.py @@ -59,10 +59,19 @@ def __init__( self.posted = False # Flag to indicate if the changelog has been posted async def _get_channel_from_ctx(self) -> discord.TextChannel | None: - """Get the changelog channel from the guild settings. + """Retrieve the changelog channel from the guild settings. + + Fetch the guild object using the context's guild ID and use it to obtain + the designated changelog channel. This method relies on the existence of + a valid context (self.ctx) to function properly. Returns: - discord.TextChannel | None: The changelog channel if it exists, None otherwise. + discord.TextChannel | None: The designated changelog channel if it exists + and can be fetched, None otherwise. + + Note: + This method assumes that the Guild model has a method called + 'fetch_changelog_channel' which returns the appropriate channel object. """ if self.ctx: guild = await Guild.get(self.ctx.guild.id) @@ -71,10 +80,16 @@ async def _get_channel_from_ctx(self) -> discord.TextChannel | None: return None async def _validate_channel(self) -> bool: - """Validate that the channel exists. + """Validate the existence of the changelog channel. + + Verify that a valid channel for posting the changelog exists. If no channel + is set, attempt to retrieve it from the guild settings using the context. Returns: - bool: True if the channel exists, False otherwise. + bool: True if a valid changelog channel exists, False otherwise. + + Note: + This method may update the `self.channel` attribute if it's not already set. """ if not self.channel: self.channel = await self._get_channel_from_ctx() @@ -97,7 +112,22 @@ async def _validate_channel(self) -> bool: return True def _validate_versions(self, oldest_version: str, newest_version: str) -> tuple[str, str]: - """Validate the input to the ChangelogPoster.""" + """Validate the version inputs for the ChangelogPoster. + + Compare the oldest and newest version strings to ensure they are in the correct order. + Check if the provided versions exist in the changelog. + + Args: + oldest_version (str): The oldest version to include in the changelog. + newest_version (str): The newest version to include in the changelog. + + Returns: + tuple[str, str]: A tuple containing the validated oldest and newest versions. + + Raises: + commands.BadArgument: If the oldest version is newer than the newest version. + errors.VersionNotFoundError: If neither version is found in the changelog. + """ if oldest_version and newest_version and semver.compare(oldest_version, newest_version) > 0: msg = ( f"Oldest version `{oldest_version}` is newer than newest version `{newest_version}`" @@ -122,7 +152,19 @@ def has_updates(self) -> bool: return self.changelog.has_updates() async def post(self) -> None: - """Post the changelog to the specified channel.""" + """Post the changelog to the specified Discord channel. + + Validate the channel, check for updates, and send the changelog as an embed. + If no updates are found, log the information or respond to the context + with an error message. Use personality in the embed if specified. + + Raises: + discord.errors.Forbidden: If the bot lacks permissions to send messages. + discord.errors.HTTPException: If sending the message fails. + + Note: + This method sets the 'posted' attribute to True upon successful posting. + """ if not await self._validate_channel(): return @@ -153,7 +195,13 @@ async def post(self) -> None: class ChangelogParser: - """Helper class for parsing changelogs.""" + """Parse and process changelog files. + + Provide methods to read, interpret, and manipulate changelog entries. + Handle version comparisons, category filtering, and data extraction + from structured changelog files. Support various output formats + for changelog information, including Discord embeds. + """ def __init__( self, @@ -197,11 +245,33 @@ def __init__( @staticmethod def __check_version_schema(version: str) -> bool: - """Check if the version string is in the correct format.""" + """Check if the version string follows the correct format. + + Validate that the given version string adheres to the semantic versioning + format (MAJOR.MINOR.PATCH). Use a regular expression to ensure the string + contains three groups of digits separated by periods. + + Args: + version (str): The version string to be checked. + + Returns: + bool: True if the version string is valid, False otherwise. + """ return bool(re.match(r"^(\d+\.\d+\.\d+)$", version)) def __get_changelog(self) -> str: - """Get the changelog from the file.""" + """Read and return the contents of the changelog file. + + Attempt to read the changelog file from the specified path. If the file + exists, return its contents as a string. If the file is not found, log + an error and raise a FileNotFoundError. + + Returns: + str: The contents of the changelog file. + + Raises: + FileNotFoundError: If the changelog file does not exist at the specified path. + """ if not self.path.exists(): logger.error(f"Changelog file not found at {self.path}") raise FileNotFoundError @@ -209,13 +279,20 @@ def __get_changelog(self) -> str: return self.path.read_text() def __parse_changelog(self) -> dict[str, dict[str, str | list[str]]]: # noqa: C901 - """Parse the changelog into a dictionary. + """Parse the changelog into a structured dictionary. - Loop through each line in the changelog, identifying the version and category of each entry. - Store these in a nested dictionary, keyed first by the version and then by the category. + Iterate through each line of the changelog, identifying version numbers, dates, and categories. + Construct a nested dictionary where: + - The outer key is the version number + - The inner keys are 'date' and category names + - The values are either the release date (string) or lists of changelog entries + + Respect version boundaries set by oldest_version and newest_version attributes. + Skip parsing for versions outside these boundaries or when exclude_oldest_version is True. Returns: - Dict[str, Dict[str, List[str]]]: A nested dictionary containing all changelog information. + dict[str, dict[str, str | list[str]]]: A nested dictionary containing structured changelog information. + Format: {version: {'date': date_string, category1: [entries], category2: [entries], ...}} """ # Prepare compiled regular expressions version_re = re.compile(r"## v(\d+\.\d+\.\d+)") @@ -287,11 +364,14 @@ def __parse_changelog(self) -> dict[str, dict[str, str | list[str]]]: # noqa: C def __clean_changelog(self) -> None: """Clean up the changelog dictionary by removing excluded categories and empty versions. - This function modifies `self.changelog_dict` to remove any versions that have only one item - (i.e., only a date and no other changes). It also removes categories that are in the exclusion list. + Remove categories listed in the exclusion list from all versions in the changelog. + Delete any versions that contain only a date entry and no other changes. + Modify the `self.changelog_dict` in-place to reflect these changes. + + This method ensures that the changelog contains only relevant and non-empty entries. Returns: - None + None """ # Remove excluded categories categories_to_remove: dict[str, list[str]] = { @@ -311,26 +391,53 @@ def __clean_changelog(self) -> None: self.changelog_dict.pop(key) def has_updates(self) -> bool: - """Check if there are any meaningful updates in the changelog other than the date. + """Check for meaningful updates in the changelog. - This function modifies `self.changelog_dict` to remove any versions that have only one item (i.e., only a date and no other changes). + Determine if the changelog contains any significant updates beyond just date changes. + Remove versions from `self.changelog_dict` that only contain a date entry and no other changes. + This modification ensures that only versions with actual content are considered. Returns: - bool: True if there are meaningful updates, False otherwise. + bool: True if meaningful updates exist, False if the changelog is empty or contains only date changes. + + Note: + This method modifies the `self.changelog_dict` in-place by removing versions without substantial changes. """ # Return False if the dictionary is empty; True otherwise return bool(self.changelog_dict) def list_of_versions(self) -> list[str]: - """Get a list of all versions in the changelog. + """Return a sorted list of all versions in the changelog. + + This method retrieves all version keys from the changelog dictionary, + sorts them using semantic versioning rules, and returns them in + descending order (latest version first). Returns: - list[str]: A list of all versions in the changelog. + list[str]: A sorted list of all version strings in the changelog, + ordered from newest to oldest. + + Note: + The sorting is performed using the semver.Version.parse method, + ensuring proper semantic versioning order. """ return sorted(self.changelog_dict.keys(), key=semver.Version.parse, reverse=True) def get_text(self) -> str: - """Generate a text version of the changelog.""" + """Generate a text version of the changelog. + + Create a formatted string representation of the changelog, including version numbers, + dates, categories, and individual entries. Organize the content hierarchically with + version headers, category subheaders, and bulleted entries. Append a link to the full + changelog on GitHub at the end of the text. + + Returns: + str: A formatted string containing the entire changelog text. + + Note: + This method iterates through the `self.changelog_dict` to construct the text, + skipping the 'date' key when processing categories. + """ description = "" # Loop through each version in the changelog @@ -357,10 +464,16 @@ def get_text(self) -> str: return description def get_embed(self) -> discord.Embed: - """Generate an embed for the changelog. + """Generate a Discord embed containing the changelog information. + + Create a Discord embed object that includes the full changelog text. + Set the embed's description to include a header and the formatted + changelog content. If a bot instance is available, set the embed's + thumbnail to the bot's avatar. Returns: - discord.Embed: The changelog embed. + discord.Embed: An embed object containing the formatted changelog + information, ready to be sent in a Discord message. """ description = "" @@ -377,10 +490,21 @@ def get_embed(self) -> discord.Embed: return embed def get_embed_personality(self) -> discord.Embed: # pragma: no cover - """Generate an embed for the changelog. + """Generate a Discord embed for the changelog with a personalized touch. + + Create a Discord embed that presents the changelog information in a + more engaging and character-specific manner. This method adds a + randomized, personality-driven introduction to the changelog content. + + The embed includes: + - A randomized introductory sentence describing Valentina's update. + - The full changelog text. + - A note about viewing specific versions. + - The bot's avatar as the thumbnail (if available). Returns: - discord.Embed: The changelog embed. + discord.Embed: A Discord embed object containing the personalized + changelog information, ready to be sent as a message. """ # Create and populate the embed description 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)}, has {random.choice(['been granted new powers', 'leveled up', 'spent experience points', 'gained new abilities', 'been bitten by a radioactive spider', 'spent willpower points', 'been updated', 'squashed bugs and gained new features',])}!\n" diff --git a/src/valentina/models/character.py b/src/valentina/models/character.py index b011a948..b5ccef14 100644 --- a/src/valentina/models/character.py +++ b/src/valentina/models/character.py @@ -40,14 +40,38 @@ class CharacterSheetSection(BaseModel): - """Represents a character sheet section as a subdocument within Character.""" + """Represent a character sheet section as a subdocument within Character. + + Use this class to define and manage individual sections of a character sheet. + Each section includes a title and content, allowing for structured organization + of character information. Implement this class as an embedded document within + the Character model to maintain a clear hierarchy of character data. + + Attributes: + title (str): The heading or name of the character sheet section. + content (str): The detailed information or text content of the section. + + Note: + Ensure proper integration with the parent Character document for seamless + data management and retrieval. + """ title: str content: str class CharacterTrait(Document): - """Represents a character trait value as a subdocument within Character.""" + """Represent a character trait value as a subdocument within Character. + + Use this class to define and manage individual traits for a character. + Each trait includes properties such as category, name, value, and maximum value. + Implement methods to handle trait-specific operations and provide convenient + access to trait information. + + Note: + - This class is designed to be embedded within the Character document. + - Ensure proper indexing of the 'character' field for efficient querying. + """ category_name: str # TraitCategory enum name character: Indexed(str) # type: ignore [valid-type] @@ -69,7 +93,14 @@ def category(self) -> TraitCategory: class InventoryItem(Document): - """Represents an item in a character's inventory.""" + """Represent an item in a character's inventory. + + Use this class to create, manage, and store information about items + that characters possess. Each item includes details such as its name, + description, and type. Ensure that the 'character' field is properly + indexed for efficient querying and retrieval of inventory items + associated with specific characters. + """ character: Indexed(str) # type: ignore [valid-type] name: str @@ -78,7 +109,17 @@ class InventoryItem(Document): class Character(Document): - """Represents a character in the database.""" + """Represent a character in the database. + + This class defines the structure and properties of a character entity. + It includes fields for basic information, traits, inventory, notes, + and various character-specific attributes. Use this class to create, + retrieve, update, and manage character data in the database. + + The Character model supports different character types (e.g., player, + storyteller, debug) and game-specific attributes (e.g., clan, breed, + auspice) for various role-playing game systems. + """ char_class_name: str # CharClass enum name date_created: datetime = Field(default_factory=time_now) @@ -187,7 +228,7 @@ def creed(self) -> HunterCreed: async def add_image(self, extension: str, data: bytes) -> str: # pragma: no cover """Add an image to a character and upload it to Amazon S3. - This function generates a unique key for the image, uploads the image to S3, and updates the character in the database to include the new image. + Generate a unique key for the image, uploads the image to S3, and updates the character in the database to include the new image. Args: extension (str): The file extension of the image. @@ -225,7 +266,26 @@ async def add_trait( display_on_sheet: bool = True, is_custom: bool = True, ) -> "CharacterTrait": - """Create a new trait.""" + """Create a new trait for the character. + + Add a new trait to the character's list of traits. Check if the trait already exists, + determine if it's a custom trait, and set the appropriate maximum value. Save the new + trait to the database and update the character's trait list. + + Args: + category (TraitCategory): The category of the trait. + name (str): The name of the trait. + value (int): The initial value of the trait. + max_value (int | None, optional): The maximum value for the trait. Defaults to None. + display_on_sheet (bool, optional): Whether to display the trait on the character sheet. Defaults to True. + is_custom (bool, optional): Whether the trait is custom. Defaults to True. + + Returns: + CharacterTrait: The newly created trait object. + + Raises: + errors.TraitExistsError: If a trait with the same name and category already exists for the character. + """ # Check if the trait already exists for trait in cast(list[CharacterTrait], self.traits): if trait.name == name and trait.category_name == category.name.upper(): @@ -261,15 +321,22 @@ async def associate_with_campaign( # pragma: no cover ) -> bool: """Associate a character with a campaign. - This method associates the character with the specified campaign, updates the database, - confirms the character's channel, and sorts the campaign channels. + Associate the character with the specified campaign, update the database, + confirm the character's channel, and sort the campaign channels. If the + character is already associated with the given campaign, no action is taken. Args: - ctx (ValentinaContext): The context object containing guild information. - new_campaign (Campaign): The new campaign to associate with the character. + ctx (ValentinaContext): The context object containing guild information + and other relevant data for the operation. + new_campaign (Campaign): The campaign to associate with the character. Returns: - bool: True if the character was successfully associated with the new campaign, False if already associated. + bool: True if the character was successfully associated with the new + campaign, False if the character was already associated with the + campaign. + + Raises: + None, but may propagate exceptions from called methods. """ if self.campaign == str(new_campaign.id): logger.debug(f"Character {self.name} is already associated with {new_campaign.name}") @@ -285,16 +352,26 @@ async def associate_with_campaign( # pragma: no cover async def confirm_channel( self, ctx: "ValentinaContext", campaign: Optional["Campaign"] ) -> discord.TextChannel | None: - """Confirm or create the channel for the character within the campaign. + """Confirm or create the character's channel within the campaign. + + Ensure the character's channel exists within the campaign's category. Update the channel + information in the database if necessary. Rename the channel if it has the wrong name. + Create a new channel if it doesn't exist. - This method ensures the character's channel exists within the campaign's category. It updates the channel information in the database if necessary, renames it if it has the wrong name, or creates a new one if it doesn't exist. + Follow these steps: + 1. Fetch the campaign if not provided. + 2. Retrieve the campaign's category and channels. + 3. Check if the channel name or ID exists in the category. + 4. Update the database with the channel ID if the name exists but ID is missing. + 5. Rename the channel if it exists with the wrong name. + 6. Create a new channel if it doesn't exist. Args: - ctx (ValentinaContext): The context object containing guild information. - campaign (Optional[Campaign]): The campaign object. If not provided, it will be fetched using the character's campaign ID. + ctx (ValentinaContext): The context object containing guild information and bot instance. + campaign (Optional[Campaign]): The campaign object. If not provided, fetch it using the character's campaign ID. Returns: - discord.TextChannel | None: The channel object if found or created, otherwise None. + discord.TextChannel | None: The channel object if found or created, None if the campaign category doesn't exist. """ campaign = campaign or await Campaign.get(self.campaign) if not campaign: @@ -352,12 +429,15 @@ async def confirm_channel( return discord.utils.get(channels, name=self.channel_name) async def delete_channel(self, ctx: "ValentinaContext") -> None: # pragma: no cover - """Delete the channel associated with the character. + """Delete the channel associated with the character and update the character's information. - This method removes the channel linked to the character from the guild and updates the character's channel information. + Remove the Discord channel linked to this character from the guild. + Update the character's channel information to reflect the deletion. + If no channel is associated or the channel doesn't exist, do nothing. Args: - ctx (ValentinaContext): The context object containing guild information. + ctx (ValentinaContext): The context object containing guild information + and other relevant data for the operation. Returns: None @@ -377,11 +457,12 @@ async def delete_channel(self, ctx: "ValentinaContext") -> None: # pragma: no c async def delete_image(self, key: str) -> None: # pragma: no cover """Delete a character's image from both the character data and Amazon S3. - This method updates the character's data to remove the image reference - and also deletes the actual image stored in Amazon S3. + Remove the specified image key from the character's data and delete the + corresponding image file from Amazon S3 storage. This method ensures + that both the database reference and the actual image file are removed. Args: - key (str): The key representing the image to be deleted. + key (str): The unique identifier of the image to be deleted. Returns: None @@ -409,16 +490,24 @@ async def fetch_trait_by_name(self, name: str) -> Union["CharacterTrait", None]: async def update_channel_permissions( self, ctx: "ValentinaContext", campaign: "Campaign" ) -> discord.TextChannel | None: # pragma: no cover - """Update the permissions for the character's channel. + """Update the permissions and settings for the character's Discord channel. - This method updates the permissions for a character's channel, renames it, and sets the appropriate category and topic. Run this method after updating the character's user_owner. + Update the permissions, name, category, and topic for a character's Discord channel. + This method should be called after updating the character's user_owner. + + Perform the following actions: + 1. Retrieve the channel using the stored channel ID. + 2. Generate a new channel name based on the character's name. + 3. Fetch the user object for the character's owner. + 4. Locate the appropriate category for the campaign. + 5. Update the channel's name, category, permissions, and topic. Args: - ctx (ValentinaContext): The context object containing guild information. + ctx (ValentinaContext): The context object containing guild and bot information. campaign (Campaign): The campaign object to which the character belongs. Returns: - discord.TextChannel | None: The updated channel object, or None if the channel does not exist. + discord.TextChannel | None: The updated channel object if successful, or None if the channel does not exist. """ if not self.channel: return None @@ -441,13 +530,19 @@ async def update_channel_permissions( ) def sheet_section_top_items(self) -> dict[str, str]: - """Return the items to populate the top portion of a character sheet. + """Generate a dictionary of key attributes for the top section of a character sheet. + + Compile a dictionary containing essential character attributes for display + in the top portion of a character sheet. Include attributes such as class, + concept, demeanor, nature, and class-specific traits (e.g., clan for vampires, + auspice for werewolves). Omit attributes that are not applicable or not set. - Populate a dictionary with attributes that are present in the character - and return the dictionary with properly titled values. + Format attribute names as properly titled keys and ensure all values are + strings, using '-' for missing or inapplicable attributes. Returns: - dict[str, str]: Dictionary with character attributes. + dict[str, str]: A dictionary of character attributes, where keys are + attribute names and values are their corresponding string representations. """ attributes = [ ("Class", self.char_class_name if self.char_class_name else "-"), diff --git a/src/valentina/models/database.py b/src/valentina/models/database.py index 90c40803..4c8d7b3c 100644 --- a/src/valentina/models/database.py +++ b/src/valentina/models/database.py @@ -18,19 +18,40 @@ class GlobalProperty(Document): - """Represents global properties in the database.""" + """Represent global properties in the database. + + Use this class to store and manage global configuration settings and properties + that are applicable across the entire application. These properties may include + version information, system-wide settings, or any other data that needs to be + globally accessible. + """ versions: list[str] = Field(default_factory=list) last_update: datetime = Field(default_factory=time_now) @before_event(Insert, Replace, Save, Update, SaveChanges) async def update_last_update(self) -> None: - """Update the last_update field.""" + """Update the last_update field with the current timestamp. + + This method is automatically called before insert, replace, save, update, + and save changes events. It ensures that the last_update field always + reflects the most recent modification time of the GlobalProperty document. + + The time_now() function is used to get the current timestamp in a + consistent format across the application. + """ self.last_update = time_now() @property def most_recent_version(self) -> str: - """Return the most recent version.""" + """Return the most recent version from the list of versions. + + Determine and return the highest semantic version from the versions list. + If the list is empty, return '0.0.0' as the default version. + + Returns: + str: The most recent version string. + """ if len(self.versions) == 0: return "0.0.0" diff --git a/src/valentina/models/dicerolls.py b/src/valentina/models/dicerolls.py index bcf7d1d3..3e62a388 100644 --- a/src/valentina/models/dicerolls.py +++ b/src/valentina/models/dicerolls.py @@ -17,42 +17,28 @@ class DiceRoll: - """A container class that determines the result of a roll and logs dicerolls to the database. - - Dice rolling mechanics are based on our unique system, which is loosely based on the Storyteller system. The following rules apply only to throws of 10 sided dice. - - * A roll is made up of a pool of dice, which is the total number of dice rolled. - * The difficulty is the number that must be rolled on the dice to count as a success. - * The dice type is the type of dice rolled. (Default is a d10.) - * Ones negate two successes - * Tens count as two successes - * Ones and tens cancel each other out - * A botch occurs when the result of all dice is less than 0 - * A critical occurs when the roll is a success and the number of 10s rolled is greater than half the pool - * A failure occurs when the number of dice rolled above the difficulty is zero after accounting for cancelling ones and tens - * A success occurs when the number of dice rolled above the difficulty is greater than zero after accounting for cancelling ones and tens - * The result of a roll is the number of successes after accounting for botches and cancelling ones and tens + """Represent a dice roll and its results. + + This class encapsulates the logic for performing a dice roll, including + handling different pool sizes, difficulties, and dice types. It also + manages the recording of roll statistics and provides methods for + interpreting and presenting roll results. + + Use this class to: + - Create and execute dice rolls with various parameters + - Calculate and interpret roll results + - Log roll statistics for characters, campaigns, and guilds + - Generate formatted output for displaying roll results Attributes: - botches (int): The number of ones in the dice. - criticals (int): The number of rolled criticals (Highest number on dice). - desperation_roll (list[int]): A list of the result all rolled desperation dice. - desperation_botches (int): The number of ones in the desperation dice. - dice_type (DiceType): The type of dice to roll. - difficulty (int): The difficulty of the roll. - embed_color (int): The color of the embed. - failures (int): The number of unsuccessful dice not including botches. - is_botch (bool): Whether the roll is a botch. - 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. - 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. - roll (list[int]): A list of the result all rolled dice. - successes (int): The number of successful dice not including criticals. + ctx (Optional[ValentinaContext]): The context of the command, if available + character (Optional[Character]): The character associated with the roll + desperation_pool (int): Number of dice in the desperation pool + campaign (Optional[Campaign]): The campaign associated with the roll + guild_id (Optional[int]): The ID of the guild where the roll was made + author_id (Optional[int]): The ID of the user who made the roll + author_name (Optional[str]): The name of the user who made the roll + dice_type (DiceType): The type of dice used for the roll """ def __init__( @@ -68,20 +54,7 @@ def __init__( author_id: int | None = None, author_name: str | None = None, ) -> None: - """A container class that determines the result of a roll. - - Args: - 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 - """ + """A container class that determines the result of a roll.""" self.ctx = ctx self.character = character self.desperation_pool = desperation_pool @@ -131,7 +104,21 @@ def __init__( self._desperation_dice_as_emoji_images: str = None def _calculate_result(self) -> RollResultType: - """Calculate the result type of the roll.""" + """Calculate and return the result type of the roll. + + Determine the outcome of the dice roll based on the dice type, result value, + and pool size. Use the following criteria: + + - For non-d10 dice: Always return RollResultType.OTHER. + - For d10 dice: + - If result < 0: Return RollResultType.BOTCH. + - If result == 0: Return RollResultType.FAILURE. + - If result > pool: Return RollResultType.CRITICAL. + - Otherwise: Return RollResultType.SUCCESS. + + Returns: + RollResultType: The calculated result type of the roll. + """ if self.dice_type != DiceType.D10: return RollResultType.OTHER @@ -147,10 +134,21 @@ def _calculate_result(self) -> RollResultType: return RollResultType.SUCCESS async def log_roll(self, traits: list[str] = []) -> None: - """Log the roll to the database. + """Log the roll to the database for statistical analysis. + + Record the details of the current dice roll in the database, including + information about the roll result, character, and associated traits. + This method is crucial for maintaining historical data and generating + roll statistics. Args: - traits (list[str], optional): The traits to log the roll for. Defaults to []. + traits (list[str], optional): A list of trait names associated with + the roll. Use this to track which character traits were involved + in the roll. Defaults to an empty list. + + Note: + This method only logs d10 rolls to the database. Other dice types + are not recorded for statistical purposes. """ # Ensure the user in the database to avoid foreign key errors @@ -174,7 +172,19 @@ async def log_roll(self, traits: list[str] = []) -> None: @property def result_type(self) -> RollResultType: - """Retrieve the result type of the roll.""" + """Determine and return the result type of the roll. + + Calculate the result type based on the roll outcome if not already determined. + The result type is cached to avoid recalculation on subsequent accesses. + + Returns: + RollResultType: An enum representing the outcome of the roll + (e.g., BOTCH, FAILURE, SUCCESS, CRITICAL). + + Note: + This property uses lazy evaluation, calculating the result type + only when first accessed and storing it for future use. + """ if not self._result_type: self._result_type = self._calculate_result() @@ -182,7 +192,19 @@ def result_type(self) -> RollResultType: @property def roll(self) -> list[int]: - """Roll the dice and return the results.""" + """Roll the dice and return the results. + + Generate random numbers for each die in the pool and store them. + If the roll has not been performed yet, execute it and cache the results. + Return the list of rolled values. + + Returns: + list[int]: A list of integers representing the results of the dice roll. + + Note: + This property uses lazy evaluation, performing the roll only when + first accessed and storing the results for future use. + """ if not self._roll: self._roll = [int(random_num(self.dice_type.value)) for x in range(self.pool)] @@ -190,7 +212,19 @@ def roll(self) -> list[int]: @property def desperation_roll(self) -> list[int]: - """Roll the desperation dice and return the results.""" + """Roll the desperation dice and return the results. + + Generate random numbers for each die in the desperation pool and store them. + If the desperation roll has not been performed yet, execute it and cache the results. + Return the list of rolled values for the desperation dice. + + Returns: + list[int]: A list of integers representing the results of the desperation dice roll. + + Note: + This property uses lazy evaluation, performing the roll only when + first accessed and storing the results for future use. + """ if not self._desperation_roll: self._desperation_roll = [ random_num(self.dice_type.value) for x in range(self.desperation_pool) @@ -200,7 +234,15 @@ def desperation_roll(self) -> list[int]: @property def botches(self) -> int: - """Retrieve the number of ones in the dice.""" + """Calculate and return the number of botches (ones) in the dice roll. + + Count the number of dice that rolled a 1, which are considered botches. + Include both regular and desperation dice in the count if applicable. + Use lazy evaluation to calculate the result only when first accessed. + + Returns: + int: The total number of botches (ones) in the dice roll. + """ if not self._botches: if self.desperation_pool > 0: desperation_botches = self.desperation_roll.count(1) @@ -212,7 +254,18 @@ def botches(self) -> int: @property def desperation_botches(self) -> int: - """Retrieve the number of ones in the desperation dice.""" + """Calculate and return the number of botches in the desperation dice roll. + + Count the number of ones (botches) rolled on the desperation dice. + Use lazy evaluation to calculate the result only when first accessed. + Cache the result for subsequent accesses to improve performance. + + Returns: + int: The number of ones (botches) in the desperation dice roll. + + Note: + This property returns 0 if there is no desperation pool. + """ if not self._desperation_botches and self.desperation_pool > 0: self._desperation_botches = self.desperation_roll.count(1) @@ -220,7 +273,16 @@ def desperation_botches(self) -> int: @property def criticals(self) -> int: - """Retrieve the number of rolled criticals (Highest number on dice).""" + """Calculate and return the number of critical successes in the dice roll. + + Count the number of dice that rolled the highest possible value for the + given dice type, which are considered critical successes. Include both + regular and desperation dice in the count if applicable. Use lazy + evaluation to calculate the result only when first accessed. + + Returns: + int: The total number of critical successes in the dice roll. + """ if not self._criticals: if self.desperation_pool > 0: desperation_criticals = self.desperation_roll.count(self.dice_type.value) @@ -232,7 +294,18 @@ def criticals(self) -> int: @property def failures(self) -> int: - """Retrieve the number of unsuccessful dice not including botches.""" + """Calculate and return the number of failed dice rolls, excluding botches. + + Count the dice rolls that are above 1 (not botches) but below the difficulty threshold. + This property represents unsuccessful attempts that are not critical failures. + + Returns: + int: The number of failed dice rolls, not including botches. + + Note: + This property uses lazy evaluation and caches the result for subsequent accesses. + It includes both regular and desperation dice rolls if applicable. + """ if not self._failures: count = 0 for die in self.roll: @@ -251,7 +324,18 @@ def failures(self) -> int: @property def successes(self) -> int: - """Retrieve the total number of dice which beat the difficulty not including criticals.""" + """Calculate and return the total number of successful dice rolls, excluding criticals. + + Count the dice rolls that meet or exceed the difficulty threshold but are not + critical successes. Include both regular and desperation dice if applicable. + + Returns: + int: The number of successful dice rolls, not including criticals. + + Note: + Use lazy evaluation to calculate the result only when first accessed. + Cache the result for subsequent accesses to improve performance. + """ if not self._successes: count = 0 for die in self.roll: @@ -270,7 +354,17 @@ def successes(self) -> int: @property def result(self) -> int: - """Retrieve the number of successes to count.""" + """Calculate and return the final result of the dice roll. + + Determine the total number of successes to count, considering criticals, + failures, and botches. For d10 rolls, apply special rules for criticals + and botches. Use lazy evaluation to calculate the result only when first + accessed and cache it for subsequent accesses. + + Returns: + int: The final result of the dice roll, representing the total + number of successes to count. + """ if not self._result: if self.dice_type != DiceType.D10: self._result = self.successes + self.criticals - self.failures - self.botches @@ -285,13 +379,33 @@ def result(self) -> int: return self._result async def thumbnail_url(self) -> str: # pragma: no cover - """Determine the thumbnail to use for the Discord embed.""" + """Determine and return the thumbnail URL for the Discord embed. + + Fetch the appropriate thumbnail URL based on the roll result type from the + Guild's custom roll result thumbnails. If no custom thumbnail is set, use + the default thumbnail for the given result type. + + Returns: + str: The URL of the thumbnail image to be used in the Discord embed. + """ guild = await Guild.get(self.guild_id or self.ctx.guild.id) return await guild.fetch_diceroll_thumbnail(self.result_type) @property def embed_color(self) -> int: # pragma: no cover - """Determine the Discord embed color based on the result of the roll.""" + """Determine the Discord embed color based on the result of the roll. + + Return an integer color value corresponding to the roll result type. + Use predefined color mappings for different roll outcomes: + - OTHER: Info color + - BOTCH: Error color + - CRITICAL: Success color + - SUCCESS: Success color + - FAILURE: Warning color + + Returns: + int: The color value to be used in the Discord embed. + """ color_map = { RollResultType.OTHER: EmbedColor.INFO, RollResultType.BOTCH: EmbedColor.ERROR, @@ -303,7 +417,15 @@ def embed_color(self) -> int: # pragma: no cover @property def roll_result_humanized(self) -> str: - """The humanized result of the dice roll. ie - "botch", "2 successes", etc.""" + """Return a human-readable description of the dice roll result. + + Generate a concise, user-friendly string that describes the outcome of the dice roll. + The description varies based on the roll's result type, providing context-appropriate + feedback such as "Botch!", "Critical Success!", or a specific number of successes. + + Returns: + str: A human-readable string describing the roll result, e.g., "Botch!", "2 successes", etc. + """ title_map = { RollResultType.OTHER: "Dice roll", RollResultType.BOTCH: "Botch!", @@ -315,7 +437,16 @@ def roll_result_humanized(self) -> str: @property def num_successes_humanized(self) -> str: - """The number of successes rolled written as `x successess`.""" + """Return the number of successes as a humanized string. + + Generate a string representation of the number of successes rolled, + formatted as 'x successes'. Use proper pluralization for 'success' + based on the number of successes. + + Returns: + str: A human-readable string describing the number of successes, + e.g., '1 success', '2 successes', etc. + """ description_map = { RollResultType.OTHER: "", RollResultType.BOTCH: f"{self.result} {p.plural_noun('Success', self.result)}", @@ -327,7 +458,20 @@ def num_successes_humanized(self) -> str: @property def dice_as_emoji_images(self) -> str: - """Return the rolled dice as emoji images.""" + """Convert the rolled dice values to emoji images and return as a string. + + Generate a string representation of the dice roll results using emoji images. + Sort the dice values in ascending order before conversion. Use the + convert_int_to_emoji function to transform each die value into its + corresponding emoji image. + + Returns: + str: A space-separated string of emoji images representing the rolled dice. + + Note: + This property uses lazy evaluation, generating the emoji string only + when first accessed and caching it for future use. + """ 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) @@ -336,7 +480,20 @@ def dice_as_emoji_images(self) -> str: @property def desperation_dice_as_emoji_images(self) -> str: - """Return the rolled desperation dice as emoji images.""" + """Convert the rolled desperation dice values to emoji images and return as a string. + + Generate a string representation of the desperation dice roll results using emoji images. + Sort the dice values in ascending order before conversion. Use the + convert_int_to_emoji function to transform each die value into its + corresponding emoji image. + + Returns: + str: A space-separated string of emoji images representing the rolled desperation dice. + + Note: + This property uses lazy evaluation, generating the emoji string only + when first accessed and caching it for future use. + """ 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) diff --git a/src/valentina/models/guild.py b/src/valentina/models/guild.py index 3ab39d53..78302f15 100644 --- a/src/valentina/models/guild.py +++ b/src/valentina/models/guild.py @@ -63,7 +63,11 @@ class GuildChannels(BaseModel): class Guild(Document): - """Represents a guild in the database.""" + """Represent a Discord guild in the database. + + This class models a Discord guild (server) and stores relevant information + such as campaigns, channel IDs, permissions, and roll result thumbnails. + """ id: int # type: ignore [assignment] @@ -156,8 +160,11 @@ def fetch_error_log_channel( async def setup_roles(self, guild: discord.Guild) -> None: # pragma: no cover """Create or update the guild's roles. + Create or update the storyteller and player roles for the given guild. + Ensure these roles exist and have the appropriate permissions. + Args: - guild (discord.Guild): The guild to create/update roles for. + guild (discord.Guild): The Discord guild to create or update roles for. """ # Create roles await create_storyteller_role(guild) @@ -165,10 +172,14 @@ async def setup_roles(self, guild: discord.Guild) -> None: # pragma: no cover logger.debug(f"GUILD: Roles created/updated on {self.name}") async def delete_campaign(self, campaign: "Campaign") -> None: - """Delete a campaign from the guild. Remove the campaign from the guild's list of campaigns and delete the campaign from the database. + """Delete a campaign from the guild and mark it as deleted in the database. + + Remove the campaign from the guild's list of campaigns and mark it as deleted + in the database. This method does not permanently delete the campaign data, + but rather sets a flag to indicate its deleted status. Args: - campaign (Campaign): The campaign to delete. + campaign (Campaign): The campaign object to be deleted. """ if campaign in self.campaigns: self.campaigns.remove(campaign) @@ -197,14 +208,16 @@ async def add_roll_result_thumbnail( ) async def fetch_diceroll_thumbnail(self, result: RollResultType) -> str: - """Take a string and return a random gif url. + """Fetch a random thumbnail URL for a given roll result type. + + Retrieve a random thumbnail URL from a combined list of default thumbnails + and guild-specific thumbnails for the specified roll result type. Args: - ctx (): The application context. - result (RollResultType): The roll result type. + result (RollResultType): The roll result type to fetch a thumbnail for. Returns: - Optional[str]: The thumbnail URL, or None if no thumbnail is found. + str | None: A random thumbnail URL if available, or None if no thumbnails are found. """ # Get the list of default thumbnails for the result type thumb_list = DICEROLL_THUMBS.get(result.name, []) diff --git a/src/valentina/models/statistics.py b/src/valentina/models/statistics.py index b8b6ebb1..4ca25a6f 100644 --- a/src/valentina/models/statistics.py +++ b/src/valentina/models/statistics.py @@ -31,7 +31,14 @@ class RollStatistic(Document): class Statistics: - """Compute and display roll statistics.""" + """Compute and display roll statistics for Vampire: The Masquerade. + + This class provides methods to calculate, analyze, and present various + statistics related to dice rolls in the game. Use it to track and + visualize performance metrics such as success rates, critical rolls, + and average difficulty levels across different characters, campaigns, + or entire guilds. + """ def __init__( self, @@ -52,29 +59,54 @@ def __init__( @property def criticals_percentage(self) -> str: - """Return the percentage of critical successes.""" + """Calculate and return the percentage of critical successes. + + Returns: + str: The percentage of critical successes as a string with two decimal places. + Returns "0" if no rolls have been made. + """ 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.""" + """Calculate and return the percentage of successful rolls. + + Returns: + str: The percentage of successful rolls as a string with two decimal places. + Returns "0" if no rolls have been made. + """ 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.""" + """Calculate and return the percentage of failed rolls. + + Returns: + str: The percentage of failed rolls as a string with two decimal places. + Returns "0" if no rolls have been made. + """ 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.""" + """Calculate and return the percentage of botched rolls. + + Returns: + str: The percentage of botched rolls as a string with two decimal places. + Returns "0" if no rolls have been made. + """ 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. + """Generate a dictionary containing all statistics as string values. + + Convert all statistical data to string format and return them in a dictionary. + This method is useful for serialization or when string representation of all + statistics is needed. Returns: - dict: Dictionary with the statistics. + dict[str, str]: A dictionary where keys are statistic names and values + are their corresponding string representations. """ return { "total_rolls": str(self.total_rolls), @@ -91,14 +123,19 @@ def _get_json(self) -> dict[str, str]: } def _get_text(self, with_title: bool = True, with_help: bool = True) -> str: - """Return a string with the statistics. + """Generate a formatted string representation of the statistics. + + Create a string containing all statistics, optionally including a title + and help text. This method is useful for displaying statistics in a + human-readable format. Args: - 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. + with_title (bool, optional): Include the title in the output. Defaults to True. + with_help (bool, optional): Include the help text in the output. Defaults to True. Returns: - str: String with the statistics. + str: A formatted string containing the statistics, and optionally + the title and help text. """ msg = "\n" if with_title: @@ -129,10 +166,20 @@ def _get_text(self, with_title: bool = True, with_help: bool = True) -> str: return msg async def _get_embed(self, with_title: bool = True, with_help: bool = True) -> discord.Embed: - """Return an embed with the statistics. + """Generate and return an embed containing the statistics. + + Create a Discord embed that displays the statistics in a formatted manner. + The embed includes the statistics text, with optional title and help text. + + Args: + with_title (bool): Include the title in the statistics text. Defaults to True. + with_help (bool): Include the help text in the statistics text. Defaults to True. Returns: - discord.Embed: Embed with the statistics. + discord.Embed: An embed object containing the formatted statistics. + + Raises: + errors.NoCTXError: If no context (self.ctx) is provided. """ if not self.ctx: msg = "No context provided." @@ -160,14 +207,23 @@ async def guild_statistics( ) -> discord.Embed | str | dict[str, str]: """Compute and display guild statistics. + Retrieve and process roll statistics for a specific guild, including counts of + different roll results and average difficulty and pool sizes. + 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. + as_embed (bool, optional): Return the statistics as a Discord embed. Defaults to False. + as_json (bool, optional): Return the statistics as a JSON object. Defaults to False. + with_title (bool, optional): Include the title in the output. Defaults to True. + with_help (bool, optional): Include the help text in the output. Defaults to True. Returns: - discord.Embed | str: Embed or string with the statistics. + discord.Embed | str | dict[str, str]: Statistics presented in the specified format. + If as_embed is True, return a Discord embed. + If as_json is True, return a dictionary. + Otherwise, return a formatted string. + + Raises: + errors.NoCTXError: If neither context nor guild ID is provided. """ if not self.ctx and not self.guild_id: msg = "No context or guild ID provided." @@ -237,17 +293,23 @@ async def user_statistics( with_title: bool = True, with_help: bool = True, ) -> discord.Embed | str | dict[str, str]: - """Compute and display user statistics. + """Compute and display roll statistics for a specific user. + + Calculate various roll statistics for the given user, including botches, + successes, criticals, failures, and other roll types. Also compute the + average difficulty and pool size for the user's rolls. 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. + user (discord.Member): The Discord member to compute statistics for. + as_embed (bool, optional): Return the statistics as a Discord embed. Defaults to False. + as_json (bool, optional): Return the statistics as a JSON object. Defaults to False. + with_title (bool, optional): Include a title in the output. Defaults to True. + with_help (bool, optional): Include help text in the output. Defaults to True. Returns: - discord.Embed | str: Embed or string with the statistics. + discord.Embed | str | dict[str, str]: Statistics presented in the specified format. + Returns an Embed if as_embed is True, a JSON object if as_json is True, + or a formatted string otherwise. """ if not as_json: self.title = f"Roll statistics for @{user.display_name}" @@ -306,17 +368,23 @@ async def character_statistics( with_title: bool = True, with_help: bool = True, ) -> discord.Embed | str | dict[str, str]: - """Compute and display character statistics. + """Compute and display statistics for a specific character. + + Retrieve and process roll statistics for the given character from the database. + Calculate various metrics such as botches, successes, criticals, failures, + average difficulty, and average pool size. 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. + character (Character): The character for which to compute statistics. + as_embed (bool, optional): Return the statistics as a Discord embed. Defaults to False. + as_json (bool, optional): Return the statistics as a JSON object. Defaults to False. + with_title (bool, optional): Include a title in the output. Defaults to True. + with_help (bool, optional): Include help text in the output. Defaults to True. Returns: - discord.Embed | str: Embed or string with the statistics. + Union[discord.Embed, str, Dict[str, str]]: Statistics presented in the specified format. + Returns a Discord embed if as_embed is True, a JSON object if as_json is True, + or a formatted string otherwise. """ self.title = f"Roll statistics for {character.name}" @@ -375,17 +443,24 @@ async def campaign_statistics( with_title: bool = True, with_help: bool = True, ) -> discord.Embed | str | dict[str, str]: - """Compute and display character statistics. + """Compute and display campaign statistics. + + Calculate and present various roll statistics for a given campaign, including + the number of botches, successes, criticals, failures, and other rolls. + Also compute average difficulty and pool size for the rolls in the campaign. 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. + campaign (Campaign): The campaign for which to compute statistics. + as_embed (bool, optional): Return the statistics as a Discord embed. Defaults to False. + as_json (bool, optional): Return the statistics as a JSON object. Defaults to False. + with_title (bool, optional): Include a title in the output. Defaults to True. + with_help (bool, optional): Include help text in the output. Defaults to True. Returns: - discord.Embed | str: Embed or string with the statistics. + discord.Embed | str | dict[str, str]: Campaign statistics in the specified format. + If as_embed is True, return a Discord embed. + If as_json is True, return a dictionary. + Otherwise, return a formatted string. """ self.title = f"Roll statistics for {campaign.name}" diff --git a/src/valentina/models/user.py b/src/valentina/models/user.py index bb284cfa..fe7efca5 100644 --- a/src/valentina/models/user.py +++ b/src/valentina/models/user.py @@ -42,7 +42,13 @@ class UserMacro(BaseModel): class User(Document): - """Represents a user in the database.""" + """Represent a user in the database and manage user-related data. + + Use this class to create, retrieve, update, and delete user records in the database. + Store and manage user information such as ID, avatar, characters, campaign experience, + creation date, macros, name, and associated guilds. Implement methods to handle + user-specific operations and provide convenient access to user data. + """ id: int # type: ignore [assignment] @@ -62,7 +68,19 @@ async def update_modified_date(self) -> None: @property def lifetime_experience(self) -> int: - """Return the user's lifetime experience level.""" + """Calculate and return the user's total lifetime experience across all campaigns. + + Sum up the total experience points (XP) accumulated by the user in all campaigns + they have participated in. This method provides a comprehensive view of the user's + overall progression and achievements within the system. + + Returns: + int: The total lifetime experience points of the user. + + Note: + This property aggregates XP from all campaign experiences stored in the + user's campaign_experience dictionary. + """ xp = 0 for obj in self.campaign_experience.values(): @@ -72,7 +90,19 @@ def lifetime_experience(self) -> int: @property def lifetime_cool_points(self) -> int: - """Return the user's lifetime cool points.""" + """Calculate and return the user's total lifetime cool points across all campaigns. + + Sum up the cool points accumulated by the user in all campaigns they have + participated in. This property provides an overview of the user's overall + achievements and recognition within the system. + + Returns: + int: The total lifetime cool points of the user. + + Note: + Aggregate cool points from all campaign experiences stored in the + user's campaign_experience dictionary. + """ cool_points = 0 for obj in self.campaign_experience.values(): @@ -81,13 +111,20 @@ def lifetime_cool_points(self) -> int: return cool_points def _find_campaign_xp(self, campaign: Campaign) -> CampaignExperience | None: - """Return the user's campaign experience for a given campaign. + """Find and return the user's campaign experience for a given campaign. + + Search for the campaign experience associated with the provided campaign + in the user's campaign_experience dictionary. If found, return the + CampaignExperience object. If not found, raise a NoExperienceInCampaignError. Args: campaign (Campaign): The campaign to fetch experience for. Returns: - CampaignExperience|None: The user's campaign experience if it exists; otherwise, None. + CampaignExperience: The user's campaign experience for the specified campaign. + + Raises: + NoExperienceInCampaignError: If no experience is found for the given campaign. """ try: return self.campaign_experience[str(campaign.id)] @@ -95,13 +132,21 @@ def _find_campaign_xp(self, campaign: Campaign) -> CampaignExperience | None: raise errors.NoExperienceInCampaignError from e def fetch_campaign_xp(self, campaign: Campaign) -> tuple[int, int, int]: - """Return the user's campaign experience for a given campaign. + """Fetch and return the user's campaign experience for a given campaign. + + Retrieve the user's experience data for the specified campaign. If the user + has no experience in the campaign, return default values. Args: campaign (Campaign): The campaign to fetch experience for. Returns: - tuple[int, int, int]: Tuple of (current xp, total xp, cool points) if the user has experience for the campaign; otherwise, None. + tuple[int, int, int]: A tuple containing (current XP, total XP, cool points). + If the user has no experience in the campaign, return (0, 0, 0). + + Note: + This method handles the NoExperienceInCampaignError internally and + returns default values instead of raising an exception. """ try: campaign_experience = self._find_campaign_xp(campaign) @@ -115,14 +160,22 @@ def fetch_campaign_xp(self, campaign: Campaign) -> tuple[int, int, int]: ) async def spend_campaign_xp(self, campaign: Campaign, amount: int) -> int: - """Spend experience for a campaign. + """Spend experience points for a specific campaign. + + Deduct the specified amount of experience points from the user's current + experience in the given campaign. Raise an error if there are insufficient + points to spend. Args: - campaign (Campaign): The campaign to spend experience for. - amount (int): The amount of experience to spend. + campaign (Campaign): The campaign for which to spend experience points. + amount (int): The amount of experience points to spend. Returns: - int: The new campaign experience. + int: The new current experience points for the campaign after spending. + + Raises: + errors.NotEnoughExperienceError: If the user doesn't have enough + experience points to spend the specified amount. """ campaign_experience = self._find_campaign_xp(campaign) @@ -138,14 +191,21 @@ async def spend_campaign_xp(self, campaign: Campaign, amount: int) -> int: return new_xp async def add_campaign_xp(self, campaign: Campaign, amount: int) -> int: - """Add experience for a campaign. + """Add experience points to a user's campaign. + + Increase both the current and total experience points for the specified campaign. + If the user has no prior experience in the campaign, create a new CampaignExperience entry. Args: - campaign (Campaign): The campaign to add experience for. - amount (int): The amount of experience to add. + campaign (Campaign): The campaign to add experience points for. + amount (int): The amount of experience points to add. Returns: - int: The new campaign experience. + int: The new current experience points for the campaign after addition. + + Note: + This method handles the NoExperienceInCampaignError internally by creating + a new CampaignExperience entry if needed. """ try: campaign_experience = self._find_campaign_xp(campaign) @@ -160,14 +220,22 @@ async def add_campaign_xp(self, campaign: Campaign, amount: int) -> int: return campaign_experience.xp_current async def add_campaign_cool_points(self, campaign: Campaign, amount: int) -> int: - """Add cool points and increase experience for the current campaign. + """Add cool points and increase experience for the specified campaign. + + Add the given amount of cool points to the user's campaign experience. + Also increase the total and current experience points based on the cool points added. + If the user has no prior experience in the campaign, create a new CampaignExperience entry. Args: campaign (Campaign): The campaign to add cool points for. amount (int): The amount of cool points to add. Returns: - int: The new campaign cool points. + int: The new total of cool points for the campaign after addition. + + Note: + This method handles the NoExperienceInCampaignError internally by creating + a new CampaignExperience entry if needed. """ try: campaign_experience = self._find_campaign_xp(campaign) @@ -183,11 +251,30 @@ async def add_campaign_cool_points(self, campaign: Campaign, amount: int) -> int return campaign_experience.cool_points def all_characters(self, guild: discord.Guild) -> list[Character]: - """Return all characters for the user in the guild.""" + """Retrieve all characters belonging to the user in the specified guild. + + This method filters the user's characters based on the given guild ID. + + Args: + guild (discord.Guild): The Discord guild to filter characters by. + + Returns: + list[Character]: A list of Character objects associated with the user + and the specified guild. + """ return [x for x in cast(list[Character], self.characters) if x.guild == guild.id] async def remove_character(self, character: Character) -> None: - """Remove a character from the user's list of characters.""" + """Remove a character from the user's list of characters. + + Remove the specified character from the user's list of characters and save the updated user data. + + Args: + character (Character): The character to be removed from the user's list. + + Returns: + None + """ # Remove the character from the list of characters self.characters = [x for x in self.characters if x.id != character.id] # type: ignore [attr-defined] diff --git a/src/valentina/utils/__init__.py b/src/valentina/utils/__init__.py index 8f896051..ae8053ab 100644 --- a/src/valentina/utils/__init__.py +++ b/src/valentina/utils/__init__.py @@ -1,12 +1,13 @@ """Utility functions for Valentina.""" -from .config import ValentinaConfig +from .config import ValentinaConfig, debug_environment_variables from .console import console from .helpers import random_num from .logging import InterceptHandler, instantiate_logger __all__ = [ "InterceptHandler", + "debug_environment_variables", "ValentinaConfig", "console", "instantiate_logger", diff --git a/src/valentina/utils/autocomplete.py b/src/valentina/utils/autocomplete.py index d4b7ddf0..e8bf56e0 100644 --- a/src/valentina/utils/autocomplete.py +++ b/src/valentina/utils/autocomplete.py @@ -27,15 +27,16 @@ ################## Character Autocomplete Functions ################## async def select_any_player_character(ctx: discord.AutocompleteContext) -> list[OptionChoice]: - """Generate a list of all type_player characters in the guild for autocomplete. + """Generate a list of all player characters in the guild for autocomplete. - This function fetches all player characters for the guild, filters them based on the user's input, and returns a list of OptionChoice objects to populate the autocomplete list. + Fetch all player characters for the guild, filter them based on the user's input, + and return a list of OptionChoice objects to populate the autocomplete list. Args: ctx (discord.AutocompleteContext): The context object containing interaction and user details. Returns: - list[OptionChoice]: A list of OptionChoice objects for the autocomplete list which contains character names and character ids. + list[OptionChoice]: A list of OptionChoice objects containing character names and IDs. """ # Fetch and prepare player characters all_chars_owners = sorted( @@ -74,15 +75,16 @@ async def select_any_player_character(ctx: discord.AutocompleteContext) -> list[ async def select_campaign_any_player_character( ctx: discord.AutocompleteContext, ) -> list[OptionChoice]: # pragma: no cover - """Generate a list of all type_player characters associated with a specific campaign. + """Generate a list of all player characters associated with a specific campaign. - This function fetches all player characters for the guild, filters them based on the user's input, and returns a list of OptionChoice objects to populate the autocomplete list. + Fetch all player characters for the campaign, filter them based on the user's input, + and return a list of OptionChoice objects to populate the autocomplete list. Args: ctx (discord.AutocompleteContext): The context object containing interaction and user details. Returns: - list[OptionChoice]: A list of OptionChoice objects for the autocomplete list which contains character names and character ids. + list[discord.OptionChoice]: A list of OptionChoice objects containing character names and IDs for the autocomplete list. """ channel_objects = await fetch_channel_object(ctx, raise_error=False) campaign = channel_objects.campaign @@ -127,15 +129,16 @@ async def select_campaign_any_player_character( async def select_campaign_character_from_user( ctx: discord.AutocompleteContext, ) -> list[OptionChoice]: - """Generate a list of the user's available characters associated with a campaign for autocomplete. + """Generate a list of the user's available characters for autocomplete. - This function fetches all alive player characters for the user, filters them based on the user's input, and returns a list of OptionChoice objects to populate the autocomplete list. + Fetch all player characters for the user in the current campaign, filter them based on user input, + and return a list of OptionChoice objects for autocomplete. Args: ctx (discord.AutocompleteContext): The context object containing interaction and user details. Returns: - list[OptionChoice]: A list of OptionChoice objects for the autocomplete list. + list[discord.OptionChoice]: A list of OptionChoice objects for the autocomplete list. """ channel_objects = await fetch_channel_object(ctx, raise_error=False) campaign = channel_objects.campaign @@ -202,7 +205,17 @@ async def select_storyteller_character(ctx: discord.AutocompleteContext) -> list async def select_aws_object_from_guild( ctx: discord.AutocompleteContext, ) -> list[OptionChoice]: # pragma: no cover - """Populate the autocomplete list for the aws_object option based on the user's input.""" + """Populate the autocomplete list for the aws_object option based on the user's input. + + Fetch AWS objects associated with the current guild and generate autocomplete options. + + Args: + ctx (discord.AutocompleteContext): The context object containing interaction details. + + Returns: + list[OptionChoice]: A list of OptionChoice objects representing AWS objects, + limited to MAX_OPTION_LIST_SIZE. + """ aws_svc = AWSService() guild_prefix = f"{ctx.interaction.guild.id}/" @@ -215,7 +228,16 @@ async def select_aws_object_from_guild( async def select_changelog_version_1( ctx: discord.AutocompleteContext, ) -> list[str]: # pragma: no cover - """Populate the autocomplete for the version option. This is for the first of two options.""" + """Populate the autocomplete for the first version option. + + Generate a list of changelog versions that start with the user's input. + + Args: + ctx: The autocomplete context containing the interaction details. + + Returns: + A list of version strings matching the user's input, limited to MAX_OPTION_LIST_SIZE. + """ bot = cast(Valentina, ctx.bot) possible_versions = ChangelogParser(bot).list_of_versions() @@ -227,7 +249,16 @@ async def select_changelog_version_1( async def select_changelog_version_2( ctx: discord.AutocompleteContext, ) -> list[str]: # pragma: no cover - """Populate the autocomplete for the version option. This is for the second of two options.""" + """Populate the autocomplete for the second version option. + + Generate a list of changelog versions that start with the user's input. + + Args: + ctx: The autocomplete context containing the interaction details. + + Returns: + A list of version strings matching the user's input, limited to MAX_OPTION_LIST_SIZE. + """ bot = cast(Valentina, ctx.bot) possible_versions = ChangelogParser(bot).list_of_versions() @@ -237,19 +268,20 @@ async def select_changelog_version_2( async def select_book(ctx: discord.AutocompleteContext) -> list[OptionChoice]: - """Populate the autocomplete for the chapter option. + """Populate the autocomplete for the book option. - This function fetches the active campaign from the bot's campaign service, - fetches all books of that campaign, sorts them by book number, - and filters them based on the starting string of the book name. - If the number of books reaches a maximum size, it stops appending more books. - If there is no active campaign, it returns a list with a single string "No active campaign". + Fetch the active campaign, retrieve all books for that campaign, sort them by book number, + and filter based on the user's input. If no active campaign is found, return a single option + indicating this. If the number of books exceeds the maximum allowed options, return all + matching books up to the limit. Args: - ctx (discord.AutocompleteContext): The context in which the function is called. + ctx (discord.AutocompleteContext): The context of the autocomplete interaction. Returns: - list[OptionChoice]: A list of available chapter names mapped to book database id. + list[OptionChoice]: A list of OptionChoice objects representing available books. + Each option contains the book number and name as the label, and the book's + database ID as the value. """ # Fetch the active campaign channel_objects = await fetch_channel_object(ctx, raise_error=False) @@ -272,11 +304,15 @@ async def select_book(ctx: discord.AutocompleteContext) -> list[OptionChoice]: async def select_campaign(ctx: discord.AutocompleteContext) -> list[OptionChoice]: """Generate a list of available campaigns for the guild. + Fetch and sort non-deleted campaigns for the current guild, then filter based on user input. + If too many options are available, return a single option prompting for more specific input. + Args: - ctx (discord.AutocompleteContext): The context in which the function is called. + ctx (discord.AutocompleteContext): The autocomplete context containing interaction details. Returns: - list[OptionChoice]: A list of available campaign names and db ids. + list[OptionChoice]: A list of OptionChoice objects representing available campaigns. + Each option contains the campaign name as the label and the campaign's database ID as the value. """ all_campaigns = sorted( await Campaign.find( @@ -304,17 +340,16 @@ async def select_campaign(ctx: discord.AutocompleteContext) -> list[OptionChoice async def select_chapter(ctx: discord.AutocompleteContext) -> list[OptionChoice]: """Populate the autocomplete for the chapter option. - This function fetches the active book based on the Discord channel, - fetches all chapters of that book, sorts them by chapter number, - and filters them based on the starting string of the chapter name. - If the number of chapters reaches a maximum size, it stops appending more chapters. - If there is no active campaign, it returns a list with a single string "No active campaign". + Fetch the active book for the current channel, retrieve its chapters, sort them by number, + and filter based on user input. If no active book is found, return a single option indicating this. Args: - ctx (discord.AutocompleteContext): The context in which the function is called. + ctx (discord.AutocompleteContext): The autocomplete context containing interaction details. Returns: - list[OptionChoice]: A list of available chapter names mapped to chapter.number. + list[OptionChoice]: A list of OptionChoice objects representing available chapters. + Each option contains the chapter number and name as the label, and the chapter's + database ID as the value. """ # Fetch the active campaign channel_objects = await fetch_channel_object(ctx, raise_error=False) @@ -336,19 +371,17 @@ async def select_chapter_old( ) -> list[OptionChoice]: # pragma: no cover """Populate the autocomplete for the chapter option. - This function fetches the active campaign from the bot's campaign service, - fetches all chapters of that campaign, sorts them by chapter number, - and filters them based on the starting string of the chapter name. - If the number of chapters reaches a maximum size, it stops appending more chapters. - If there is no active campaign, it returns a list with a single string "No active campaign". - - TODO: Remove this after migration to the new chapter system. + Fetch the active campaign, retrieve its chapters, sort them by number, + and filter based on user input. If no active campaign is found, return a single option indicating this. Args: - ctx (discord.AutocompleteContext): The context in which the function is called. + ctx (discord.AutocompleteContext): The autocomplete context containing interaction details. Returns: - list[OptionChoice]: A list of available chapter names mapped to chapter.number. + list[OptionChoice]: A list of OptionChoice objects representing available chapters. + Each option contains the chapter number and name as the label, and the chapter number as the value. + + TODO: Remove this after migration to the new chapter system. """ # Fetch the active campaign channel_objects = await fetch_channel_object(ctx, raise_error=False) @@ -369,15 +402,14 @@ async def select_chapter_old( async def select_char_class(ctx: discord.AutocompleteContext) -> list[OptionChoice]: """Generate a list of available character classes. - This function fetches the available character classes, sorts them by name, - and filters them based on the starting string of the class name. - If the number of classes reaches a maximum size, it stops appending more classes. + Fetch available character classes, sort them by name, and filter based on user input. Args: - ctx (discord.AutocompleteContext): The context in which the function is called. + ctx (discord.AutocompleteContext): The autocomplete context containing interaction details. Returns: - list[OptionChoice]: A list of available character class names. + list[OptionChoice]: A list of OptionChoice objects representing available character classes. + Each option contains the class name as both the label and the value. """ # Filter and return character class names return [ @@ -390,15 +422,14 @@ async def select_char_class(ctx: discord.AutocompleteContext) -> list[OptionChoi async def select_char_concept(ctx: discord.AutocompleteContext) -> list[OptionChoice]: """Generate a list of available character concepts. - This function fetches the available character concepts, sorts them by name, - and filters them based on the starting string of the argument. - If the number of concepts reaches a maximum size, it stops appending more classes. + Fetch available character concepts and filter based on user input. Args: - ctx (discord.AutocompleteContext): The context in which the function is called. + ctx (discord.AutocompleteContext): The autocomplete context containing interaction details. Returns: - list[OptionChoice]: A list of available character concepts. + list[OptionChoice]: A list of OptionChoice objects representing available character concepts. + Each option contains the concept name (title-cased) as the label and the concept name (uppercase) as the value. """ # Filter and return character class names return [ diff --git a/src/valentina/utils/config.py b/src/valentina/utils/config.py index 07b24420..4dfbccfc 100644 --- a/src/valentina/utils/config.py +++ b/src/valentina/utils/config.py @@ -1,11 +1,14 @@ """Gather configuration from environment variables.""" +import os from pathlib import Path from typing import Annotated, ClassVar from confz import BaseConfig, ConfigSources, EnvSource from pydantic import BeforeValidator +from .console import console + DIR = Path(__file__).parents[3].absolute() @@ -22,7 +25,14 @@ def convert_to_boolean(value: str) -> bool: #### NEW CONFIG #### class ValentinaConfig(BaseConfig): # type: ignore [misc] - """Valentina configuration.""" + """Define and manage configuration settings for the Valentina application. + + Utilize environment variables and configuration files to set up and control + various aspects of the application, including database connections, API keys, + logging preferences, and web interface settings. Implement type checking and + validation for configuration values to ensure proper functionality and + security across different deployment environments. + """ aws_access_key_id: str | None = None aws_secret_access_key: str | None = None @@ -46,7 +56,7 @@ class ValentinaConfig(BaseConfig): # type: ignore [misc] # WebUI Configuration webui_enable: ENV_BOOLEAN = False webui_host: str = "127.0.0.1" - webui_port: str = "8000" + webui_port: str = "8088" webui_log_level: str = "INFO" webui_debug: ENV_BOOLEAN = False webui_base_url: str = "http://127.0.0.1:8088" @@ -61,3 +71,16 @@ class ValentinaConfig(BaseConfig): # type: ignore [misc] EnvSource(prefix="VALENTINA_", file=DIR / ".env", allow_all=True), EnvSource(prefix="VALENTINA_", file=DIR / ".env.secrets", allow_all=True), ] + + +def debug_environment_variables() -> None: + """Print environment variables and ValentinaConfig settings to the console.""" + console.rule("Env Vars") + for key, value in os.environ.items(): + if key not in {"PS1", "LS_COLORS", "PATH"}: + console.log(f"{key}: {value}") + + console.rule("ValentinaConfig") + settings_object = ValentinaConfig().model_dump(mode="python") + for key, value in settings_object.items(): + console.log(f"{key}: {value}") diff --git a/src/valentina/utils/converters.py b/src/valentina/utils/converters.py index dbdb8aaf..f2a68d61 100644 --- a/src/valentina/utils/converters.py +++ b/src/valentina/utils/converters.py @@ -55,10 +55,31 @@ async def convert(self, ctx: commands.Context, argument: str) -> CampaignChapter class ValidCampaign(Converter): - """A converter to grab a campaign object from it's name.""" + """Convert a campaign name to a Campaign object. + + This converter takes a campaign name as input and retrieves the corresponding + Campaign object from the database. It ensures that the provided name + corresponds to a valid, existing campaign. If the campaign is not found, + raise a BadArgument exception. + """ async def convert(self, ctx: commands.Context, argument: str) -> Campaign: # noqa: ARG002 - """Convert from mongo db id to campaign object.""" + """Convert a MongoDB ID to a Campaign object. + + Retrieve a Campaign object from the database using the provided MongoDB ID. + If a matching campaign is found, return the Campaign object. Otherwise, + raise a BadArgument exception with an appropriate error message. + + Args: + ctx (commands.Context): The context of the command invocation. + argument (str): The MongoDB ID of the campaign to retrieve. + + Returns: + Campaign: The retrieved Campaign object. + + Raises: + BadArgument: If no campaign is found with the given ID. + """ campaign = await Campaign.get(argument, fetch_links=True) if campaign: @@ -69,10 +90,21 @@ async def convert(self, ctx: commands.Context, argument: str) -> Campaign: # no class ValidCampaignBook(Converter): # pragma: no cover - """A converter that converts a book id to a CampaignBook object.""" + """Convert a book ID to a CampaignBook object.""" async def convert(self, ctx: commands.Context, argument: str) -> CampaignBook: # noqa: ARG002 - """Validate and normalize book ids.""" + """Convert a book ID to a CampaignBook object. + + Args: + ctx: The context of the command invocation. + argument: The book ID to convert. + + Returns: + CampaignBook: The retrieved CampaignBook object. + + Raises: + BadArgument: If no book is found with the given ID. + """ book = await CampaignBook.get(argument) if book: return book @@ -82,10 +114,21 @@ async def convert(self, ctx: commands.Context, argument: str) -> CampaignBook: class ValidCampaignBookChapter(Converter): # pragma: no cover - """A converter that converts a book id to a CampaignBookChapter object.""" + """Convert a chapter ID to a CampaignBookChapter object.""" async def convert(self, ctx: commands.Context, argument: str) -> CampaignBookChapter: # noqa: ARG002 - """Validate and normalize book ids.""" + """Convert a chapter ID to a CampaignBookChapter object. + + Args: + ctx: The context of the command invocation. + argument: The chapter ID to convert. + + Returns: + CampaignBookChapter: The retrieved CampaignBookChapter object. + + Raises: + BadArgument: If no chapter is found with the given ID. + """ chapter = await CampaignBookChapter.get(argument) if chapter: return chapter @@ -95,10 +138,22 @@ async def convert(self, ctx: commands.Context, argument: str) -> CampaignBookCha class ValidBookNumber(Converter): - """A converter that ensures a requested book number is valid.""" + """Convert and validate a book number for an active campaign.""" async def convert(self, ctx: commands.Context, argument: str) -> int: - """Validate and normalize book numbers.""" + """Convert and validate a book number for an active campaign. + + Args: + ctx: The context of the command invocation. + argument: The book number to convert and validate. + + Returns: + int: The validated book number. + + Raises: + BadArgument: If no active campaign is found, the argument is not a valid integer, + the book number is less than 1, or the book number is not part of the active campaign. + """ channel_objects = await fetch_channel_object(ctx, raise_error=False) campaign = channel_objects.campaign @@ -126,10 +181,22 @@ async def convert(self, ctx: commands.Context, argument: str) -> int: class ValidChapterNumber(Converter): - """A converter that ensures a requested chapter number is valid.""" + """Convert and validate a chapter number for a book.""" async def convert(self, ctx: commands.Context, argument: str) -> int: - """Validate and normalize chapter numbers.""" + """Convert and validate a chapter number for a book. + + Args: + ctx: The context of the command invocation. + argument: The chapter number to convert and validate. + + Returns: + int: The validated chapter number. + + Raises: + BadArgument: If the argument is not a valid integer, the chapter number is less than 1, + or the chapter number is not part of the book. + """ channel_objects = await fetch_channel_object(ctx, raise_error=False) book = channel_objects.book book_chapter_numbers = [x.number for x in await book.fetch_chapters()] diff --git a/src/valentina/utils/database.py b/src/valentina/utils/database.py index 1d9c2d86..00f34f6d 100644 --- a/src/valentina/utils/database.py +++ b/src/valentina/utils/database.py @@ -23,7 +23,15 @@ def test_db_connection() -> bool: # pragma: no cover - """Test the database connection using pymongo.""" + """Test the database connection using pymongo. + + This function attempts to establish a connection to the MongoDB database + using the configuration specified in ValentinaConfig. It uses a short + timeout to quickly determine if the connection can be established. + + Returns: + bool: True if the connection is successful, False otherwise. + """ logger.debug("DB: Testing connection...") mongo_uri = ValentinaConfig().mongo_uri @@ -38,11 +46,14 @@ def test_db_connection() -> bool: # pragma: no cover async def init_database(client=None, database=None) -> None: # type: ignore [no-untyped-def] - """Initialize the database. If a client is not provided, one will be created. + """Initialize the database connection and set up Beanie ODM. + + This function initializes the database connection using the provided client or creates a new one if not provided. + It then sets up the Beanie ODM with the specified document models. Args: - client (AsyncIOMotorClient, optional): The database client. Defaults to None. - database (AsyncIOMotorClient, optional): The database. Defaults to None. + client (AsyncIOMotorClient, optional): The existing database client. If None, a new client will be created. + database (AsyncIOMotorDatabase, optional): The existing database instance. If None, a new database will be selected from the client. """ logger.debug("DB: Initializing...") mongo_uri = ValentinaConfig().mongo_uri diff --git a/src/valentina/utils/discord_utils.py b/src/valentina/utils/discord_utils.py index b1b96e19..62c76c0d 100644 --- a/src/valentina/utils/discord_utils.py +++ b/src/valentina/utils/discord_utils.py @@ -22,14 +22,13 @@ async def assert_permissions( ) -> None: # pragma: no cover """Check if the bot has the required permissions to run the command. - This method verifies that the bot has the necessary permissions specified in the `permissions` argument. If any required permissions are missing, it raises a `BotMissingPermissionsError`. + Verify that the bot has the necessary permissions specified in the permissions argument. + Raise an error if any required permissions are missing. Args: ctx (ValentinaContext): The context object containing the bot's permissions. - **permissions (bool): A dictionary of permissions to check, where the key is the permission name and the value is the required state (True/False). - - Returns: - None + **permissions (bool): Key-value pairs of permissions to check. Keys are permission + names and values are the required states (True/False). Raises: BotMissingPermissionsError: If any required permissions are missing. @@ -43,11 +42,11 @@ async def assert_permissions( async def create_storyteller_role(guild: discord.Guild) -> discord.Role: # pragma: no cover """Create or update the storyteller role for the guild. - This method ensures a "Storyteller" role exists in the guild with the appropriate permissions. - If the role does not exist, it is created. If it exists, its permissions are updated. + Create a "Storyteller" role if it doesn't exist, or update its permissions if it does. + The role is given specific permissions suitable for a storyteller in a role-playing game context. Args: - guild (discord.Guild): The guild where the role should be created or updated. + guild (discord.Guild): The Discord guild to create or update the role in. Returns: discord.Role: The created or updated "Storyteller" role. @@ -98,16 +97,16 @@ async def create_storyteller_role(guild: discord.Guild) -> discord.Role: # prag async def create_player_role(guild: discord.Guild) -> discord.Role: # pragma: no cover - """Create or update the player role for the guild. + """Create or update the Player role in a Discord guild. - This method ensures a "Player" role exists in the guild with the appropriate permissions. - If the role does not exist, it is created. If it exists, its permissions are updated. + This function creates a new Player role if it doesn't exist, or updates an existing one. + The role is set with specific permissions suitable for regular players in the game. Args: - guild (discord.Guild): The guild where the role should be created or updated. + guild (discord.Guild): The Discord guild where the role should be created or updated. Returns: - discord.Role: The created or updated "Player" role. + discord.Role: The created or updated Player role. """ player = discord.utils.get(guild.roles, name="Player", mentionable=True, hoist=True) @@ -153,17 +152,17 @@ async def create_player_role(guild: discord.Guild) -> discord.Role: # pragma: n def set_channel_perms(requested_permission: ChannelPermission) -> discord.PermissionOverwrite: - """Translate a ChannelPermission enum to a discord.PermissionOverwrite object. + """Create a Discord PermissionOverwrite object based on the requested permission level. - Takes a requested channel permission represented as an enum and - sets the properties of a discord.PermissionOverwrite object - to match those permissions. + This function maps a ChannelPermission enum to a set of Discord permissions, + creating a PermissionOverwrite object with the appropriate settings. Args: - requested_permission (ChannelPermission): The channel permission enum. + requested_permission (ChannelPermission): The desired permission level for the channel. Returns: - discord.PermissionOverwrite: Permission settings as a Discord object. + discord.PermissionOverwrite: A PermissionOverwrite object with the permissions set + according to the requested permission level. """ # Map each ChannelPermission to the properties that should be False permission_mapping: dict[ChannelPermission, dict[str, bool]] = { @@ -220,9 +219,9 @@ async def fetch_channel_object( need_character: bool = False, need_campaign: bool = False, ) -> ChannelObjects: - """Determine the type of channel the command was invoked in and fetch the associated objects. + """Determine the channel type and fetch associated objects. - This method identifies the channel type and fetches the related campaign, book, and character objects. It raises errors if specified conditions are not met. + Identify the channel type and fetch related campaign, book, and character objects. Raise errors if specified conditions are not met. Args: ctx (discord.ApplicationContext | discord.AutocompleteContext | commands.Context): The context containing the channel object. diff --git a/src/valentina/utils/helpers.py b/src/valentina/utils/helpers.py index 8af3ce4c..4daa7df6 100644 --- a/src/valentina/utils/helpers.py +++ b/src/valentina/utils/helpers.py @@ -81,15 +81,18 @@ def convert_int_to_emoji(num: int, markdown: bool = False, images: bool = False) def random_num(ceiling: int = 100) -> int: - """Get a random number between 1 and the specified ceiling. + """Generate a random integer within a specified range. - This method returns a random integer between 1 and the given ceiling (inclusive). + Generate and return a random integer between 1 and the given ceiling (inclusive). Args: ceiling (int, optional): The upper limit for the random number. Defaults to 100. Returns: int: A random integer between 1 and the ceiling. + + Raises: + ValueError: If ceiling is less than 1. """ return _rng.integers(1, ceiling + 1) @@ -99,14 +102,16 @@ async def fetch_random_name( ) -> list[tuple[str, str]] | tuple[str, str]: # pragma: no cover """Fetch a random name from the randomuser.me API. + Retrieve one or more random names from the randomuser.me API based on specified criteria. + Args: - country (str, optional): The country to fetch the name from. Defaults to "us". - results (int, optional): The number of results to fetch. Defaults to 1. - gender (str, optional): The gender of the name to fetch. Defaults to None + gender (str | None): The gender of the name to fetch. If None, a random gender is chosen. + country (str): The country code to fetch the name from. Defaults to "us". + results (int): The number of results to fetch. Defaults to 1. Returns: - list[tuple[str, str]] | tuple[str, str]: A list of tuples containing the first and last name. If only one result, a single tuple is returned. - + list[tuple[str, str]] | tuple[str, str]: If results > 1, returns a list of tuples containing + (first_name, last_name). If results == 1, returns a single tuple (first_name, last_name). """ if not gender: gender = random.choice(["male", "female"]) @@ -133,24 +138,35 @@ async def fetch_random_name( def divide_total_randomly( total: int, num: int, max_value: int | None = None, min_value: int = 0 ) -> list[int]: - """Divide a total into 'num' random segments whose sum equals the total. + """Divide a total into random segments with specified constraints. - This function divides a given 'total' into 'num' elements, each with a random value. - The sum of these elements will always equal 'total'. If 'max_value' is provided, - no single element will be greater than this value. Additionally, no element will ever - be less than or equal to 0. + Generate a list of random integers that sum up to a given total. The function ensures + that each segment falls within the specified minimum and maximum values (if provided). + This can be useful for various applications such as resource allocation, random + character attribute generation in games, or any scenario requiring a random + distribution of a total value. + + The algorithm uses an iterative approach to adjust the randomly generated segments + until they meet all specified constraints. It first generates random values within + the allowed range, then iteratively adjusts these values to meet the total sum + requirement while respecting the min and max constraints. Args: - total (int): The total sum to be divided. - num (int): The number of segments to divide the total into. - max_value (int | None): The maximum value any single element can have. - min_value (int): The minimum value any single element can have. Defaults to 0. + total (int): The total sum to be divided. Must be a positive integer. + num (int): The number of segments to divide the total into. Must be a positive integer. + max_value (int | None): The maximum value for any single segment. If None, no upper + limit is applied beyond the total itself. + min_value (int): The minimum value for any single segment. Defaults to 0. Must be + non-negative and less than or equal to max_value (if specified). Returns: - list[int]: A list of integers representing the divided segments. + list[int]: A list of integers representing the divided segments. The length of the + list will be equal to 'num', and the sum of all elements will equal 'total'. Raises: - ValueError: If the total cannot be divided as per the given constraints. + ValueError: If the total cannot be divided according to the given constraints. This + can occur if the total is less than num * min_value, if max_value is less than + min_value, or if num * max_value is less than total. """ if total < num * min_value or (max_value is not None and max_value < min_value): msg = "Impossible to divide under given constraints." @@ -271,7 +287,20 @@ def get_trait_new_value(trait: str, category: str) -> int: async def fetch_data_from_url(url: str) -> io.BytesIO: # pragma: no cover - """Fetch data from a URL to be used to upload to Amazon S3.""" + """Fetch data from a URL and return it as a BytesIO object. + + This function retrieves data from a specified URL and returns it as a BytesIO object, + which can be used for further processing or uploading to services like Amazon S3. + + Args: + url (str): The URL from which to fetch the data. + + Returns: + io.BytesIO: A BytesIO object containing the fetched data. + + Raises: + errors.URLNotAvailableError: If the URL cannot be accessed or returns a non-200 status code. + """ async with ClientSession() as session, session.get(url) as resp: if resp.status != 200: # noqa: PLR2004 msg = f"Could not fetch data from {url}" @@ -322,5 +351,9 @@ def truncate_string(text: str, max_length: int = 1000) -> str: def time_now() -> datetime: - """Return the current time in UTC.""" + """Return the current time in UTC. + + Returns: + datetime: The current UTC time with microseconds set to 0. + """ return datetime.now(UTC).replace(microsecond=0) diff --git a/src/valentina/webui/__init__.py b/src/valentina/webui/__init__.py index 70d98791..dd9a5de5 100644 --- a/src/valentina/webui/__init__.py +++ b/src/valentina/webui/__init__.py @@ -1,4 +1,9 @@ -"""Quart application for the Valentina web interface.""" +"""Define and configure the Quart application for the Valentina web interface. + +This module initializes the Quart application, sets up Discord OAuth2 integration, +configures session management with Redis, and registers custom filters, error handlers, +and JinjaX templates. It serves as the main entry point for the Valentina web UI. +""" import quart_flask_patch # isort: skip # noqa: F401 import asyncio @@ -36,9 +41,11 @@ register_filters(app) register_error_handlers(app) catalog = register_jinjax_catalog(app) + +# Configure the session to use Redis for storage. app.config["SESSION_TYPE"] = "redis" app.config["SESSION_REVERSE_PROXY"] = bool(ValentinaConfig().webui_behind_reverse_proxy) -app.config["SESSION_PROTECTION"] = True +app.config["SESSION_PROTECTION"] = False app.config["SESSION_URI"] = ( (f"redis://:{ValentinaConfig().redis_password}@{ValentinaConfig().redis_addr}") if {ValentinaConfig().redis_addr} @@ -48,7 +55,12 @@ def import_blueprints() -> None: - """Import routes to avoid circular imports.""" + """Register all the blueprints with the Flask application. + + Call this function to import and register the application's blueprints to + avoid circular import issues. It should be invoked during the application + setup phase. + """ from .blueprints import campaign_bp, character_bp, gameplay_bp from .routes import home, oauth @@ -60,13 +72,35 @@ def import_blueprints() -> None: 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 + """Create and configure a Quart application for development. + + Create a Quart app configured for development purposes. Perform the following actions: + 1. Import and register all necessary blueprints. + 2. Set up a database connection pool initialization before the server starts. + 3. Configure error handling and request preprocessing. + + This function should be used to set up the application in a development environment, + ensuring all components are properly initialized and connected. + + Returns: + Quart: A configured Quart application instance ready for development use. + """ import_blueprints() @app.before_serving async def create_db_pool() -> None: - """Initialize the database connection pool.""" + """Initialize the database connection pool before the Quart application starts serving. + + Attempt to establish a connection to the database. If the initial connection fails: + 1. Log the error encountered during the connection attempt. + 2. Wait for 60 seconds before retrying. + 3. Repeat the process until a successful connection is established. + + This ensures that the application will eventually connect to the database, even if it's + temporarily unavailable during startup. The function will not return until a successful + connection is made, preventing the application from serving requests without a valid + database connection. + """ import pymongo from valentina.utils.database import init_database @@ -84,7 +118,10 @@ async def create_db_pool() -> None: def remove_trailing_slash() -> Response: """Redirect requests with trailing slashes to the correct URL. - example.com/url/ -> example.com/url + Redirect URLs that end with a trailing slash (e.g., example.com/url/) + to their non-slash counterparts (e.g., example.com/url) using a 301 + permanent redirect. If the URL does not end with a slash, allow the + request to proceed as normal. """ request_path: str = request.path if request_path != "/" and request_path.endswith("/"): @@ -96,7 +133,21 @@ def remove_trailing_slash() -> Response: async def run_webserver() -> None: - """Run the web server in production.""" + """Run the Quart web server in a production environment. + + Configure and start the Hypercorn server with settings derived from the application's configuration. This function performs the following tasks: + + 1. Import necessary blueprints to avoid circular imports. + 2. Set up a request handler to remove trailing slashes from URLs. + 3. Create a Hypercorn configuration object with settings from ValentinaConfig. + 4. Start the server using the configured Hypercorn instance. + + Call this function to initiate the server in production mode. The server will run + indefinitely until manually stopped or an unhandled exception occurs. + + Note: Ensure all required configurations are properly set in ValentinaConfig + before calling this function. + """ # Imnport these here to avoid circular imports import_blueprints() @@ -104,7 +155,10 @@ async def run_webserver() -> None: def remove_trailing_slash() -> Response: """Redirect requests with trailing slashes to the correct URL. - example.com/url/ -> example.com/url + Redirect URLs that end with a trailing slash (e.g., example.com/url/) + to their non-slash counterparts (e.g., example.com/url) using a 301 + permanent redirect. If the URL does not end with a slash, allow the + request to proceed as normal. """ request_path: str = request.path if request_path != "/" and request_path.endswith("/"): diff --git a/src/valentina/webui/blueprints.py b/src/valentina/webui/blueprints.py index 5a15a30c..723626ca 100644 --- a/src/valentina/webui/blueprints.py +++ b/src/valentina/webui/blueprints.py @@ -1,4 +1,4 @@ -"""Routes for the webui module.""" +"""Blueprints for the webui module.""" from quart import Blueprint diff --git a/src/valentina/webui/utils/discord.py b/src/valentina/webui/utils/discord.py index b5aa6093..cdacd5c6 100644 --- a/src/valentina/webui/utils/discord.py +++ b/src/valentina/webui/utils/discord.py @@ -15,7 +15,20 @@ 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.""" + """Log a message to the console and log file with contextual information. + + Determine the appropriate logging context (module and function name) based + on the call stack, then log the provided message with the given log level. + If a user is provided, append the username to the log entry. + + Args: + msg (str): The message to be logged. + level (str, optional): The log level (e.g., "INFO", "ERROR"). Defaults to "INFO". + user (User, optional): The user associated with the log entry. Defaults to None. + + Returns: + None + """ username = f"@{user.name}" if user else "" if inspect.stack()[1].function == "log_message" and inspect.stack()[2].function in { @@ -48,7 +61,24 @@ async def log_message( level: str = "INFO", view: str = "", ) -> None: - """Log the message to the console, log file, and Discord.""" + """Log a message to the console, log file, and Discord. + + Fetch the current user and log the message with the specified level. + Then, send the log message to a designated Discord channel based on the + log type (either "audit" or "error"). If a user or view is provided, + include this information in the log entry's footer. + + Args: + log_type (Literal["audit", "error"]): The type of log, determining the + Discord channel (audit or error log). + msg (str): The message to be logged. + level (str, optional): The log level (e.g., "INFO", "ERROR"). Defaults to "INFO". + view (str, optional): The name of the view or context in which the log is generated. + Defaults to an empty string. + + Returns: + None + """ user = await fetch_user() log_to_logfile(msg, level, user) @@ -83,21 +113,54 @@ async def log_message( 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.""" + """Send a message to the audit log channel for a guild. + + Log the provided message with the specified level and view context to the + audit log channel. + + Args: + msg (str): The message to be logged. + level (str, optional): The log level (e.g., "INFO", "ERROR"). Defaults to "INFO". + view (str, optional): The name of the view or context in which the log is generated. + Defaults to an empty string. + + Returns: + None + """ 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.""" + """Send a message to the error log channel for a guild. + + Log the provided message with the specified level and view context to the + error log channel. + + Args: + msg (str): The message to be logged. + level (str, optional): The log level (e.g., "ERROR", "WARNING"). Defaults to "ERROR". + view (str, optional): The name of the view or context in which the log is generated. + Defaults to an empty string. + + Returns: + None + """ 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. + """Send a private message to a user on Discord. + + Create a direct message channel with the specified user and send the + provided message to that channel. Args: - user (FlaskDiscordUser): The user to send the message to. - message (str): The message to send. + user (FlaskDiscordUser): The user to whom the message will be sent. + message (str): The content of the message to send. + + Returns: + dict | str: The response from the Discord API, either as a dictionary + or a string indicating the result of the message send operation. """ dm_channel = discord_oauth.bot_request( "/users/@me/channels", "POST", json={"recipient_id": user.id} diff --git a/src/valentina/webui/utils/errors.py b/src/valentina/webui/utils/errors.py index 17ed66aa..cfadc4b1 100644 --- a/src/valentina/webui/utils/errors.py +++ b/src/valentina/webui/utils/errors.py @@ -7,16 +7,42 @@ def register_error_handlers(app: Quart) -> None: - """Register error handlers for the app.""" + """Register custom error handlers for the Quart application. + + Set up error handlers for unauthorized access and general HTTP exceptions. + Unauthorized users are redirected to the login page, and other HTTP + exceptions are handled by rendering a custom error page. + + Args: + app (Quart): The Quart application instance to which the error handlers + will be registered. + + Returns: + None + """ @app.errorhandler(Unauthorized) async def redirect_unauthorized(e: type[Exception] | int) -> Response: # noqa: ARG001 - """Redirect unauthorized users to the login page.""" + """Redirect unauthorized users to the login page. + + Args: + e (type[Exception] | int): The exception or error code that triggered the handler. + + Returns: + Response: A redirect response 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.""" + """Handle HTTP exceptions by rendering a custom error page. + + Args: + exc (HTTPException): The HTTP exception to be handled. + + Returns: + str: The rendered HTML content of the custom error page. + """ return await render_template( "error.html", detail=exc.description, diff --git a/src/valentina/webui/utils/helpers.py b/src/valentina/webui/utils/helpers.py index 3e5cf036..bfd8de71 100644 --- a/src/valentina/webui/utils/helpers.py +++ b/src/valentina/webui/utils/helpers.py @@ -10,7 +10,19 @@ async def fetch_active_campaign( campaign_id: str = "", fetch_links: bool = False ) -> Campaign | None: - """Update and return the active campaign from the session.""" + """Fetch and return the active campaign based on the session state. + + If the guild has only one campaign, return that campaign. If there are multiple + campaigns, return the active campaign based on the provided or existing + `campaign_id`. Update the session with the new active campaign ID if it has changed. + + Args: + campaign_id (str, optional): The ID of the campaign to be set as active. Defaults to "". + fetch_links (bool, optional): Whether to fetch the database-linked objects. Defaults to False. + + Returns: + Campaign | None: The active `Campaign` object if found, or `None` if no active campaign is determined. + """ if len(session["GUILD_CAMPAIGNS"]) == 0: return None @@ -37,7 +49,19 @@ async def fetch_active_campaign( async def fetch_active_character( character_id: str = "", fetch_links: bool = False ) -> Character | None: - """Update and return the active character from the session.""" + """Fetch and return the active character based on the session state. + + If the user has only one character, return that character. If there are multiple + characters, return the active character based on the provided or existing + `character_id`. Update the session with the new active character ID if it has changed. + + Args: + character_id (str, optional): The ID of the character to be set as active. Defaults to "". + fetch_links (bool, optional): Whether to fetch the database-linked objects. Defaults to False. + + Returns: + Character | None: The active `Character` object if found, or `None` if no active character is determined. + """ if len(session["USER_CHARACTERS"]) == 0: return None @@ -62,11 +86,20 @@ async def fetch_active_character( 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. + """Fetch the Guild from the database based on the Discord guild_id stored in the session. + + Retrieve the guild from the database using the guild ID stored in the session, + optionally fetching linked objects. Update the session with the guild's name + if it has changed. Args: - session (SessionMixin): The session to fetch the guild from. - fetch_links (bool): Whether to fetch the database linked objects. + fetch_links (bool): Whether to fetch the database-linked objects. + + Returns: + Guild: The Guild object corresponding to the session's guild ID. + + Raises: + None: If the guild ID is not found in the session, the session is cleared and None is returned. """ # Guard clause to prevent mangled session data if not session.get("GUILD_ID", None): @@ -82,11 +115,20 @@ async def fetch_guild(fetch_links: bool = False) -> Guild: async def fetch_user(fetch_links: bool = False) -> User: - """Fetch the database User based on Discord user_id from the session. + """Fetch the User from the database based on the Discord user_id stored in the session. + + Retrieve the user from the database using the user ID stored in the session, + optionally fetching linked objects. Update the session with the user's name + and avatar URL if they have changed. Args: - fetch_links (bool): Whether to fetch the database linked objects. - session (SessionMixin): The session to fetch the user from. + fetch_links (bool): Whether to fetch the database-linked objects. + + Returns: + User: The User object corresponding to the session's user ID. + + Raises: + None: If the user ID is not found in the session, the session is cleared and None is returned. """ # Guard clause to prevent mangled session data if not session.get("USER_ID", None): @@ -107,11 +149,20 @@ async def fetch_user(fetch_links: bool = False) -> 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. + """Fetch the user's characters and update the session with their names and IDs. + + Retrieve the characters owned by the user within the current guild from the database, + optionally fetching linked objects. Update the session with a dictionary mapping + character names to their IDs if the session data has changed. Args: - fetch_links (bool): Whether to fetch the database linked objects. - session (SessionMixin): The session to fetch the characters from. + fetch_links (bool): Whether to fetch the database-linked objects. + + Returns: + list[Character]: A list of characters owned by the user within the current guild. + + Raises: + None: If the user ID or guild ID is not found in the session, the session is cleared and an empty list is returned. """ # Guard clause to prevent mangled session data if not session.get("USER_ID", None) or not session.get("GUILD_ID", None): @@ -134,11 +185,20 @@ async def fetch_user_characters(fetch_links: bool = True) -> list[Character]: 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. + """Fetch the guild's campaigns and update the session with their names and IDs. + + Retrieve the campaigns associated with the current guild from the database, + optionally fetching linked objects. Update the session with a dictionary + mapping campaign names to their IDs if the session data has changed. Args: - fetch_links (bool): Whether to fetch the database linked objects. - session (SessionMixin): The session to fetch the characters from. + fetch_links (bool): Whether to fetch the database-linked objects. + + Returns: + list[Campaign]: A list of campaigns associated with the guild. + + Raises: + None: If the guild ID is not found in the session, the session is cleared and an empty list is returned. """ # Guard clause to prevent mangled session data if not session.get("GUILD_ID", None): @@ -160,10 +220,14 @@ async def fetch_campaigns(fetch_links: bool = True) -> list[Campaign]: async def update_session() -> None: - """Make updates to the session based on the user's current state. + """Update the session with the user's current state. - Args: - session (SessionMixin): The session to update. + Fetch and update session data related to the user's guild, user details, + characters, and campaigns. If the application is in debug mode and the + log level is set to "DEBUG" or "TRACE", log the session details to the console. + + Returns: + None """ logger.debug("Updating session") await fetch_guild(fetch_links=False) diff --git a/src/valentina/webui/utils/jinja_filters.py b/src/valentina/webui/utils/jinja_filters.py index 190fad75..6c32ce8b 100644 --- a/src/valentina/webui/utils/jinja_filters.py +++ b/src/valentina/webui/utils/jinja_filters.py @@ -6,13 +6,33 @@ def from_markdown(value: str) -> str: - """Convert a Markdown string to HTML.""" + """Convert a Markdown string to HTML. + + Escape the provided Markdown string to prevent HTML injection, + then convert the escaped Markdown content to HTML. + + Args: + value (str): The Markdown string to be converted. + + Returns: + str: The HTML representation of the provided Markdown string. + """ value = escape(value) return markdown(value) def register_filters(app: Quart) -> Quart: - """Register custom Jinja2 filters for the app.""" + """Register custom Jinja2 filters for the Quart application. + + Add custom filters to the Jinja2 environment, such as converting Markdown + to HTML using the `from_markdown` function. + + Args: + app (Quart): The Quart application instance to which the filters will be registered. + + Returns: + Quart: The updated Quart application instance with the registered filters. + """ 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 index 1aee8480..cf8fbc6c 100644 --- a/src/valentina/webui/utils/jinjax.py +++ b/src/valentina/webui/utils/jinjax.py @@ -9,7 +9,19 @@ def register_jinjax_catalog(app: Quart) -> jinjax.Catalog: - """Register the JinJax catalog with the app.""" + """Register the JinJax catalog with the Quart application. + + Initialize a JinJax catalog, add component and template folders to it, + and configure the Jinja2 environment with custom filters and settings. + The catalog is then made available globally in the application's Jinja2 environment. + + Args: + app (Quart): The Quart application instance to which the JinJax catalog + and Jinja2 environment settings will be registered. + + Returns: + jinjax.Catalog: The configured JinJax catalog. + """ catalog = jinjax.Catalog(jinja_env=app.jinja_env) catalog.add_folder(Path(__file__).parent.parent / "components") catalog.add_folder(Path(__file__).parent.parent / "templates") diff --git a/src/valentina/webui/views/campaign_view.py b/src/valentina/webui/views/campaign_view.py index 326cc837..af0f826f 100644 --- a/src/valentina/webui/views/campaign_view.py +++ b/src/valentina/webui/views/campaign_view.py @@ -21,7 +21,25 @@ def __init__(self) -> None: self.is_htmx = bool(request.headers.get("Hx-Request", False)) async def handle_tabs(self, campaign: Campaign) -> str: - """Handle HTMX tabs for the campaign view.""" + """Handle rendering of HTMX tab content for the campaign view. + + Determine the requested tab from the "tab" query parameter and render + the corresponding template for the campaign view. Supported tabs include + 'overview', 'books', 'characters', and 'statistics'. + + Args: + campaign (Campaign): The campaign object to use for rendering the view. + + Returns: + str: The rendered HTML content for the selected tab. + + Raises: + 404: If the requested tab is not recognized or supported. + + Note: + This method is designed to work with HTMX requests for dynamic + tab content loading in the campaign view. + """ if request.args.get("tab") == "overview": return catalog.render("campaign_view.Overview", campaign=campaign) @@ -48,7 +66,34 @@ async def handle_tabs(self, campaign: Campaign) -> str: return abort(404) async def get(self, campaign_id: str = "") -> str: - """Handle GET requests.""" + """Handle GET requests for a specific campaign view. + + Fetch the campaign using the provided campaign ID and render the appropriate view. + Process the request based on whether it's an HTMX request or a regular GET request. + + For HTMX requests: + - Delegate rendering to the `handle_tabs` method. + - Return content specific to the selected tab. + + For regular GET requests: + - Render the main campaign view. + + Args: + campaign_id (str): The unique identifier of the campaign to retrieve. + + Returns: + str: The rendered HTML content for the campaign view. This can be either + tab-specific content for HTMX requests or the full campaign view for + regular GET requests. + + Raises: + 401: If no campaign is found with the provided ID. + 404: If an invalid tab is requested in an HTMX request (raised by `handle_tabs`). + + Note: + This method uses the `request` object to determine if it's an HTMX request + and to access any query parameters for tab selection. + """ campaign = await Campaign.get(campaign_id, fetch_links=True) if not campaign: abort(401) @@ -68,7 +113,26 @@ def __init__(self) -> None: self.is_htmx = bool(request.headers.get("Hx-Request", False)) async def get(self, campaign_id: str = "") -> str: - """Handle GET requests.""" + """Handle GET requests for viewing or editing a campaign. Fetch the campaign using the provided campaign ID and render the appropriate view. + + Determine the view mode based on the "view" query parameter: + - If "view" is set to "edit", render the form for editing the campaign's overview. + - Otherwise, display the campaign's overview information. + + Args: + campaign_id (str): The unique identifier of the campaign to retrieve. + + Returns: + str: Rendered HTML content for either the campaign overview display or + the edit form, depending on the request parameters. + + Raises: + 401: If no campaign is found with the provided ID. + + Note: + This method uses the `request` object to access query parameters for + determining the view mode. + """ campaign = await Campaign.get(campaign_id, fetch_links=True) if not campaign: abort(401) @@ -88,7 +152,28 @@ async def get(self, campaign_id: str = "") -> str: return catalog.render("campaign_view.partials.OverviewDisplay", campaign=campaign) async def post(self, campaign_id: str = "") -> str: - """Handle POST requests.""" + """Handle POST requests for updating a campaign's overview. + + Fetch the campaign using the provided campaign ID. Validate the submitted + form data. If valid, update the campaign's name and description. Update + the session if the campaign's name changes. Render the updated campaign + overview on success, or re-render the edit form with errors if validation fails. + + Args: + campaign_id (str): The unique identifier of the campaign to update. + + Returns: + str: Rendered HTML content for either the updated campaign overview + or the edit form with validation errors. + + Raises: + 401: If no campaign is found with the provided ID. + + Note: + This method uses form validation to ensure data integrity before + updating the campaign. It also handles session updates to maintain + consistency across the application. + """ campaign = await Campaign.get(campaign_id, fetch_links=True) if not campaign: abort(401) diff --git a/src/valentina/webui/views/character_create_full.py b/src/valentina/webui/views/character_create_full.py index 155c124d..5f67d32c 100644 --- a/src/valentina/webui/views/character_create_full.py +++ b/src/valentina/webui/views/character_create_full.py @@ -34,7 +34,13 @@ def __init__(self) -> None: self.key = "CharacterCreateFullData" def _clear_if_expired(self) -> None: - """Clear the session data if it has expired.""" + """Clear session data if it has expired. + + Check if the session contains data associated with the key. If the data + has an expiration time and it is earlier than the current time, clear + the data from the session. + + """ if self.key not in session: return @@ -45,7 +51,15 @@ def _clear_if_expired(self) -> None: self.clear_data() def write_data(self, data: dict) -> None: - """Write data to the session.""" + """Write data to the session with an expiration time. + + Clear existing session data if it has expired. Then, write the provided + data to the session under the specified key. If no expiration time is + set, add a default expiration of 10 minutes from the current time. + + Args: + data (dict): The data to be written to the session. + """ self._clear_if_expired() if self.key not in session: @@ -59,12 +73,28 @@ def write_data(self, data: dict) -> None: session[self.key].update(data) def read_data(self) -> dict: - """Read data from the session.""" + """Read data from the session. + + Clear session data if it has expired, then return the current data + associated with the specified key. If no data exists, return an empty + dictionary. + + Returns: + dict: The session data associated with the key, or an empty dictionary + if no data is found. + """ self._clear_if_expired() return session.get(self.key, {}) def clear_data(self) -> None: - """Clear the data from the session.""" + """Clear the data associated with the key from the session. + + Remove the session data corresponding to the specified key. If the key + does not exist in the session, do nothing. + + Returns: + None + """ session.pop(self.key, None) @@ -74,7 +104,13 @@ class CreateCharacterStart(MethodView): decorators: ClassVar = [requires_authorization] async def get(self) -> str: - """Process initial page load.""" + """Process the initial page load. + + Render and return the main template for the character creation page. + + Returns: + str: The rendered HTML content for the character creation page. + """ return catalog.render("character_create_full.Main") @@ -89,7 +125,15 @@ def __init__(self) -> None: self.join_label = True async def get(self) -> str: - """Process initial page load.""" + """Process the initial page load for the first step of character creation. + + Create and return a form for the first step of character creation. If + there is existing form data stored in the session, prepopulate the form + fields with this data, which is useful when navigating back to this step. + + Returns: + str: The rendered HTML content for the first step of the character creation process. + """ 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. @@ -104,7 +148,21 @@ async def get(self) -> str: ) async def post(self) -> str | Response: - """Process form responses.""" + """Process form submissions for the first step of character creation. + + Validate the submitted form data. If valid, create a new character object + with the provided data and save it to the database. Store the form data + in the session and redirect to the next step in the character creation + process based on the selected character class. If the form validation + fails, re-render the form with validation errors. + + Returns: + str: The rendered HTML content for the first step of the character + creation process if validation fails. + Response: A redirect to the appropriate next step in the character + creation process if the form is successfully validated and the + character is created. + """ form = await CharacterCreateFullStep1().create_form() if await form.validate_on_submit(): character = Character( @@ -157,7 +215,19 @@ def __init__(self) -> None: self.join_label = True def _get_class_specific_form(self, char_class: str) -> QuartForm | None: - """Return the form for the selected class specific items.""" + """Return the form for the selected character class. + + Based on the provided character class, return the corresponding form + for class-specific items. If the class is not recognized, return None. + + Args: + char_class (str): The character class for which to retrieve the form + (e.g., "vampire", "hunter", "werewolf"). + + Returns: + QuartForm | None: The form corresponding to the selected character + class, or None if the class is not recognized. + """ if char_class: match char_class.lower(): case "vampire": @@ -170,7 +240,23 @@ def _get_class_specific_form(self, char_class: str) -> QuartForm | None: return None async def get(self, character_id: str, char_class: str) -> str: - """Process initial page load.""" + """Process the initial page load for the second step of character creation. + + Validate that both a character ID and character class are provided. If valid, + retrieve and create the form corresponding to the character class. If form data + is already stored in the session (useful for navigating back), prepopulate the + form fields with this data. Render and return the form for this step. + + Args: + character_id (str): The unique identifier of the character being created. + char_class (str): The character class to determine which form to render. + + Returns: + str: The rendered HTML content for the second step of the character creation process. + + Raises: + 400: If either the character ID or character class is not provided. + """ if not character_id or not char_class: await post_to_error_log( msg="No character ID or char_class provided to CreateCharacterStep2", @@ -195,7 +281,26 @@ async def get(self, character_id: str, char_class: str) -> str: ) async def post(self, character_id: str, char_class: str) -> str | Response: - """Process form responses.""" + """Process form submissions for the second step of character creation. + + Validate that both a character ID and character class are provided. Retrieve + and validate the form data specific to the character class. If valid, update + the character with the provided data and save it to the database. Store the + form data in the session and redirect to the next step in the character + creation process. If validation fails, re-render the form with errors. + + Args: + character_id (str): The unique identifier of the character being created. + char_class (str): The character class to determine which form to process. + + Returns: + str: The rendered HTML content for the second step of the character creation process if validation fails. + Response: A redirect to the next step in the character creation process if the form is successfully validated and the character is updated. + + Raises: + 400: If either the character ID or character class is not provided. + 401: If the character ID is not found during the form processing. + """ if not character_id or not char_class: await post_to_error_log( msg="No character ID or char_class provided to CreateCharacterStep2", @@ -266,7 +371,18 @@ def __init__(self) -> None: ) async def _fetch_trait_names(self, category: TraitCategory, character: Character) -> list[str]: - """Fetch the trait names for the selected category.""" + """Fetch the trait names for a given category and character class. + + Retrieve and return a list of trait names by combining common traits + and class-specific traits for the selected category. + + Args: + category (TraitCategory): The category of traits to fetch. + character (Character): The character whose class-specific traits are needed. + + Returns: + list[str]: A list of trait names for the given category and character class. + """ return list(category.value.COMMON) + list( getattr(category.value, character.char_class_name) ) @@ -274,7 +390,20 @@ async def _fetch_trait_names(self, category: TraitCategory, character: Character async def _fetch_sheet_traits( self, character: Character ) -> dict[str, dict[str, dict[str, int]]]: - """Fetch the traits for the character sheet.""" + """Fetch and organize traits for the character sheet. + + Retrieve traits for each section of the character sheet, organizing them + by section and category. The traits are sorted according to their defined + order, and their maximum values are determined. + + Args: + character (Character): The character for whom the traits are being fetched. + + Returns: + dict[str, dict[str, dict[str, int]]]: A nested dictionary where the top-level keys + are the section names, the second-level keys are the category names, and the + innermost keys are trait names with their corresponding maximum values. + """ 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] = {} @@ -296,7 +425,21 @@ async def _fetch_sheet_traits( return sheet_traits async def get(self, character_id: str) -> str: - """Process initial page load.""" + """Process the initial page load for the third step of character creation. + + Validate the presence of a character ID, then fetch the character and + its associated traits to render the character sheet. If no character ID + is provided, log the error and abort the request with a 400 status. + + Args: + character_id (str): The unique identifier of the character being created. + + Returns: + str: The rendered HTML content for the third step of the character creation process. + + Raises: + 400: If the character ID is not provided. + """ if not character_id: await post_to_error_log( msg="No character ID provided to CreateCharacterStep3", @@ -315,7 +458,24 @@ async def get(self, character_id: str) -> str: ) async def post(self, character_id: str) -> str | Response: - """Process form responses.""" + """Process form submissions for the third step of character creation. + + Validate the presence of a character ID, then retrieve the character and + process the submitted form data. Extract traits from the form, create + `CharacterTrait` objects, and save them to the character. Clear session + data, log the creation, update the session, and redirect to the character + view with a success message. + + Args: + character_id (str): The unique identifier of the character being created. + + Returns: + str: The rendered HTML content if an error occurs. + Response: A redirect to the character view page upon successful creation. + + Raises: + 400: If the character ID is not provided. + """ if not character_id: await post_to_error_log( msg="No character ID provided to CreateCharacterStep3", diff --git a/src/valentina/webui/views/character_view.py b/src/valentina/webui/views/character_view.py index 90b95864..b3c2a670 100644 --- a/src/valentina/webui/views/character_view.py +++ b/src/valentina/webui/views/character_view.py @@ -49,7 +49,20 @@ async def get_character_object(self, character_id: str) -> 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.""" + """Return all character traits grouped by character sheet section and category. + + Retrieve the character's traits from the database and organize them into a + dictionary grouped by character sheet sections and categories. Only include + traits with non-zero values, unless the category is configured to show zero values. + + Args: + character (Character): The character whose traits are to be retrieved and grouped. + + Returns: + dict[str, dict[str, list[CharacterTrait]]]: A nested dictionary where the top-level + keys are section names, the second-level keys are category names, and the values + are lists of `CharacterTrait` objects associated with each category. + """ character_traits = await CharacterTrait.find( CharacterTrait.character == str(character.id) ).to_list() @@ -77,7 +90,19 @@ async def get_character_sheet_traits( return sheet_traits async def get_character_inventory(self, character: Character) -> dict: - """Get the character's inventory.""" + """Retrieve and return the character's inventory organized by item type. + + Fetch the inventory items associated with the specified character from the + database, grouping them by their item type. Empty item type groups are + removed from the final inventory dictionary. + + Args: + character (Character): The character whose inventory is to be retrieved. + + Returns: + dict: A dictionary where the keys are item types and the values are lists + of `InventoryItem` objects. Empty item type groups are excluded. + """ inventory: dict[str, list[InventoryItem]] = {} for x in InventoryItemType: inventory[x.name] = [] @@ -91,7 +116,18 @@ async def get_character_inventory(self, character: Character) -> dict: 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.""" + """Process form data and update character attributes accordingly. + + Iterate over the form data, updating the character's attributes if they exist + and the value is not "None". Escape any non-empty values to prevent injection. + Log a warning if an attribute in the form does not exist on the character object. + + Args: + character (Character): The character object to be updated with the form data. + + Returns: + None + """ form = await request.form # Iterate over all form fields and update character attributes if they exist and are not "None" @@ -109,13 +145,38 @@ async def process_form_data(self, character: Character) -> None: await character.save() async def get_character_image_urls(self, character: Character) -> list[str]: - """Get image URLs for a character.""" + """Retrieve and return a list of image URLs for the specified character. + + Fetch the URLs of the character's images stored in AWS by utilizing the + AWS service. + + Args: + character (Character): The character whose image URLs are to be retrieved. + + Returns: + list[str]: A list of URLs corresponding to the character's images. + """ 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.""" + """Handle HTMX tab requests and render the appropriate content. + + Based on the "tab" query parameter, render and return the corresponding + section of the character view, such as the character sheet, inventory, + profile, images, or statistics. If the requested tab is not recognized, + return a 404 error. + + Args: + character (Character): The character for which the tab content is to be rendered. + + Returns: + str: The rendered HTML content for the selected tab. + + Raises: + 404: If the requested tab is not recognized. + """ if request.args.get("tab") == "sheet": return catalog.render( "character_view.Sheet", diff --git a/src/valentina/webui/views/gameplay.py b/src/valentina/webui/views/gameplay.py index 8381997d..ee566095 100644 --- a/src/valentina/webui/views/gameplay.py +++ b/src/valentina/webui/views/gameplay.py @@ -27,7 +27,18 @@ def init(self) -> None: self.session = session def get_result_div_classes(self, result_type: RollResultType) -> str: - """Return any classes to be added to the div.""" + """Return CSS classes for the result div based on the roll result type. + + Determine and return the appropriate CSS classes to style the result div + according to the provided `result_type`. The classes will indicate success, + failure, or a critical/botch outcome. + + Args: + result_type (RollResultType): The type of roll result (e.g., critical, success, failure, botch). + + Returns: + str: A string containing the CSS classes to be applied to the result div. + """ if result_type in {RollResultType.CRITICAL, RollResultType.SUCCESS}: return "bg-success-subtle border border-success border-2" @@ -40,13 +51,16 @@ def get_result_div_classes(self, result_type: RollResultType) -> str: return "border border-2" async def process_traits_form(self, form: dict) -> tuple[int, dict[str, int]]: - """Process the trait rolling form. + """Process the form data for trait rolling. + + Calculate the total dice pool based on the provided trait values and + compile a dictionary of the selected traits and their values. Args: - form (dict): The form data. + form (dict): The form data containing trait information. Returns: - tuple[int, dic[str,int]]: The pool and a dict of trait names and values. + tuple[int, dict[str, int]]: A tuple containing the total dice pool (sum of trait values) and a dictionary mapping trait names to their respective values. """ trait1 = json.loads(form.get("trait1", {})) trait2 = json.loads(form.get("trait2", {})) @@ -63,11 +77,15 @@ async def process_traits_form(self, form: dict) -> tuple[int, dict[str, int]]: async def process_macros_form(self, form: dict) -> tuple[int, dict[str, int]]: """Process the macro rolling form. + Retrieve the active character and use the macro data from the form to + calculate the total dice pool and compile a dictionary of the selected + traits and their values. + Args: - form (dict): The form data. + form (dict): The form data containing macro information. Returns: - tuple[int, dic[str,int]]: The pool and a dict of trait names and values. + tuple[int, dict[str, int]]: A tuple containing the total dice pool (sum of trait values) and a dictionary mapping trait names to their respective values. """ character = await fetch_active_character(fetch_links=True) @@ -136,7 +154,18 @@ def __init__(self) -> None: self.dice_size_values = [member.value for member in DiceType] async def handle_form_tabs(self) -> str: - """Tab switcher for the gameplay template.""" + """Switch tabs for the gameplay template based on the selected tab. + + Fetch the active campaign and character, displaying an error message if either is missing. + Depending on the tab specified in the request, render the appropriate tab content + (e.g., "throw", "traits", "macros"). If an unrecognized tab is requested, return a 404 error. + + Returns: + str: The rendered HTML content for the selected tab or an error message if applicable. + + Raises: + 404: If the requested tab is not recognized. + """ campaign = await fetch_active_campaign(fetch_links=True) character = await fetch_active_character(fetch_links=True) error_msg = None diff --git a/tests/models/test_statistics.py b/tests/models/test_statistics.py index e3f6f556..2fbf4c04 100644 --- a/tests/models/test_statistics.py +++ b/tests/models/test_statistics.py @@ -9,7 +9,7 @@ from valentina.models import RollStatistic, Statistics -@pytest.mark.drop_db() +@pytest.mark.drop_db async def test_guild_statistics_no_results(mock_ctx1): """Test pulling guild statistics.""" # GIVEN a guild with no statistics @@ -28,7 +28,7 @@ async def test_guild_statistics_no_results(mock_ctx1): assert result == "\n## Roll statistics for guild `Test Guild`\nNo statistics found" -@pytest.mark.drop_db() +@pytest.mark.drop_db async def test_guild_statistics_results(mock_ctx1): """Test pulling guild statistics.""" # GIVEN a guild with statistics @@ -75,7 +75,7 @@ async def test_guild_statistics_results(mock_ctx1): assert "Successful Rolls ........ 1 (50.00%)" in result.description -@pytest.mark.drop_db() +@pytest.mark.drop_db async def test_user_statistics_no_results(mock_ctx1): """Test pulling user statistics.""" # GIVEN a user with no statistics @@ -94,7 +94,7 @@ async def test_user_statistics_no_results(mock_ctx1): assert result == "\nNo statistics found" -@pytest.mark.drop_db() +@pytest.mark.drop_db async def test_user_statistics_results(mock_ctx1): """Test pulling user statistics.""" # GIVEN a user with statistics @@ -142,7 +142,7 @@ async def test_user_statistics_results(mock_ctx1): assert "Definitions:" not in result.description -@pytest.mark.drop_db() +@pytest.mark.drop_db async def test_character_statistics_no_results(mock_ctx1, character_factory): """Test pulling character_statistics.""" # GIVEN a character with no statistics @@ -263,7 +263,7 @@ async def test_campaign_statistics_results(mock_ctx1, character_factory, campaig assert "Definitions:" not in result.description -@pytest.mark.drop_db() +@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