diff --git a/courageous-comets/.devcontainer/Dockerfile b/courageous-comets/.devcontainer/Dockerfile new file mode 100644 index 0000000..d5d6a65 --- /dev/null +++ b/courageous-comets/.devcontainer/Dockerfile @@ -0,0 +1 @@ +FROM mcr.microsoft.com/vscode/devcontainers/base:debian diff --git a/courageous-comets/.devcontainer/devcontainer.json b/courageous-comets/.devcontainer/devcontainer.json new file mode 100644 index 0000000..90f7a35 --- /dev/null +++ b/courageous-comets/.devcontainer/devcontainer.json @@ -0,0 +1,85 @@ +{ + "containerEnv": { + "BOT_CONFIG_PATH": "${containerWorkspaceFolder}/application.yaml", + "ENVIRONMENT": "development", + "HF_HOME": "${containerWorkspaceFolder}/hf_data", + "NLTK_DATA": "${containerWorkspaceFolder}/nltk_data", + "POETRY_VIRTUALENVS_CREATE": "false", + "REDIS_HOST": "localhost", + "REDIS_PASSWORD": "redis", + "REDIS_PORT": "6379", + "SOPS_AGE_KEY_FILE": "${containerWorkspaceFolder}/secrets/keys.txt" + }, + "customizations": { + "vscode": { + "extensions": [ + "charliermarsh.ruff", + "DavidAnson.vscode-markdownlint", + "KnisterPeter.vscode-commitizen", + "tamasfe.even-better-toml", + "-ms-python.autopep8", + "esbenp.prettier-vscode" + ], + "settings": { + "[markdown]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff" + }, + "editor.formatOnSave": true, + "editor.formatOnSaveMode": "file", + "files.exclude": { + "**/.pytest_cache": true, + "**/.ruff_cache": true, + "**/__pycache__": true + }, + "files.insertFinalNewline": true, + "python.analysis.typeCheckingMode": "basic", + "python.testing.pytestArgs": [ + "." + ], + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false + } + } + }, + "dockerComposeFile": "docker-compose.yaml", + "features": { + "ghcr.io/devcontainers-contrib/features/act:1": {}, + "ghcr.io/devcontainers-contrib/features/apt-packages": { + "packages": "age" + }, + "ghcr.io/devcontainers-contrib/features/poetry:2": {}, + "ghcr.io/devcontainers-contrib/features/sops:1": {}, + "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}, + "ghcr.io/devcontainers/features/python:1": { + "installTools": false, + "version": "3.12" + } + }, + "forwardPorts": [ + 6379, + 8000, + 8001 + ], + "name": "Courageous Comets \u2604\ufe0f", + "portsAttributes": { + "6379": { + "label": "Redis", + "onAutoForward": "silent" + }, + "8000": { + "label": "MkDocs", + "onAutoForward": "notify" + }, + "8001": { + "label": "RedisInsight", + "onAutoForward": "silent" + } + }, + "postCreateCommand": "bash .devcontainer/postcreate.sh", + "service": "dev", + "updateContentCommand": "bash .devcontainer/updatecontent.sh", + "workspaceFolder": "/workspace" +} diff --git a/courageous-comets/.devcontainer/docker-compose.yaml b/courageous-comets/.devcontainer/docker-compose.yaml new file mode 100644 index 0000000..73d52f2 --- /dev/null +++ b/courageous-comets/.devcontainer/docker-compose.yaml @@ -0,0 +1,24 @@ +services: + + # This is the development container + dev: + build: + context: . + dockerfile: Dockerfile + cache_from: + - ghcr.io/thijsfranck/courageous-comets-devcontainer + volumes: + - ..:/workspace + command: 'sleep infinity' + + redis-stack: + image: redis/redis-stack:7.2.0-v11 + environment: + REDIS_ARGS: "--requirepass redis" + volumes: + - redis-stack-data:/data + network_mode: service:dev + restart: unless-stopped + +volumes: + redis-stack-data: diff --git a/courageous-comets/.devcontainer/postcreate.sh b/courageous-comets/.devcontainer/postcreate.sh new file mode 100644 index 0000000..06d8386 --- /dev/null +++ b/courageous-comets/.devcontainer/postcreate.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +# Mark the git repository as safe +git config --global --add safe.directory $PWD + +# Install pre-commit hooks +poetry run pre-commit install + +# Generate a new secret key if one doesn't exist +if [ ! -f secrets/keys.txt ]; then + echo + echo "Generating a new secret key. Share the following public key with the team:" + age-keygen -o secrets/keys.txt + echo +fi diff --git a/courageous-comets/.devcontainer/updatecontent.sh b/courageous-comets/.devcontainer/updatecontent.sh new file mode 100644 index 0000000..2ef1414 --- /dev/null +++ b/courageous-comets/.devcontainer/updatecontent.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# Install or update dependencies +poetry install diff --git a/courageous-comets/.env.lock b/courageous-comets/.env.lock new file mode 100644 index 0000000..19f6f5f --- /dev/null +++ b/courageous-comets/.env.lock @@ -0,0 +1,11 @@ +DISCORD_TOKEN=ENC[AES256_GCM,data:3tRB8k5Yv1VmwJAhG5Bkxv7/ylRBv1eqrXbouZ7jQNUJp+ztXrFBeNUIvI7xoyKCUBaIE/PcCkchbU/Osq9JeB5nI1RKRHC8,iv:jTQ6lPD6jjJ3MWOKKqdkiXXXnGH+CuaxogQ9MtxzOUg=,tag:C6oAqZ9Xgw8B5juWjBR42A==,type:str] +sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCckZmcmVaMkFwZFB6TWtI\nVGw3YjI1d3FmNUlORFlvQ0tmQmpmanIrOHd3Ckw1dlNBZ25uQTczdm96UTM4VDRU\nYUFKMEl1citaVDZGbWhnUE9kaHByMG8KLS0tIDdyTmlJL0IzL3UvSHI0czNLZFpo\nT3JHT21oZmtvTzhyMFJlMXZML1FwY2cKRYz8DUTACEa+Kft3J1i/iN+wJXVCZrCJ\n1rtpDOx9Ys8GmK54bRQih8k5O2S6u7Eps2zLRWOmMTSQj4d2ZCkO1A==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_0__map_recipient=age1sgv2yduaugas4travt8tjctmxe0jyge5p0w6ufcfard5umwaxvss2na2ff +sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5ZGNFNUNjaTNTQ0l5ekNI\nSjJUN1ZLWjdJYkx1UHpQbmhZMHFiOW1qNFFNCnZ4MkhPdWVxcThHUWE5c3FtOGxy\nVTdoeUJoQmtFZEhNSDhudzZib3RpUTQKLS0tIHI3TzdmYmNEZGhnRDRYaFVrTk9P\nWkhKWEhjWHZRVVBjN2FOZzFVUGR5Q1UKh/wkyRmeo4DLKk0yqVA0f1DfhOJpAPCw\nnrkvJ8trH19147od9tDlSQaZo8ipKK0nmijowU6rnS52FjU0hAQm0g==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_1__map_recipient=age1lhk7gg022hrfn6z85m2qdmudm8vql4vn90fuyaulvw99fqv8yctq4uh39k +sops_age__list_2__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFVEJCQ2xDWE1Ed21icXJy\nMDBSY1E5aW50R01YeDEzcTV3Y1c1eFZpQ2dRCm5xNjhGWTVXR2tPcDBzNnd3bldE\nZXhPSDVGNXM5ZkEvaDFLVU1iUFEvR1EKLS0tIERKSEVJZit0cXhzcEJUNUJlV0h3\nS3B5QlB3SFZIVHJLQUlYNHErTGxuT0EKFCsTjivBASb4wnsTjZwEzzM+YDVWtClp\n/+rRuwHrPSKDwce1PaxyqhuG4/UH7StEk/mCkCLnsUqJyTachVzJGQ==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_2__map_recipient=age1u9a2890jjzd3qak5k9xnjngt4qjjvttwag2yjm9sctf6vuawpu2skzekyn +sops_lastmodified=2024-07-18T20:35:27Z +sops_mac=ENC[AES256_GCM,data:Y8e/+o6z3iDosEydIbz2dbPR160I5SqHTgxcQNHEgQqxPTd9MNxsg8Iz4sygTNlB5ByAFOPm6/EbrFreQ9/ChairtoLWEnyRBuBanFSpB0ZU8a6hLC4Y/0ZQ2YN53/OvuZjBfYaHEsrQxvRTTbY9sp1+6HzbUqMvmMKu6WKz2F4=,iv:jhiy7F8NMtfmv7fqqXNQyewHl4CJzCzEc2sgdNf715c=,tag:/KyvJ5oKtBUZXLNUPxfZpg==,type:str] +sops_unencrypted_suffix=_unencrypted +sops_version=3.9.0 diff --git a/courageous-comets/.github/workflows/build-devcontainer.yaml b/courageous-comets/.github/workflows/build-devcontainer.yaml new file mode 100644 index 0000000..f671f7f --- /dev/null +++ b/courageous-comets/.github/workflows/build-devcontainer.yaml @@ -0,0 +1,32 @@ +name: Build Devcontainer + +on: + push: + branches: + - main + paths: + - .devcontainer/**/* + - .github/workflows/build-devcontainer.yaml + +permissions: write-all + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Log in to the Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Pre-build devcontainer image + uses: devcontainers/ci@v0.3 + with: + imageName: ghcr.io/thijsfranck/courageous-comets-devcontainer + cacheFrom: ghcr.io/thijsfranck/courageous-comets-devcontainer + push: always diff --git a/courageous-comets/.github/workflows/ci.yaml b/courageous-comets/.github/workflows/ci.yaml new file mode 100644 index 0000000..3803471 --- /dev/null +++ b/courageous-comets/.github/workflows/ci.yaml @@ -0,0 +1,68 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + ci: + runs-on: ubuntu-latest + + services: + redis: + image: redis/redis-stack:7.2.0-v11 + ports: + - 6379:6379 + # Set health checks to wait until redis has started + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + env: + BOT_CONFIG_PATH: ${{ github.workspace }}/application.yaml + DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }} + NLTK_DATA: ${{ github.workspace }}/nltk_data + HF_HOME: ${{ github.workspace }}/hf_data + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up caching for NLTK_DATA + uses: actions/cache@v4 + with: + path: ${{ env.NLTK_DATA }} + key: ${{ runner.os }}-nltk-data-${{ hashFiles(env.BOT_CONFIG_PATH) }} + restore-keys: | + ${{ runner.os }}-nltk-data- + + - name: Set up caching for HF_HOME + uses: actions/cache@v4 + with: + path: ${{ env.HF_HOME }} + key: ${{ runner.os }}-hf-data-${{ hashFiles(env.BOT_CONFIG_PATH) }} + restore-keys: | + ${{ runner.os }}-hf-data- + + - name: Setup Python, Poetry and dependencies + uses: packetcoders/action-setup-cache-python-poetry@main + with: + python-version: 3.12 + poetry-version: 1.8.3 + + - name: Update GITHUB_PATH + run: echo "$(poetry env info --path)/bin" >> $GITHUB_PATH + shell: bash + + - name: Run pre-commit hooks + uses: pre-commit/action@v3.0.1 + + - name: Run tests + run: poetry run pytest + shell: bash diff --git a/courageous-comets/.github/workflows/publish.yaml b/courageous-comets/.github/workflows/publish.yaml new file mode 100644 index 0000000..d0ff0d5 --- /dev/null +++ b/courageous-comets/.github/workflows/publish.yaml @@ -0,0 +1,52 @@ +name: Publish + +on: + push: + tags: + - v*.*.* + +permissions: write-all + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Log in to the Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Python, Poetry and Dependencies + uses: packetcoders/action-setup-cache-python-poetry@main + with: + python-version: 3.12 + poetry-version: 1.8.3 + + - name: Build the Application + run: poetry build -f wheel + shell: bash + + - name: Build and Publish the Docker Image + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ghcr.io/thijsfranck/courageous-comets:${{ github.ref_name }},ghcr.io/thijsfranck/courageous-comets:latest + + - name: Set up Git user + uses: fregante/setup-git-user@v2.0.1 + + - name: Build documentation + run: bash docs/replace-token.sh "" "${{ github.ref_name }}" + + - name: Publish documentation + run: poetry run mike deploy -b public-docs --push --update-aliases ${{ github.ref_name }} latest + shell: bash diff --git a/courageous-comets/.gitignore b/courageous-comets/.gitignore new file mode 100644 index 0000000..6bf316f --- /dev/null +++ b/courageous-comets/.gitignore @@ -0,0 +1,189 @@ +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +# VSCode +.vscode/ + +# Secrets +*.asc + +# Vim +*.vim + +# NLTK +nltk_data + +# Huggingface +hf_data + +# Artifacts +artifacts/ diff --git a/courageous-comets/.markdownlint.json b/courageous-comets/.markdownlint.json new file mode 100644 index 0000000..4f0486d --- /dev/null +++ b/courageous-comets/.markdownlint.json @@ -0,0 +1,22 @@ +{ + "MD013": { + "code_blocks": false, + "line_length": 110, + "tables": false + }, + "MD024": { + "siblings_only": true + }, + "MD033": { + "allowed_elements": [ + "div", + "img", + "figcaption", + "figure", + "source", + "video" + ] + }, + "MD046": false, + "default": true +} diff --git a/courageous-comets/.markdownlintignore b/courageous-comets/.markdownlintignore new file mode 100644 index 0000000..a9bb6dc --- /dev/null +++ b/courageous-comets/.markdownlintignore @@ -0,0 +1 @@ +docs/CHANGELOG.md diff --git a/courageous-comets/.pre-commit-config.yaml b/courageous-comets/.pre-commit-config.yaml new file mode 100644 index 0000000..0a50b0f --- /dev/null +++ b/courageous-comets/.pre-commit-config.yaml @@ -0,0 +1,57 @@ +default_install_hook_types: [commit-msg, pre-commit] + +repos: + - repo: https://github.com/commitizen-tools/commitizen + rev: v3.27.0 + hooks: + - id: commitizen + name: Check commit message format + stages: [commit-msg] + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: check-merge-conflict + name: Check for merge conflicts + - id: check-json + name: Check for JSON syntax errors + - id: check-toml + name: Check for TOML syntax errors + - id: check-yaml + name: Check for YAML syntax errors + args: [--unsafe] + - id: end-of-file-fixer + name: Ensure files end with a newline + - id: pretty-format-json + name: Format JSON files + args: [--autofix] + - id: trailing-whitespace + name: Trim trailing whitespace + args: [--markdown-linebreak-ext=md] + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.5.2 + hooks: + - id: ruff + name: Check for Python linting errors + args: [ --fix ] + - id: ruff-format + name: Run Python formatter + + - repo: https://github.com/igorshubovych/markdownlint-cli + rev: v0.41.0 + hooks: + - id: markdownlint + name: Check for Markdown linting errors + + - repo: https://github.com/RobertCraigie/pyright-python + rev: v1.1.373 + hooks: + - id: pyright + name: pyright (system) + description: 'pyright static type checker' + entry: pyright + language: system + 'types_or': [python, pyi] + require_serial: true + minimum_pre_commit_version: '2.9.2' diff --git a/courageous-comets/.sops.yaml b/courageous-comets/.sops.yaml new file mode 100644 index 0000000..cb11af7 --- /dev/null +++ b/courageous-comets/.sops.yaml @@ -0,0 +1,5 @@ +creation_rules: + - age: >- + age1sgv2yduaugas4travt8tjctmxe0jyge5p0w6ufcfard5umwaxvss2na2ff, + age1lhk7gg022hrfn6z85m2qdmudm8vql4vn90fuyaulvw99fqv8yctq4uh39k, + age1u9a2890jjzd3qak5k9xnjngt4qjjvttwag2yjm9sctf6vuawpu2skzekyn diff --git a/courageous-comets/Dockerfile b/courageous-comets/Dockerfile new file mode 100644 index 0000000..554acdb --- /dev/null +++ b/courageous-comets/Dockerfile @@ -0,0 +1,41 @@ +# Use the official Python 3.12 slim image as the base image +FROM python:3.12-slim + +# Add image metadata +LABEL org.opencontainers.image.authors="Courageous Comets ☄️" +LABEL org.opencontainers.image.description="This application was built by the Courageous Comets team for the Python Discord Summer Code Jam 2024" +LABEL org.opencontainers.image.documentation=https://thijsfranck.github.io/courageous-comets/ +LABEL org.opencontainers.image.licenses=MIT +LABEL org.opencontainers.image.source=https://github.com/thijsfranck/courageous-comets +LABEL org.opencontainers.image.title="Courageous Comets" + +# Set default environment variables +ENV BOT_CONFIG_PATH=/app/application.yaml +ENV LOG_LEVEL=INFO +ENV MPLCONFIGDIR=/app/matplotlib +ENV NLTK_DATA=/app/nltk_data +ENV HF_HOME=/app/hf_data + +# Add a non-root user and group +RUN addgroup --system courageous-comets && \ + adduser --system --ingroup courageous-comets courageous-comets + +# Set the working directory +WORKDIR /app + +# Assign the working directory to the non-root user and set the permissions +RUN chown -R courageous-comets:courageous-comets /app && \ + chmod -R 0770 /app + +# Copy the app config and the wheel file to the working directory and set the permissions +COPY --chown=courageous-comets:courageous-comets --chmod=0440 application.yaml dist/*.whl ./ + +# Install the wheel file and clean up to reduce image size +RUN pip install --no-cache-dir *.whl && \ + rm *.whl + +# Switch to the non-root user +USER courageous-comets + +# Run the application +ENTRYPOINT ["python", "-m", "courageous_comets"] diff --git a/courageous-comets/LICENSE b/courageous-comets/LICENSE new file mode 100644 index 0000000..d08daf7 --- /dev/null +++ b/courageous-comets/LICENSE @@ -0,0 +1,7 @@ +Copyright 2024 Courageous Comets + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/courageous-comets/application.yaml b/courageous-comets/application.yaml new file mode 100644 index 0000000..db25308 --- /dev/null +++ b/courageous-comets/application.yaml @@ -0,0 +1,21 @@ +cogs: + - courageous_comets.cogs.about + - courageous_comets.cogs.keywords.search_command + - courageous_comets.cogs.keywords.search_context_menu + - courageous_comets.cogs.keywords.topics_command + - courageous_comets.cogs.keywords.user_context_menu + - courageous_comets.cogs.messages + - courageous_comets.cogs.ping + - courageous_comets.cogs.sentiment.message_context_menu + - courageous_comets.cogs.sentiment.search_command + - courageous_comets.cogs.sentiment.search_context_menu + - courageous_comets.cogs.sentiment.user_context_menu + - courageous_comets.cogs.frequency +dev-cogs: + - jishaku +nltk: + - punkt + - stopwords + - vader_lexicon +transformers: + - sentence-transformers/all-MiniLM-L6-v2 diff --git a/courageous-comets/courageous_comets/__init__.py b/courageous-comets/courageous_comets/__init__.py new file mode 100644 index 0000000..3e6d1ef --- /dev/null +++ b/courageous-comets/courageous_comets/__init__.py @@ -0,0 +1,13 @@ +import importlib.metadata +import logging + +from .client import bot + +__all__ = ["bot"] + +# Package version may not be available in CI. +try: + __version__ = f"v{importlib.metadata.version("courageous_comets")}" +except importlib.metadata.PackageNotFoundError: + logging.warning("Could not determine the package version.") + __version__ = "latest" diff --git a/courageous-comets/courageous_comets/__main__.py b/courageous-comets/courageous_comets/__main__.py new file mode 100644 index 0000000..6739627 --- /dev/null +++ b/courageous-comets/courageous_comets/__main__.py @@ -0,0 +1,34 @@ +import asyncio +import contextlib +import logging + +import discord + +from courageous_comets import __version__, bot, exceptions, settings + + +async def main() -> None: + """ + Start the appication. + + If a critical error occurs, attempt to shut down gracefully. + """ + # Override logging configuration by dependencies + settings.setup_logging() + + logging.info("Starting the Courageous Comets application (%s) ☄️", __version__) + try: + await bot.start(settings.DISCORD_TOKEN) + except discord.LoginFailure: + logging.critical("Discord login failed. Check the DISCORD_TOKEN environment variable.") + except (exceptions.CourageousCometsError, discord.DiscordException) as e: + logging.critical( + "A fatal error occurred while running the Courageous Comets application.", + exc_info=e, + ) + finally: + await bot.close() + + +with contextlib.suppress(KeyboardInterrupt): + asyncio.run(main()) diff --git a/courageous-comets/courageous_comets/client.py b/courageous-comets/courageous_comets/client.py new file mode 100644 index 0000000..2b83dfa --- /dev/null +++ b/courageous-comets/courageous_comets/client.py @@ -0,0 +1,175 @@ +import logging +import typing +from typing import override + +import discord +import yaml +from discord import Intents +from discord.ext import commands +from redis.asyncio import Redis + +from courageous_comets import settings +from courageous_comets.nltk import init_nltk +from courageous_comets.redis import init_redis +from courageous_comets.vectorizer import Vectorizer + +DESCRIPTION = """ +Thank you for using Courageous Comets! ☄️ + +This bot is designed to help you: + +- 🚀 Connect with others who share your interests. +- 🤗 Find the friendliest communities. +- 🛡️ Moderate your server with ease. +""" + +logger = logging.getLogger(__name__) + + +intents = Intents.default() +intents.members = True +intents.message_content = True + +with settings.BOT_CONFIG_PATH.open("r") as config_file: + CONFIG = yaml.safe_load(config_file) + + +class CourageousCometsBot(commands.Bot): + """ + The Courageous Comets Discord bot. + + Attributes + ---------- + redis : redis.asyncio.Redis | None + The Redis connection instance for the bot, or `None` if not connected. + """ + + redis: Redis | None = None + vectorizer = Vectorizer() + + def __init__(self) -> None: + super().__init__( + command_prefix=commands.when_mentioned, + intents=intents, + description=DESCRIPTION, + ) + + @override + async def close(self) -> None: + """ + Gracefully shut down the application. + + First closes the Discord client, then the Redis connection if it exists. + + Overrides the `close` method in `discord.ext.commands.Bot`. + """ + logger.info("Gracefully shutting down the application...") + + await super().close() + + if self.redis is not None: + await self.redis.aclose() + logger.info("Closed the Redis connection") + + logger.info("Application shutdown complete. Goodbye! 👋") + + async def load_cogs(self, cogs: list[str]) -> None: + """Load all given cogs.""" + for cog in cogs: + try: + await bot.load_extension(cog) + logger.debug("Loaded cog %s", cog) + except commands.ExtensionError as e: + logger.exception("Failed to load cog %s", cog, exc_info=e) + + async def on_ready(self) -> None: + """Log a message when the bot is ready.""" + logger.info("Logged in as %s", self.user) + + async def setup_hook(self) -> None: + """ + On startup, initialize the bot. + + Performs the following setup actions: + + - Connect to Redis. + - Load the NLTK resources. + - Set up the vectorizer. + - Load the cogs. + """ + logger.info("Initializing the Discord client...") + + self.redis = await init_redis() + + nltk_resources = CONFIG.get("nltk", []) + await init_nltk(nltk_resources) + + cogs = CONFIG.get("cogs", []) + await self.load_cogs(cogs) + + if settings.IS_DEV: + dev_cogs = CONFIG.get("dev-cogs", []) + await self.load_cogs(dev_cogs) + + logger.info("Initialization complete 🚀") + + +bot = CourageousCometsBot() + + +@bot.command() +@commands.guild_only() +@commands.is_owner() +async def sync( + ctx: commands.Context[commands.Bot], + guilds: commands.Greedy[discord.Object], + spec: typing.Literal["~", "*", "^"] | None = None, +) -> None: + """ + Sync to the given scope. + + If no scope is provided and no guilds are given, sync the current tree to the global scope. + `~` - Sync to the current guild. + `*` - Sync to the global scope. + `^` - Remove all non-global commands from the current guild. + No spec - Sync the current tree to the current guild, used mostly for development. + + Parameters + ---------- + ctx : commands.Context[commands.Bot] + The context of the command. + guilds : commands.Greedy[discord.Object] + The guilds to sync to. + spec : typing.Literal["~", "*", "^"] | None + The scope to sync to. Defaults to `~`. + """ + async with ctx.typing(): + if not guilds: + if spec == "~": + synced = await ctx.bot.tree.sync(guild=ctx.guild) + elif spec == "*": + synced = await ctx.bot.tree.sync() + elif spec == "^": + ctx.bot.tree.clear_commands(guild=ctx.guild) + await ctx.bot.tree.sync(guild=ctx.guild) + synced = [] + else: + ctx.bot.tree.copy_global_to(guild=ctx.guild) # type: ignore (@commands.guild_only() ensures ctx has guild attribute) + synced = await ctx.bot.tree.sync(guild=ctx.guild) + + scope = "globally." if spec == "*" else "to the current guild." + await ctx.send(f"Synced {len(synced)} command(s) {scope}") + + return + + ret = 0 + + for guild in guilds: + try: + await ctx.bot.tree.sync(guild=guild) + except discord.HTTPException as e: + logger.exception("Failed to sync to guild %s", guild, exc_info=e) + else: + ret += 1 + + await ctx.send(f"Synced the tree to {ret}/{len(guilds)}.") diff --git a/courageous-comets/courageous_comets/cogs/__init__.py b/courageous-comets/courageous_comets/cogs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/courageous-comets/courageous_comets/cogs/about.py b/courageous-comets/courageous_comets/cogs/about.py new file mode 100644 index 0000000..693c646 --- /dev/null +++ b/courageous-comets/courageous_comets/cogs/about.py @@ -0,0 +1,44 @@ +import logging + +import discord +from discord import app_commands +from discord.ext import commands + +from courageous_comets.ui.embeds import about + +logger = logging.getLogger(__name__) + + +class About(commands.Cog): + """A cog that provides information about the app upon request.""" + + def __init__(self, bot: commands.Bot) -> None: + self.bot = bot + + @app_commands.command( + name="about", + description="Get information about the app.", + ) + async def about_command(self, interaction: discord.Interaction) -> None: + """ + Respond to the `/about` command with information about the app. + + Parameters + ---------- + interaction : discord.Interaction + The interaction that triggered the command + """ + logger.info( + "User %s requested the about message using the /about command.", + interaction.user.id, + ) + + await interaction.response.send_message( + embed=about.render(self.bot.description), + ephemeral=True, + ) + + +async def setup(bot: commands.Bot) -> None: + """Load the cog.""" + await bot.add_cog(About(bot)) diff --git a/courageous-comets/courageous_comets/cogs/frequency.py b/courageous-comets/courageous_comets/cogs/frequency.py new file mode 100644 index 0000000..957b66f --- /dev/null +++ b/courageous-comets/courageous_comets/cogs/frequency.py @@ -0,0 +1,122 @@ +import logging + +import discord +from discord import app_commands +from discord.ext import commands + +from courageous_comets.client import CourageousCometsBot +from courageous_comets.enums import Duration +from courageous_comets.redis.messages import get_messages_frequency +from courageous_comets.ui.charts import frequency_line +from courageous_comets.ui.embeds import message_frequency + +logger = logging.getLogger(__name__) + + +class Frequency(commands.Cog): + """ + A cog that provides frequency analysis for messages over a duration. + + Attributes + ---------- + bot : CourageousCometsBot + The bot instance. + """ + + def __init__(self, bot: CourageousCometsBot) -> None: + self.bot = bot + + @app_commands.command( + name="frequency", + description="Get frequency of messages over a duration.", + ) + async def frequency_command( + self, + interaction: discord.Interaction, + duration: Duration, + ) -> None: + """ + Allow users to view the frequency of messages. + + Generates an embed of a graph of number of messages over duration. + + Parameters + ---------- + interaction: discord.Interaction + The interaction that triggered the command. + duration: courageous_comets.enums.Duration + The duration over which to aggregate the number of messages. + """ + logger.info( + "User %s requested a frequency chart %s using the /frequency command.", + interaction.user.id, + interaction.id, + ) + + if self.bot.redis is None: + logger.error( + "Could not answer frequency request %s due to Redis being unavailable.", + interaction.id, + ) + return await interaction.response.send_message( + "This feature is currently unavailable. Please try again later.", + ephemeral=True, + ) + + if interaction.guild is None: + logger.debug( + "Could not answer frequency request %s due to it being used outside of a guild.", + interaction.id, + ) + return await interaction.response.send_message( + "This feature is only available in guilds.", + ephemeral=True, + ) + + if duration not in Duration: + logger.debug( + "Could not answer frequency request %s due to an invalid duration.", + interaction.id, + ) + return await interaction.response.send_message( + "Invalid duration provided.", + ephemeral=True, + ) + + frequencies = await get_messages_frequency( + self.bot.redis, + guild_id=str(interaction.guild.id), + duration=duration, + limit=10_000, + ) + + if not frequencies: + logger.debug( + "Could not answer frequency request %s due to no messages being found.", + interaction.id, + ) + return await interaction.response.send_message( + "No messages were found over the specified duration at this time.", + ephemeral=True, + ) + + embed = message_frequency.render(frequencies, duration) + + chart = frequency_line.render(frequencies, duration) + embed.set_image(url=f"attachment://{chart.filename}") + + logger.debug( + "Returning frequency chart for frequency request %s.", + interaction.id, + ) + + return await interaction.response.send_message( + embed=embed, + file=chart, + ephemeral=True, + ) + + +async def setup(bot: CourageousCometsBot) -> None: + """Load the cog.""" + await bot.add_cog(Frequency(bot)) diff --git a/courageous-comets/courageous_comets/cogs/keywords/__init__.py b/courageous-comets/courageous_comets/cogs/keywords/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/courageous-comets/courageous_comets/cogs/keywords/search_command.py b/courageous-comets/courageous_comets/cogs/keywords/search_command.py new file mode 100644 index 0000000..fc1897b --- /dev/null +++ b/courageous-comets/courageous_comets/cogs/keywords/search_command.py @@ -0,0 +1,120 @@ +import logging + +import discord +from discord import app_commands +from discord.ext import commands + +from courageous_comets import preprocessing +from courageous_comets.client import CourageousCometsBot +from courageous_comets.discord.messages import resolve_messages +from courageous_comets.redis.messages import get_messages_by_semantics_similarity +from courageous_comets.ui.embeds import search_results + +logger = logging.getLogger(__name__) + + +class SearchCommand(commands.Cog): + """ + A cog for the /search command. + + This cog allows users to search for related messages using the /search command. + + Attributes + ---------- + bot : CourageousCometsBot + The bot instance. + """ + + def __init__(self, bot: CourageousCometsBot) -> None: + self.bot = bot + + @app_commands.command(name="search", description="Search for related messages.") + async def search_by_topic( + self, + interaction: discord.Interaction, + query: str, + ) -> None: + """ + Search for related messages using the /search command. + + Parameters + ---------- + interaction : discord.Interaction + The interaction that triggered the command. + query : str + The query to search for related messages. + """ + logger.info( + "User %s requested a search for related messages %s using the /search command.", + interaction.user.id, + interaction.id, + ) + + if self.bot.redis is None: + logger.error( + "Could not answer search request %s due to Redis being unavailable.", + interaction.id, + ) + return await interaction.response.send_message( + "This feature is currently unavailable. Please try again later.", + ephemeral=True, + ) + + if not interaction.guild: + logger.debug( + "Could not answer search request %s due to it being used outside of a guild.", + interaction.id, + ) + return await interaction.response.send_message( + "This command can only be used in a guild.", + ephemeral=True, + ) + + if not interaction.channel: + logger.debug( + "Could not answer search request %s due to it being used outside of a channel.", + interaction.id, + ) + return await interaction.response.send_message( + "This command can only be used in a channel.", + ephemeral=True, + ) + + if not query: + logger.debug( + "Could not answer search request %s due to it not having a query.", + interaction.id, + ) + return await interaction.response.send_message( + "Please provide a query to search for related messages.", + ephemeral=True, + ) + + await interaction.response.defer(ephemeral=True, thinking=True) + + query_processed = preprocessing.process(query) + embedding = await self.bot.vectorizer.aencode(query_processed) + + messages = await get_messages_by_semantics_similarity( + self.bot.redis, + guild_id=str(interaction.guild.id), + embedding=embedding, + limit=5, + ) + + resolved_messages = await resolve_messages(self.bot, messages) + + if not resolved_messages: + logger.debug("No related messages were found for search request %s.", interaction.id) + return await interaction.followup.send("No related messages were found.") + + embed = search_results.render(query, resolved_messages) + + logger.debug("Returning search results for search request %s.", interaction.id) + + return await interaction.followup.send(embed=embed) + + +async def setup(bot: CourageousCometsBot) -> None: + """Load the cog.""" + await bot.add_cog(SearchCommand(bot)) diff --git a/courageous-comets/courageous_comets/cogs/keywords/search_context_menu.py b/courageous-comets/courageous_comets/cogs/keywords/search_context_menu.py new file mode 100644 index 0000000..2f02e0f --- /dev/null +++ b/courageous-comets/courageous_comets/cogs/keywords/search_context_menu.py @@ -0,0 +1,118 @@ +import logging + +import discord +from discord import app_commands +from discord.ext import commands + +from courageous_comets import preprocessing +from courageous_comets.client import CourageousCometsBot +from courageous_comets.discord.messages import resolve_messages +from courageous_comets.processing import process_message +from courageous_comets.redis.keys import key_schema +from courageous_comets.redis.messages import get_messages_by_semantics_similarity +from courageous_comets.ui.embeds import search_results + +logger = logging.getLogger(__name__) + + +class SearchContextMenu(commands.Cog): + """A boilerplate cog.""" + + def __init__(self, bot: CourageousCometsBot) -> None: + self.bot = bot + + menu = app_commands.ContextMenu( + name="Search by topic", + callback=self.search_by_topic, + ) + self.bot.tree.add_command(menu) + + async def search_by_topic( + self, + interaction: discord.Interaction, + message: discord.Message, + ) -> None: + """ + Search for related messages using a context menu. + + Parameters + ---------- + interaction : discord.Interaction + The interaction that triggered the command. + message : discord.Message + The message to use as a reference for the search. + """ + logger.info( + "User %s requested search by topic %s for message %s.", + interaction.user.id, + interaction.id, + message.id, + ) + + if self.bot.redis is None: + logger.error( + "Could not answer search request %s due to Redis being unavailable.", + interaction.id, + ) + return await interaction.response.send_message( + "This feature is currently unavailable. Please try again later.", + ephemeral=True, + ) + + if message.guild is None: + logger.debug( + "Could not answer search request %s due to it being used outside of a guild.", + interaction.id, + ) + return await interaction.response.send_message( + "This feature is only available in guilds.", + ephemeral=True, + ) + + await interaction.response.defer(ephemeral=True, thinking=True) + + key = key_schema.guild_messages( + guild_id=message.guild.id, + message_id=message.id, + ) + + if not self.bot.redis.exists(key): + await process_message( + message, + redis=self.bot.redis, + vectorizer=self.bot.vectorizer, + ) + + content_processed = preprocessing.process(message.clean_content) + embedding = await self.bot.vectorizer.aencode(content_processed) + + messages = await get_messages_by_semantics_similarity( + self.bot.redis, + guild_id=str(message.guild.id), + embedding=embedding, + limit=6, + ) + + resolved_messages = [ + resolved_message + for resolved_message in await resolve_messages(self.bot, messages) + if resolved_message.id != message.id + ] + + if not resolved_messages: + logger.debug("No related messages were found for search request %s.", interaction.id) + return await interaction.followup.send( + "No related messages were found.", + ephemeral=True, + ) + + embed = search_results.render(message.clean_content, resolved_messages) + + logger.debug("Returning search results for search request %s.", interaction.id) + + return await interaction.followup.send(embed=embed, ephemeral=True) + + +async def setup(bot: CourageousCometsBot) -> None: + """Load the cog.""" + await bot.add_cog(SearchContextMenu(bot)) diff --git a/courageous-comets/courageous_comets/cogs/keywords/topics_command.py b/courageous-comets/courageous_comets/cogs/keywords/topics_command.py new file mode 100644 index 0000000..b99be79 --- /dev/null +++ b/courageous-comets/courageous_comets/cogs/keywords/topics_command.py @@ -0,0 +1,125 @@ +import logging + +import discord +import discord.ext +import discord.ext.commands + +from courageous_comets.client import CourageousCometsBot +from courageous_comets.enums import StatisticScope +from courageous_comets.redis.messages import get_tokens_count +from courageous_comets.ui.charts import keywords_bars +from courageous_comets.ui.embeds import popular_topics + +logger = logging.getLogger(__name__) + + +class TopicsCommand(discord.ext.commands.Cog): + """ + A cog that provides keyword analysis for a guild, channel or user using a slash command. + + Attributes + ---------- + bot : CourageousCometsBot + The bot instance. + """ + + def __init__(self, bot: CourageousCometsBot) -> None: + self.bot = bot + + @discord.app_commands.command( + name="topics", + description="Show the most commonly used keywords.", + ) + async def show_keywords( + self, + interaction: discord.Interaction, + scope: StatisticScope = StatisticScope.GUILD, + ) -> None: + """ + Allow users to view the most commonly used keywords in the server. + + Replies with an embed with the most commonly used keywords in the given scope. + The embed contains a bar chart of the keywords used in the server. + + Parameters + ---------- + interaction: discord.Interaction + The interaction that triggered the command. + scope : StatisticScope + The scope of the keywords to show. + """ + logger.info( + "User %s requested the most commonly used keywords %s in %s using the /topics command.", + interaction.user.id, + interaction.id, + scope.name, + ) + + if self.bot.redis is None: + logger.error( + "Could not answer topics request %s due to Redis being unavailable.", + interaction.id, + ) + return await interaction.response.send_message( + "This feature is currently unavailable. Please try again later.", + ephemeral=True, + ) + + if not interaction.guild: + logger.debug( + "Could not answer topics request %s due to it being used outside of a guild.", + interaction.id, + ) + return await interaction.response.send_message( + "This command can only be used in a guild.", + ephemeral=True, + ) + + if scope == StatisticScope.CHANNEL and not interaction.channel: + logger.debug( + "Could not answer topics request %s due to it being used outside of a channel.", + interaction.id, + ) + return await interaction.response.send_message( + "This command can only be used in a channel.", + ephemeral=True, + ) + + await interaction.response.defer(ephemeral=True, thinking=True) + + ids = ( + [str(interaction.channel.id)] # type: ignore + if scope == StatisticScope.CHANNEL + else [str(interaction.user.id)] + if scope == StatisticScope.USER + else None + ) + + keywords = await get_tokens_count( + self.bot.redis, + guild_id=str(interaction.guild.id), + scope=scope, + ids=ids, + ) + + if not keywords: + logger.debug( + "Could not find any keywords for scope %s.", + scope.name, + ) + return await interaction.followup.send( + f"No keywords found for scope {scope.name}.", + ephemeral=True, + ) + + embed = popular_topics.render(scope, keywords) + + chart = keywords_bars.render(keywords) + embed.set_image(url=f"attachment://{chart.filename}") + + return await interaction.followup.send(embed=embed, file=chart, ephemeral=True) + + +async def setup(bot: CourageousCometsBot) -> None: + """Load the cog.""" + await bot.add_cog(TopicsCommand(bot)) diff --git a/courageous-comets/courageous_comets/cogs/keywords/user_context_menu.py b/courageous-comets/courageous_comets/cogs/keywords/user_context_menu.py new file mode 100644 index 0000000..c01f15b --- /dev/null +++ b/courageous-comets/courageous_comets/cogs/keywords/user_context_menu.py @@ -0,0 +1,99 @@ +import logging + +import discord +import discord.ext +import discord.ext.commands + +from courageous_comets.client import CourageousCometsBot +from courageous_comets.enums import StatisticScope +from courageous_comets.redis.messages import get_tokens_count +from courageous_comets.ui.charts import keywords_bars +from courageous_comets.ui.embeds import user_keywords + +logger = logging.getLogger(__name__) + + +class KeywordsUserContextMenu(discord.ext.commands.Cog): + """ + A cog that provides keyword analysis for a user using a context menu. + + Attributes + ---------- + bot : CourageousCometsBot + The bot instance. + """ + + def __init__(self, bot: CourageousCometsBot) -> None: + self.bot = bot + + menu = discord.app_commands.ContextMenu( + name="Show user interests", + callback=self.show_keywords, + ) + self.bot.tree.add_command(menu) + + async def show_keywords( + self, + interaction: discord.Interaction, + user: discord.User | discord.Member, + ) -> None: + """ + Allow users to view the most commonly used keywords of a user using a context menu. + + Generates an embed with the most commonly used keywords of a user and sends it to the user. + The embed contains a bar chart of the keywords used by the user. + + Parameters + ---------- + interaction : discord.Interaction + The interaction that triggered the command. + user : discord.User | discord.Member + The user to analyze. + """ + logger.info( + "User %s requested keywords %s for user %s.", + interaction.user.id, + interaction.id, + user.id, + ) + + if self.bot.redis is None: + logger.error( + "Could not answer search request %s due to Redis being unavailable.", + interaction.id, + ) + return await interaction.response.send_message( + "This feature is currently unavailable. Please try again later.", + ephemeral=True, + ) + + if not interaction.guild: + logger.debug( + "Could not answer search request %s due to it being used outside of a guild.", + interaction.id, + ) + return await interaction.response.send_message( + "This command can only be used in a guild.", + ephemeral=True, + ) + + await interaction.response.defer(ephemeral=True, thinking=True) + + tokens = await get_tokens_count( + self.bot.redis, + guild_id=str(interaction.guild.id), + scope=StatisticScope.USER, + ids=[str(user.id)], + ) + + embed = user_keywords.render(user, tokens) + + chart = keywords_bars.render(tokens) + embed.set_image(url=f"attachment://{chart.filename}") + + return await interaction.followup.send(embed=embed, file=chart, ephemeral=True) + + +async def setup(bot: CourageousCometsBot) -> None: + """Load the cog.""" + await bot.add_cog(KeywordsUserContextMenu(bot)) diff --git a/courageous-comets/courageous_comets/cogs/messages.py b/courageous-comets/courageous_comets/cogs/messages.py new file mode 100644 index 0000000..0a4aac6 --- /dev/null +++ b/courageous-comets/courageous_comets/cogs/messages.py @@ -0,0 +1,78 @@ +import logging + +import discord +from discord.ext import commands + +from courageous_comets.client import CourageousCometsBot +from courageous_comets.processing import process_message + +logger = logging.getLogger(__name__) + + +class Messages(commands.Cog): + """ + A cog that listens for messages from discord and forwards them to processing. + + Attributes + ---------- + bot : CourageousCometsBot + The bot instance. + """ + + def __init__(self, bot: CourageousCometsBot) -> None: + self.bot = bot + + @commands.Cog.listener(name="on_message") + async def on_message(self, message: discord.Message) -> None: + """ + When a message is received, forward it to processing. + + Ignore messages that are not in a guild or if the bot is not connected to Redis. + + Parameters + ---------- + message : discord.Message + The message to save. + """ + if not self.bot.redis: + return logger.error( + "Ignoring message %s because the bot is not connected to Redis", + message.id, + ) + + validation_errors = { + "bot": message.author.bot, + "empty": not message.clean_content, + "sync": self.bot.user in message.mentions and "sync" in message.clean_content, + } + + if any(validation_errors.values()): + return logger.debug( + "Ignoring message %s, reason: %s", + message.id, + validation_errors, + ) + + key = await process_message( + message, + redis=self.bot.redis, + vectorizer=self.bot.vectorizer, + ) + + return logger.debug( + "Processed message %s, saved with key %s", + message.id, + key, + ) + + +async def setup(bot: CourageousCometsBot) -> None: + """ + Load the cog. + + Parameters + ---------- + bot : CourageousCometsBot + The bot instance. + """ + await bot.add_cog(Messages(bot)) diff --git a/courageous-comets/courageous_comets/cogs/ping.py b/courageous-comets/courageous_comets/cogs/ping.py new file mode 100644 index 0000000..aa3cdcf --- /dev/null +++ b/courageous-comets/courageous_comets/cogs/ping.py @@ -0,0 +1,27 @@ +import discord +from discord import app_commands +from discord.ext import commands + + +class Ping(commands.Cog): + """ + A cog containing a simple ping command. + + Attributes + ---------- + bot : commands.Bot + The bot instance. + """ + + def __init__(self, bot: commands.Bot) -> None: + self.bot = bot + + @app_commands.command(name="ping") + async def ping(self, interaction: discord.Interaction) -> None: + """Ping the bot.""" + await interaction.response.send_message("Pong!") + + +async def setup(bot: commands.Bot) -> None: + """Load the cog.""" + await bot.add_cog(Ping(bot)) diff --git a/courageous-comets/courageous_comets/cogs/sentiment/__init__.py b/courageous-comets/courageous_comets/cogs/sentiment/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/courageous-comets/courageous_comets/cogs/sentiment/message_context_menu.py b/courageous-comets/courageous_comets/cogs/sentiment/message_context_menu.py new file mode 100644 index 0000000..9568a7f --- /dev/null +++ b/courageous-comets/courageous_comets/cogs/sentiment/message_context_menu.py @@ -0,0 +1,127 @@ +import logging + +import discord +from discord import app_commands +from discord.ext import commands + +from courageous_comets.client import CourageousCometsBot +from courageous_comets.processing import process_message +from courageous_comets.redis.keys import key_schema +from courageous_comets.redis.messages import get_message_sentiment +from courageous_comets.ui.charts import sentiment_bars +from courageous_comets.ui.embeds import message_sentiment +from courageous_comets.ui.views.sentiment import SentimentView + +logger = logging.getLogger(__name__) + + +class SentimentMessageContextMenu(commands.Cog): + """ + A cog that provides sentiment analysis for a message using a context menu. + + Attributes + ---------- + bot : CourageousCometsBot + The bot instance. + """ + + def __init__(self, bot: CourageousCometsBot) -> None: + self.bot = bot + + menu = app_commands.ContextMenu( + name="Show message sentiment", + callback=self.show_message_sentiment, + ) + self.bot.tree.add_command(menu) + + async def show_message_sentiment( + self, + interaction: discord.Interaction, + message: discord.Message, + ) -> None: + """ + Allow users to view the sentiment analysis of a message using a context menu. + + Generates an embed with the sentiment analysis of a message and sends it to the user. + + The embed contains a text description of the sentiment analysis and a bar chart. + A view is attached to the message to allow users to interact with the sentiment analysis. + + Parameters + ---------- + interaction : discord.Interaction + The interaction that triggered the command. + message : discord.Message + The message to analyze. + """ + logger.info( + "User %s requested sentiment analysis results %s for message %s.", + interaction.user.id, + interaction.id, + message.id, + ) + + if self.bot.redis is None: + logger.error( + "Could not answer sentiment request %s due to Redis being unavailable.", + interaction.id, + ) + return await interaction.response.send_message( + "This feature is currently unavailable. Please try again later.", + ephemeral=True, + ) + + if message.guild is None: + logger.debug( + "Could not answer sentiment request %s due to not message being from a guild.", + interaction.id, + ) + return await interaction.response.send_message( + "This feature is only available in guilds.", + ephemeral=True, + ) + + await interaction.response.defer(ephemeral=True, thinking=True) + + key = key_schema.guild_messages( + guild_id=message.guild.id, + message_id=message.id, + ) + + if not await self.bot.redis.exists(key): + logger.debug("Message %s is not previously saved. Processing it.", message.id) + await process_message( + message, + redis=self.bot.redis, + vectorizer=self.bot.vectorizer, + ) + + analysis_result = await get_message_sentiment(key, redis=self.bot.redis) + + if analysis_result is None: + logger.debug("No data found for sentiment request %s.", interaction.id) + return await interaction.followup.send( + "No analysis results were found.", + ephemeral=True, + ) + + embed = message_sentiment.render(analysis_result) + + chart = sentiment_bars.for_message(message.id, analysis_result) + embed.set_image(url=f"attachment://{chart.filename}") + + view = SentimentView(message.author, analysis_result) + + logger.debug("Sending sentiment analysis results for %s.", interaction.id) + + return await interaction.followup.send( + embed=embed, + file=chart, + view=view, + ephemeral=True, + ) + + +async def setup(bot: CourageousCometsBot) -> None: + """Load the cog.""" + await bot.add_cog(SentimentMessageContextMenu(bot)) diff --git a/courageous-comets/courageous_comets/cogs/sentiment/search_command.py b/courageous-comets/courageous_comets/cogs/sentiment/search_command.py new file mode 100644 index 0000000..3f2e89f --- /dev/null +++ b/courageous-comets/courageous_comets/cogs/sentiment/search_command.py @@ -0,0 +1,106 @@ +import logging + +import discord +from discord import app_commands +from discord.ext import commands + +from courageous_comets import preprocessing +from courageous_comets.client import CourageousCometsBot +from courageous_comets.discord.messages import resolve_messages +from courageous_comets.redis.messages import get_messages_by_sentiment_similarity +from courageous_comets.sentiment import calculate_sentiment +from courageous_comets.ui.embeds import search_results + +logger = logging.getLogger(__name__) + + +class SentimentSearchCommand(commands.Cog): + """ + A cog that provides sentiment search using a the /sentiment_search command. + + Attributes + ---------- + bot : CourageousCometsBot + The bot instance. + """ + + def __init__(self, bot: CourageousCometsBot) -> None: + self.bot = bot + + @app_commands.command( + name="sentiment_search", + description="Search for messages with similar sentiment.", + ) + async def search_by_sentiment( + self, + interaction: discord.Interaction, + query: str, + ) -> None: + """ + Allow users to search for messages with similar sentiment using a context menu. + + Parameters + ---------- + interaction : discord.Interaction + The interaction that triggered the command. + query : str + The message to use as a reference for the search + """ + logger.info( + "User %s requested search by sentiment %s using a custom query.", + interaction.user.id, + interaction.id, + ) + + if self.bot.redis is None: + logger.error( + "Could not answer sentiment request %s due to Redis being unavailable.", + interaction.id, + ) + return await interaction.response.send_message( + "This feature is currently unavailable. Please try again later.", + ephemeral=True, + ) + + if interaction.guild is None: + logger.debug( + "Could not answer sentiment request %s due to not being triggered from a guild.", + interaction.id, + ) + return await interaction.response.send_message( + "This feature is only available in guilds.", + ephemeral=True, + ) + + await interaction.response.defer(ephemeral=True, thinking=True) + + prepared_content = preprocessing.process(query) + sentiment = calculate_sentiment(prepared_content) + + messages = await get_messages_by_sentiment_similarity( + self.bot.redis, + guild_id=str(interaction.guild.id), + sentiment=sentiment.compound, + radius=0.1, + limit=5, + ) + + resolved_messages = await resolve_messages(self.bot, messages) + + if not resolved_messages: + logger.debug("No data found for sentiment request %s.", interaction.id) + return await interaction.followup.send( + "No related messages were found.", + ephemeral=True, + ) + + embed = search_results.render(query, resolved_messages) + + logger.debug("Sending sentiment analysis results for %s.", interaction.id) + + return await interaction.followup.send(embed=embed, ephemeral=True) + + +async def setup(bot: CourageousCometsBot) -> None: + """Load the cog.""" + await bot.add_cog(SentimentSearchCommand(bot)) diff --git a/courageous-comets/courageous_comets/cogs/sentiment/search_context_menu.py b/courageous-comets/courageous_comets/cogs/sentiment/search_context_menu.py new file mode 100644 index 0000000..1da3823 --- /dev/null +++ b/courageous-comets/courageous_comets/cogs/sentiment/search_context_menu.py @@ -0,0 +1,135 @@ +import logging + +import discord +from discord import app_commands +from discord.ext import commands + +from courageous_comets.client import CourageousCometsBot +from courageous_comets.discord.messages import resolve_messages +from courageous_comets.processing import process_message +from courageous_comets.redis.keys import key_schema +from courageous_comets.redis.messages import ( + get_message_sentiment, + get_messages_by_sentiment_similarity, +) +from courageous_comets.ui.embeds import search_results + +logger = logging.getLogger(__name__) + + +class SentimentSearchContextMenu(commands.Cog): + """ + A cog that provides sentiment search using a context menu. + + Attributes + ---------- + bot : CourageousCometsBot + The bot instance. + """ + + def __init__(self, bot: CourageousCometsBot) -> None: + self.bot = bot + + menu = app_commands.ContextMenu( + name="Search by sentiment", + callback=self.search_by_sentiment, + ) + self.bot.tree.add_command(menu) + + async def search_by_sentiment( + self, + interaction: discord.Interaction, + message: discord.Message, + ) -> None: + """ + Allow users to search for messages with similar sentiment using a context menu. + + Parameters + ---------- + interaction : discord.Interaction + The interaction that triggered the command. + message : discord.Message + The message to use as a reference for the search. + """ + logger.info( + "User %s requested search by sentiment %s for message %s.", + interaction.user.id, + interaction.id, + message.id, + ) + + if self.bot.redis is None: + logger.error( + "Could not answer sentiment request %s due to Redis being unavailable.", + interaction.id, + ) + return await interaction.response.send_message( + "This feature is currently unavailable. Please try again later.", + ephemeral=True, + ) + + if message.guild is None: + logger.debug( + "Could not answer sentiment request %s due to message not being from a guild.", + interaction.id, + ) + return await interaction.response.send_message( + "This feature is only available in guilds.", + ephemeral=True, + ) + + await interaction.response.defer(ephemeral=True, thinking=True) + + key = key_schema.guild_messages( + guild_id=message.guild.id, + message_id=message.id, + ) + + if not await self.bot.redis.exists(key): + logger.debug("Message %s is not previously saved. Processing it.", message.id) + await process_message( + message, + redis=self.bot.redis, + vectorizer=self.bot.vectorizer, + ) + + analysis_result = await get_message_sentiment(key, redis=self.bot.redis) + + if analysis_result is None: + logger.debug("No data found for sentiment request %s.", interaction.id) + return await interaction.followup.send( + "No related messages were found.", + ephemeral=True, + ) + + messages = await get_messages_by_sentiment_similarity( + self.bot.redis, + guild_id=str(message.guild.id), + sentiment=analysis_result.compound, + radius=0.1, + limit=6, + ) + + resolved_messages = [ + resolved_message + for resolved_message in await resolve_messages(self.bot, messages) + if resolved_message.id != message.id + ] + + if not resolved_messages: + logger.debug("No search results found for sentiment request %s.", interaction.id) + return await interaction.followup.send( + "No related messages were found.", + ephemeral=True, + ) + + embed = search_results.render(message.clean_content, resolved_messages) + + logger.debug("Sending sentiment analysis results for %s.", interaction.id) + + return await interaction.followup.send(embed=embed, ephemeral=True) + + +async def setup(bot: CourageousCometsBot) -> None: + """Load the cog.""" + await bot.add_cog(SentimentSearchContextMenu(bot)) diff --git a/courageous-comets/courageous_comets/cogs/sentiment/user_context_menu.py b/courageous-comets/courageous_comets/cogs/sentiment/user_context_menu.py new file mode 100644 index 0000000..876a843 --- /dev/null +++ b/courageous-comets/courageous_comets/cogs/sentiment/user_context_menu.py @@ -0,0 +1,121 @@ +import logging + +import discord +from discord import app_commands +from discord.ext import commands + +from courageous_comets.client import CourageousCometsBot +from courageous_comets.enums import StatisticScope +from courageous_comets.redis.messages import get_average_sentiment +from courageous_comets.ui.charts import sentiment_bars +from courageous_comets.ui.embeds import user_sentiment +from courageous_comets.ui.views.sentiment import SentimentView +from courageous_comets.utils import contextmenu + +logger = logging.getLogger(__name__) + + +class SentimentUserContextMenu(commands.Cog): + """ + A cog that provides sentiment analysis for a user using a context menu. + + Attributes + ---------- + bot : CourageousCometsBot + The bot instance. + """ + + def __init__(self, bot: CourageousCometsBot) -> None: + self.bot = bot + + menu = app_commands.ContextMenu( + name="Show user sentiment", + callback=self.show_user_sentiment, + ) + self.bot.tree.add_command(menu) + + @contextmenu(name="Show user sentiment") + async def show_user_sentiment( + self, + interaction: discord.Interaction, + user: discord.User | discord.Member, + ) -> None: + """ + Allow users to view the sentiment analysis of a user using a context menu. + + Generates an embed with the sentiment analysis of a user and sends it to the user. + The embed contains a line chart of the sentiment of a user over time. + A view is attached to the message to allow users to interact with the sentiment analysis. + + Parameters + ---------- + interaction : discord.Interaction + The interaction that triggered the command. + user : discord.User | discord.Member + The user to analyze. + """ + logger.info( + "User %s requested sentiment analysis results %s for user %s.", + interaction.user.id, + interaction.id, + user.id, + ) + + if self.bot.redis is None: + logger.error( + "Could not answer sentiment request %s due to Redis being unavailable.", + interaction.id, + ) + return await interaction.response.send_message( + "This feature is currently unavailable. Please try again later.", + ephemeral=True, + ) + + if interaction.guild is None: + logger.debug( + "Could not answer sentiment request %s due to user not being from a guild.", + interaction.id, + ) + return await interaction.response.send_message( + "This feature is only available in guilds.", + ephemeral=True, + ) + + await interaction.response.defer(ephemeral=True, thinking=True) + + sentiment_results = await get_average_sentiment( + redis=self.bot.redis, + guild_id=str(interaction.guild.id), + ids=[str(user.id)], + scope=StatisticScope.USER, + ) + + if not sentiment_results: + logger.debug("No data found for sentiment request %s.", interaction.id) + await interaction.followup.send( + f"No sentiment data found for {user.mention}.", + ephemeral=True, + ) + + average_sentiment = sentiment_results[0] + + embed = user_sentiment.render(user, average_sentiment) + + chart = sentiment_bars.for_user(average_sentiment) + embed.set_image(url=f"attachment://{chart.filename}") + + view = SentimentView(user, average_sentiment) + + logger.debug("Sending sentiment analysis results for %s.", interaction.id) + + return await interaction.followup.send( + embed=embed, + file=chart, + view=view, + ephemeral=True, + ) + + +async def setup(bot: CourageousCometsBot) -> None: + """Load the cog.""" + await bot.add_cog(SentimentUserContextMenu(bot)) diff --git a/courageous-comets/courageous_comets/discord/__init__.py b/courageous-comets/courageous_comets/discord/__init__.py new file mode 100644 index 0000000..765138a --- /dev/null +++ b/courageous-comets/courageous_comets/discord/__init__.py @@ -0,0 +1,8 @@ +import asyncio + +from courageous_comets import settings + +# Limit the number of concurrent requests to the Discord API. +SEMAPHORE = asyncio.Semaphore(settings.DISCORD_API_CONCURRENCY) + +__all__ = ["SEMAPHORE"] diff --git a/courageous-comets/courageous_comets/discord/messages.py b/courageous-comets/courageous_comets/discord/messages.py new file mode 100644 index 0000000..d6efc90 --- /dev/null +++ b/courageous-comets/courageous_comets/discord/messages.py @@ -0,0 +1,87 @@ +import asyncio +import logging + +import discord +from asyncache import cached +from cachetools import LRUCache + +from courageous_comets import models +from courageous_comets.discord import SEMAPHORE + +logger = logging.getLogger(__name__) + + +async def resolve_messages( + client: discord.Client, + messages: list[models.Message], +) -> list[discord.Message]: + """ + Try and resolve a list of messages from Redis to Discord messages. + + Messages that could not be resolved are not included in the returned list. Messages that have no + content are also filtered out. + + Parameters + ---------- + client : discord.Client + The discord client to use to fetch the messages. + messages : list[models.Message] + The messages to resolve. + + Returns + ------- + list[discord.Message] + A list of `discord.Message` instances that were found. + """ + requests = [ + get_message(client, int(message.channel_id), int(message.message_id)) + for message in messages + ] + return [ + message + for message in await asyncio.gather(*requests) + if message is not None and message.clean_content + ] + + +@cached( + LRUCache(maxsize=256), + key=lambda _, channel_id, message_id: f"{channel_id}-{message_id}", +) +async def get_message( + client: discord.Client, + channel_id: int, + message_id: int, +) -> discord.Message | None: + """ + Try and fetch a message from Discord given a `channel_id` and `message_id`. + + Uses a cache to avoid fetching the same message multiple times. + + Parameters + ---------- + client : discord.Client + The discord client to use to fetch the message. + channel_id : int + The channel id of the message. + message_id : int + The message id of the message. + + Returns + ------- + discord.Message | None + A `discord.Message` instance if the message was found, else `None`. + """ + channel = client.get_channel(channel_id) + + if not channel or not isinstance(channel, discord.TextChannel): + return None + + async with SEMAPHORE: + logging.debug("Fetching message %s from channel %s", message_id, channel_id) + resolved_message = await channel.fetch_message(message_id) + + if not resolved_message: + return None + + return resolved_message diff --git a/courageous-comets/courageous_comets/enums.py b/courageous-comets/courageous_comets/enums.py new file mode 100644 index 0000000..567dfd4 --- /dev/null +++ b/courageous-comets/courageous_comets/enums.py @@ -0,0 +1,17 @@ +from enum import IntEnum, StrEnum + + +class StatisticScope(StrEnum): + """Scope of statistics results to fetch.""" + + GUILD = "guild_id" + CHANNEL = "channel_id" + USER = "user_id" + + +class Duration(IntEnum): + """Number of seconds in time durations.""" + + minute = 60 + hourly = 60 * 60 + daily = 60 * 60 * 24 diff --git a/courageous-comets/courageous_comets/exceptions.py b/courageous-comets/courageous_comets/exceptions.py new file mode 100644 index 0000000..7c6cc3e --- /dev/null +++ b/courageous-comets/courageous_comets/exceptions.py @@ -0,0 +1,44 @@ +from dataclasses import dataclass + + +class CourageousCometsError(Exception): + """Base class for all Courageous Comets exceptions.""" + + +class AuthenticationError(CourageousCometsError): + """Raised when authentication with an external service fails.""" + + +@dataclass(kw_only=True) +class ConfigurationValueError[T](CourageousCometsError): + """ + Raised when a configuration value is invalid. + + Attributes + ---------- + key : str + The configuration key. + value : T, optional + The invalid value provided. + reason : str + The reason why the value is considered invalid. + """ + + key: str + value: T | None + reason: str + + def __str__(self) -> str: + return f"Invalid value '{self.value}' for configuration key '{self.key}': {self.reason}" + + +class DatabaseConnectionError(CourageousCometsError): + """Raised when a connection to the database cannot be established.""" + + +class NltkInitializationError(CourageousCometsError): + """Raised when the application fails to download the NLTK dependencies on startup.""" + + +class HuggingFaceModelDownloadError(CourageousCometsError): + """Raised when the application fails to download a huggingface model on startup.""" diff --git a/courageous-comets/courageous_comets/models.py b/courageous-comets/courageous_comets/models.py new file mode 100644 index 0000000..417344b --- /dev/null +++ b/courageous-comets/courageous_comets/models.py @@ -0,0 +1,123 @@ +import datetime +from typing import Annotated + +import pydantic +from pydantic import AliasChoices, Field, PlainSerializer + +UnixTimestamp = Annotated[ + datetime.datetime, + PlainSerializer(lambda t: t.timestamp(), return_type=float), +] + + +class BaseModel(pydantic.BaseModel): + """Base for model definitions.""" + + # Redis tags need to be strings, thus, coerce integers passed during creation to string + model_config = pydantic.ConfigDict( + extra="ignore", + coerce_numbers_to_str=True, + from_attributes=True, + ) + + def __hash__( + self, + ) -> int: # make hashable BaseModel subclass for async_lru.alru_cache decorator + return hash((type(self), *tuple(self.__dict__.values()))) + + +class Message(BaseModel): + """ + Redis model of a Discord message. + + Attributes + ---------- + message_id : str + The ID of the message. + channel_id : str + The ID of the channel the message was sent in. + guild_id : str + The ID of the guild the message was sent in. + timestamp : UnixTimestamp + The timestamp when the message was sent. + user_id : str + The ID of the user who sent the message. + """ + + message_id: str + channel_id: str + guild_id: str + timestamp: UnixTimestamp + user_id: str + + +class SentimentResult(BaseModel): + """ + Result of sentiment analysis. + + Attributes + ---------- + neg : float + The negative sentiment score. + neu : float + The neutral sentiment score. + pos : float + The positive sentiment score. + compound : float + The compound sentiment score. + """ + + neg: float = Field( + ..., + serialization_alias="sentiment_neg", + validation_alias=AliasChoices("neg", "sentiment_neg"), + ) + neu: float = Field( + ..., + serialization_alias="sentiment_neu", + validation_alias=AliasChoices("neu", "sentiment_neu"), + ) + pos: float = Field( + ..., + serialization_alias="sentiment_pos", + validation_alias=AliasChoices("pos", "sentiment_pos"), + ) + compound: float = Field( + ..., + serialization_alias="sentiment_compound", + validation_alias=AliasChoices("compound", "sentiment_compound"), + ) + + +class MessageAnalysis(Message): + """ + Analysis of a discord message. + + Attributes + ---------- + sentiment: courageous_comets.models.SentimentResult + The result of sentiment analysis on the message. + tokens: dict[str, int] + Mapping of token to number of times it appears in message. + embedding : bytes + The embedding vector of the content. + """ + + sentiment: SentimentResult + tokens: dict[str, int] + embedding: bytes + + +class MessageFrequency(BaseModel): + """Number of messages sent over a duration. + + Attributes + ---------- + timestamp: UnixTimestamp + The timestamp when the messages were sent. + nb_messages: int + The number of messages sent at `timestamp` + """ + + timestamp: UnixTimestamp + num_messages: int diff --git a/courageous-comets/courageous_comets/nltk/__init__.py b/courageous-comets/courageous_comets/nltk/__init__.py new file mode 100644 index 0000000..3571706 --- /dev/null +++ b/courageous-comets/courageous_comets/nltk/__init__.py @@ -0,0 +1,3 @@ +from .helpers import init_nltk + +__all__ = ["init_nltk"] diff --git a/courageous-comets/courageous_comets/nltk/helpers.py b/courageous-comets/courageous_comets/nltk/helpers.py new file mode 100644 index 0000000..4dd9bd2 --- /dev/null +++ b/courageous-comets/courageous_comets/nltk/helpers.py @@ -0,0 +1,48 @@ +import asyncio +import logging +from pathlib import Path + +import nltk + +from courageous_comets import exceptions, settings + +logger = logging.getLogger(__name__) + + +async def download_nltk_resource(resource: str, semaphore: asyncio.Semaphore) -> None: + """Download an NLTK resource to the specified directory.""" + async with semaphore: + logger.debug("Downloading NLTK resource '%s'...", resource) + try: + await asyncio.to_thread( + nltk.download, + resource, + download_dir=settings.NLTK_DATA_DIR, + quiet=True, + raise_on_error=True, + ) + except ValueError as e: + message = f"Invalid NLTK resource '{resource}'" + raise exceptions.NltkInitializationError(message) from e + + +async def init_nltk(resources: list[str]) -> None: + """ + Ensure all required NLTK resources are downloaded. + + Downloads the resources specified in the bot configuration file. + """ + if not any(resources): + logger.debug("No NLTK resources to download") + return + + # Create the NLTK data directory if it does not exist to avoid a race condition when running + # multiple download tasks concurrently + Path(settings.NLTK_DATA_DIR).mkdir(parents=True, exist_ok=True) + + semaphore = asyncio.Semaphore(settings.NLTK_DOWNLOAD_CONCURRENCY) + download_tasks = [download_nltk_resource(resource, semaphore) for resource in resources] + + await asyncio.gather(*download_tasks) + + logger.debug("NLTK resources downloaded") diff --git a/courageous-comets/courageous_comets/preprocessing.py b/courageous-comets/courageous_comets/preprocessing.py new file mode 100644 index 0000000..80f6f95 --- /dev/null +++ b/courageous-comets/courageous_comets/preprocessing.py @@ -0,0 +1,154 @@ +import re +import string +from collections.abc import Callable +from functools import partial + +import contractions +from unidecode import unidecode + +from courageous_comets import settings + +Processor = Callable[[str], str] + + +def drop_code_blocks(text: str) -> str: + """ + Remove code blocks from the given text. + + Parameters + ---------- + text : str + The text to process. + + Returns + ------- + str + The text with code blocks removed. + """ + return re.sub(r"```.*?```", "", text, flags=re.DOTALL) + + +def drop_extra_whitespace(text: str) -> str: + """ + Remove extra whitespace from the given text. + + Parameters + ---------- + text : str + The text to process. + + Returns + ------- + str + The text with extra whitespace removed. + """ + return re.sub(r"\s+", " ", text.strip()) + + +def drop_links(text: str) -> str: + """ + Remove links from the given text. + + Parameters + ---------- + text : str + The text to process. + + Returns + ------- + str + The text with links removed. + """ + return re.sub(r"http\S+", "", text) + + +def drop_punctuation(text: str) -> str: + """ + Remove punctuation from the given text. + + Parameters + ---------- + text : str + The text to process. + + Returns + ------- + str + The text without punctuation. + """ + return text.translate(str.maketrans("", "", string.punctuation)) + + +def drop_very_long_words(text: str, max_length: int) -> str: + """ + Remove very long words from the given text. + + Parameters + ---------- + text : str + The text to process. + max_length : int + The maximum length of a word to keep. + + Returns + ------- + str + The text with very long words removed. + """ + return re.sub(r"\S{%d,}" % max_length, "", text) + + +def truncate(text: str, max_length: int) -> str: + """ + Truncate the given text to the specified length. + + Parameters + ---------- + text : str + The text to truncate. + max_length : int + The maximum length of the text. + + Returns + ------- + str + The truncated text. + """ + return text[:max_length] + + +# Steps are executed in order +PROCESSORS: list[Processor] = [ + drop_code_blocks, + drop_links, + unidecode, + contractions.fix, # type: ignore + drop_punctuation, + partial(drop_very_long_words, max_length=settings.PREPROCESSING_MAX_WORD_LENGTH), + drop_extra_whitespace, + partial(truncate, max_length=settings.PREPROCESSING_MESSAGE_TRUNCATE_LENGTH), +] + + +def process(text: str, processors: list[Processor] = PROCESSORS) -> str: + """ + Process the text using all available processors. + + Parameters + ---------- + text : str + The text to process. + processors : list[Processor], optional + The processors to use, by default uses a predefined set of processors. + + Returns + ------- + str + The processed text. + """ + result = text + + for processor in processors: + text = processor(result) + + return result diff --git a/courageous-comets/courageous_comets/processing.py b/courageous-comets/courageous_comets/processing.py new file mode 100644 index 0000000..547ec58 --- /dev/null +++ b/courageous-comets/courageous_comets/processing.py @@ -0,0 +1,79 @@ +import asyncio +import logging + +import discord +from redis.asyncio import Redis + +from courageous_comets import preprocessing +from courageous_comets.models import MessageAnalysis +from courageous_comets.redis import messages +from courageous_comets.sentiment import calculate_sentiment +from courageous_comets.vectorizer import Vectorizer +from courageous_comets.words import tokenize_sentence, word_frequency + +logger = logging.getLogger(__name__) + + +async def process_message( + message: discord.Message, + *, + redis: Redis, + vectorizer: Vectorizer, +) -> str | None: + """ + Process a message and save it to Redis. + + The following steps are taken to process the message: + + - Clean the message content. + - Encode the message content. + - Calculate the sentiment of the message. + - Tokenize the message content. + + Parameters + ---------- + message : discord.Message + The message to process. + redis : Redis + The Redis connection. + vectorizer : Vectorizer + The vectorizer to use for encoding the message. + + Returns + ------- + str | None + The id of the saved message or None if the message was ignored + """ + if not message.guild: + return logger.debug( + "Ignoring message %s because it's not in a guild", + message.id, + ) + + text = preprocessing.process(message.clean_content) + + if not text: + return logger.debug( + "Ignoring message %s because it's empty after processing", + message.id, + ) + + embedding, sentiment, tokens = await asyncio.gather( + vectorizer.aencode(text), + asyncio.to_thread(calculate_sentiment, text), + asyncio.to_thread(tokenize_sentence, text), + ) + + return await messages.save_message( + redis, + MessageAnalysis( + user_id=str(message.author.id), + message_id=str(message.id), + channel_id=str(message.channel.id), + guild_id=str(message.guild.id), + timestamp=message.created_at, + embedding=embedding, + sentiment=sentiment, + tokens=word_frequency(tokens), + ), + ) diff --git a/courageous-comets/courageous_comets/redis/__init__.py b/courageous-comets/courageous_comets/redis/__init__.py new file mode 100644 index 0000000..ca55dc9 --- /dev/null +++ b/courageous-comets/courageous_comets/redis/__init__.py @@ -0,0 +1,3 @@ +from .helpers import init_redis + +__all__ = ["init_redis"] diff --git a/courageous-comets/courageous_comets/redis/helpers.py b/courageous-comets/courageous_comets/redis/helpers.py new file mode 100644 index 0000000..3a95305 --- /dev/null +++ b/courageous-comets/courageous_comets/redis/helpers.py @@ -0,0 +1,68 @@ +import logging + +import redis.asyncio as redis +from redisvl.index import AsyncSearchIndex + +from courageous_comets import exceptions, settings +from courageous_comets.redis import schema + +logger = logging.getLogger(__name__) + + +async def create_indexes(redis: redis.Redis) -> None: + """Create search indexes on Redis.""" + logger.debug("Creating indexes on redis...") + + message_index = AsyncSearchIndex.from_dict(schema.MESSAGE_SCHEMA) + message_index.set_client(redis) + + await message_index.create(overwrite=True) + + logger.debug("Created indexes on Redis") + + +async def init_redis() -> redis.Redis: + """ + Initialize the Redis connection. + + Returns + ------- + redis.asyncio.Redis + The Redis connection instance. + + Raises + ------ + courageous_comets.exceptions.AuthenticationError + If the Redis password is incorrect. + courageous_comets.exceptions.DatabaseConnectionError + If the connection to Redis cannot be established. + """ + logger.debug("Connecting to Redis...") + + instance = redis.Redis( + host=settings.REDIS_HOST, + port=settings.REDIS_PORT, + password=settings.REDIS_PASSWORD, + decode_responses=True, + ) + + try: + await instance.ping() + except redis.AuthenticationError as e: + message = "Redis authentication failed. Check the REDIS_PASSWORD environment variable." + raise exceptions.AuthenticationError(message) from e + except redis.RedisError as e: + message = f"Could not connect to Redis at {settings.REDIS_HOST}:{settings.REDIS_PORT}" + raise exceptions.DatabaseConnectionError(message) from e + + logger.info( + "Connected to Redis at %s:%s", + settings.REDIS_HOST, + settings.REDIS_PORT, + ) + + await create_indexes(instance) + + logger.info("Redis initialization complete") + + return instance diff --git a/courageous-comets/courageous_comets/redis/keys.py b/courageous-comets/courageous_comets/redis/keys.py new file mode 100644 index 0000000..6fc6ae6 --- /dev/null +++ b/courageous-comets/courageous_comets/redis/keys.py @@ -0,0 +1,61 @@ +from collections.abc import Callable +from functools import wraps +from typing import ParamSpec + +from courageous_comets import settings + +P = ParamSpec("P") +Prefixable = Callable[P, str] + + +def prefix_key(func: Prefixable[P]) -> Prefixable[P]: + """ + Prefix the key returned by the decorated function with the Redis key prefix. + + This function is intended to be used as a decorator. + + Parameters + ---------- + func : courageous_comets.redis.keys.Prefixable[P] + The function to decorate. + + Returns + ------- + courageous_comets.redis.keys.Prefixable[P] + The decorated function. + """ + + @wraps(func) + def prefixed_method(*args: P.args, **kwargs: P.kwargs) -> str: + key = func(*args, **kwargs) + return f"{settings.REDIS_KEYS_PREFIX}:{key}" + + return prefixed_method + + +class KeySchema: + """ + A class to generate key names for Redis data structures. + + This class contains a reference to all possible key names used + by the application. + """ + + @prefix_key + def guild_messages(self, *, guild_id: int, message_id: int) -> str: + """Key to messages for a Discord guild. + + Redis type: hash + """ + return f"messages:{guild_id}:{message_id}" + + @prefix_key + def guild_message_tokens(self, guild_id: int) -> str: + """Key to message tokens for a Discord guild. + + Redis type: hash + """ + return f"messages:tokens:{guild_id}" + + +key_schema = KeySchema() diff --git a/courageous-comets/courageous_comets/redis/messages.py b/courageous-comets/courageous_comets/redis/messages.py new file mode 100644 index 0000000..e28a620 --- /dev/null +++ b/courageous-comets/courageous_comets/redis/messages.py @@ -0,0 +1,535 @@ +import datetime +import itertools +import json +import logging +from collections import Counter + +import redis.commands.search.aggregation as aggregations +from redis.asyncio import Redis +from redis.commands.search import AsyncSearch, reducers +from redisvl.index import AsyncSearchIndex +from redisvl.query import FilterQuery, VectorQuery +from redisvl.query.filter import FilterExpression, Num, Tag +from redisvl.query.query import BaseQuery + +from courageous_comets import models, settings +from courageous_comets.enums import Duration, StatisticScope +from courageous_comets.redis import schema +from courageous_comets.redis.keys import key_schema + +logger = logging.getLogger(__name__) + +# List of courageous_comets.models.Message return fields used acrosss queries +# that return a list of courageous_comets.models.Message +RETURN_FIELDS = ["message_id", "user_id", "channel_id", "guild_id", "timestamp"] + + +def _get_raw_index(redis: Redis) -> AsyncSearch: + """Get the raw messages index on Redis.""" + index = AsyncSearchIndex.from_dict(schema.MESSAGE_SCHEMA) + return redis.ft(index.schema.index.name) + + +async def _get_messages_from_query( + redis: Redis, + query: BaseQuery, +) -> list[models.Message]: + """Get a list of messages from Redis query. + + Assumes the fields returned in the query correspond to the attributes + of the courageous_comets.models.Message. + + Parameters + ---------- + redis: redis.Redis + The Redis connection instance. + query: redisvl.query.query.BaseQuery + The query to run on Redis. + + Returns + ------- + courageous_comets.models.Message + The list of messages from the query. + """ + index = AsyncSearchIndex.from_dict(schema.MESSAGE_SCHEMA) + index.set_client(redis) + + results = await index.search( + query.query.sort_by("timestamp", asc=False), + query.params, + ) + + return [models.Message.model_validate(doc) for doc in results.docs if results.total] + + +def build_search_scope( + guild_id: str, + ids: list[str] | None, + scope: StatisticScope, +) -> FilterExpression: + """ + Build a filter expression based on the given id and specified scope. + + Parameters + ---------- + guild_id: str + The ID of the guild to make the search. + ids : list[str] | None + The additional IDs to limit the search to. + scope : courageous_comets.enums.StatisticScope + The scope of the additional IDs. + + Notes + ----- + If the scope of the search is courageous_comets.enums.StatisticScope.GUILD, + the `ids` are ignored. + + Returns + ------- + redisvl.query.FilterExpression + The redis filter expression for the specified scope. + """ + search_scope = Tag(StatisticScope.GUILD) == guild_id + + # Ignore the other IDs as this would imply searching across multiple scopes. + if scope == StatisticScope.GUILD or not ids: + return search_scope + + return search_scope & (Tag(scope) == ids) + + +async def save_message( + redis: Redis, + message: models.MessageAnalysis, +) -> str: + """Save a message on Redis. + + Parameters + ---------- + redis : redis.Redis + The Redis connection instance. + message : courageous_comets.models.MessageAnalysis + The message to save. + + Returns + ------- + str + The key to the data on Redis. + """ + payload = { + "message_id": message.message_id, + "channel_id": message.channel_id, + "guild_id": message.guild_id, + "timestamp": message.timestamp.timestamp(), + "user_id": message.user_id, + "sentiment_neg": message.sentiment.neg, + "sentiment_neu": message.sentiment.neu, + "sentiment_pos": message.sentiment.pos, + "sentiment_compound": message.sentiment.compound, + "embedding": message.embedding, + "tokens": json.dumps(message.tokens), + } + key = key_schema.guild_messages( + guild_id=int(message.guild_id), + message_id=int(message.message_id), + ) + await redis.hset(key, mapping=payload) # type: ignore + return key + + +async def get_message_sentiment( + key: str, + *, + redis: Redis, +) -> models.SentimentResult | None: + """ + Get the sentiment of message from the database given its key. + + Parameters + ---------- + key: str + The key of the message to fetch. + redis : redis.Redis + The Redis connection instance. + + Returns + ------- + courageous_comets.models.SentimentResult | None + The sentiment analysis result if found, else None. + """ + fields = ["sentiment_neg", "sentiment_neu", "sentiment_pos", "sentiment_compound"] + + if not redis.exists(key): + return None + + data = await redis.hmget(key, fields) # type: ignore + + has_all_fields = len(data) == len(fields) + any_none = any(value is None for value in data) + + if not has_all_fields or any_none: + logger.warning("Missing sentiment analysis data for message %s.", key) + return None + + return models.SentimentResult.model_validate( + {key: float(value) for key, value in zip(fields, data, strict=True)}, + ) + + +async def get_recent_messages( + redis: Redis, + *, + guild_id: str, + ids: list[str] | None = None, + scope: StatisticScope = StatisticScope.CHANNEL, + limit: int = settings.QUERY_LIMIT, +) -> list[models.Message]: + """ + Get the most recent `limit` messages. + + Parameters + ---------- + redis : redis.Redis + The Redis connection instance. + guild_id: str + The ID of the guild to make the search. + ids : list[str] + Optional list of IDs to search for. + scope : courageous_comets.enums.StatisticScope + The scope of additional IDs (default: courageous_comets.enums.StatisticScope.CHANNEL). + Ignored it is equal to courageous_comets.enums.StatisticScope.GUILD, + limit : int + The number of messages to fetch (default: courageous_comets.settings.QUERY_LIMIT). + + Returns + ------- + list[courageous_comets.models.Message] + The list of recent messages. + """ + search_scope = build_search_scope(guild_id, ids, scope) + + query = FilterQuery( + return_fields=RETURN_FIELDS, + filter_expression=search_scope, + num_results=limit, + ) + + return await _get_messages_from_query(redis, query) + + +async def get_messages_by_semantics_similarity( # noqa: PLR0913 + redis: Redis, + *, + guild_id: str, + embedding: bytes, + ids: list[str] | None = None, + scope: StatisticScope = StatisticScope.CHANNEL, + limit: int = settings.QUERY_LIMIT, +) -> list[models.Message]: + """ + Get the messages with similar semantics to the provided message. + + Parameters + ---------- + redis : redis.Redis + The Redis connection instance. + guild_id: str + The ID of the guild to make the search. + embedding: bytes + The vector embedding of the message. + ids : list[str] | None + Optional list of IDs to search for. + scope : courageous_comets.enums.StatisticScope + The scope of additional IDs (default: courageous_comets.enums.StatisticScopeEnum.CHANNEL). + Ignored if it is set to courageous_comets.enums.StatisticScope.GUILD. + limit : int + The number of similar messages to fetch (default: courageous_comets.settings.QUERY_LIMIT). + + Returns + ------- + list[courageous_comets.models.Message] + The messages that are semantically similar + """ + search_scope = build_search_scope(guild_id, ids, scope) + + query = VectorQuery( + vector=embedding, + vector_field_name="embedding", + return_fields=RETURN_FIELDS, + filter_expression=search_scope, + num_results=limit, + ) + + return await _get_messages_from_query(redis, query) + + +async def get_messages_by_sentiment_similarity( # noqa: PLR0913 + redis: Redis, + *, + guild_id: str, + sentiment: float, + radius: float, + ids: list[str] | None = None, + scope: StatisticScope = StatisticScope.CHANNEL, + limit: int = settings.QUERY_LIMIT, +) -> list[models.Message]: + """ + Get the messages with similar sentiment analysis. + + Parameters + ---------- + redis : redis.Redis + The Redis connection instance. + guild_id: str + The ID of the guild to make the search. + sentiment : float + The sentiment analayis result of message. + radius: float + The distance threshold of the search. + ids : list[str] | None + Optional list of IDs to search for. + scope : courageous_comets.enums.StatisticScope + The scope of additional IDs (default: courageous_comets.enums.StatisticScopeEnum.CHANNEL). + Ignored if it is set to courageous_comets.enums.StatisticScope.GUILD. + limit : int + The number of similar messages to fetch (default: settings.PAGE_SIZE). + + Returns + ------- + list[courageous_comets.models.Message] + The messages that are sentimentally similar. + """ + search_scope = build_search_scope(guild_id, ids, scope) + + # Define lower and upper bounds for the sentiment compound score + low = Num("sentiment_compound") >= sentiment - radius # type: ignore + high = Num("sentiment_compound") <= sentiment + radius # type: ignore + + filter_expression = search_scope & low & high + + query = FilterQuery( + return_fields=RETURN_FIELDS, + filter_expression=filter_expression, + num_results=limit, + ) + + return await _get_messages_from_query(redis, query) + + +async def get_tokens_count( + redis: Redis, + *, + guild_id: str, + ids: list[str] | None = None, + scope: StatisticScope = StatisticScope.CHANNEL, + limit: int = settings.QUERY_LIMIT, +) -> Counter[str]: + """ + Get the count of tokens across messages. + + Parameters + ---------- + redis: redis.Redis + The Redis connection instance. + guild_id: str + The ID of the guild to make the search + ids: list[strr] + Optional list of IDs to search for. + scope : courageous_comets.enums.StatisticScope + The scope of additional IDs (default: courageous_comets.enums.StatisticScope.CHANNEL). + Ignored it is equal to courageous_comets.enums.StatisticScope.GUILD, + limit : int + The number of messages to fetch (default: courageous_comets.settings.QUERY_LIMIT). + + Returns + ------- + collections.Counter + Mapping of each token to its count. + """ + search_scope = build_search_scope(guild_id, ids, scope) + index = AsyncSearchIndex.from_dict(schema.MESSAGE_SCHEMA) + index.set_client(redis) + + query = FilterQuery( + return_fields=["tokens"], + filter_expression=search_scope, + num_results=limit, + ) + + results = await index.search( + query.query.sort_by("timestamp", asc=False), + query.params, + ) + + counter = Counter() + tokens: list[dict[str, int]] = [json.loads(doc.tokens) for doc in results.docs if results.total] + + for token in tokens: + counter.update(token) + + return counter + + +def _calculate_duration_range(duration: Duration) -> tuple[float, float]: + """Calculate the lower and upper bounds of a duration. + + Parameters + ---------- + duration: courageous_comets.enums.Duration + The duration to be used for aggregation. + + Returns + ------- + tuple[float, float] + The lower and upper bounds of the duration as UNIX timestamps. + """ + upper = datetime.datetime.now(datetime.UTC) + match duration: + case Duration.minute: + lower = upper - datetime.timedelta(minutes=60) + case Duration.hourly: + lower = upper - datetime.timedelta(hours=24) + case Duration.daily: + lower = upper - datetime.timedelta(days=7) + case _: + error_message = f"Unhandled duration: {duration}" + raise ValueError(error_message) + + return (lower.timestamp(), upper.timestamp()) + + +async def get_messages_frequency( # noqa: PLR0913 + redis: Redis, + *, + guild_id: str, + ids: list[str] | None = None, + scope: StatisticScope = StatisticScope.CHANNEL, + limit: int = settings.QUERY_LIMIT, + duration: Duration = Duration.hourly, +) -> list[models.MessageFrequency]: + """ + Get the rate of messages over an interval. + + Parameters + ---------- + redis : Redis + The Redis connection instance. + guild_id : str + The ID of the guild to make the search. + ids : list[str], optional + Optional list of IDs to search for. Defaults to None. + scope : StatisticScope, optional + The scope of additional IDs (default: StatisticScope.CHANNEL). + Ignored if it is equal to StatisticScope.GUILD. + duration : Duration, optional + The duration over which to make the aggregation (default: Duration.HOUR). + limit : int, optional + The number of messages to aggregate over (default: settings.QUERY_LIMIT). + + Returns + ------- + list[models.MessageFrequency] + A list of message frequency at different timestamps. + """ + search_scope = build_search_scope(guild_id, ids, scope) + lower_timestamp, upper_timestamp = _calculate_duration_range(duration) + filter_expression = ( + search_scope + & (Num("timestamp") >= lower_timestamp) # type: ignore + & (Num("timestamp") <= upper_timestamp) # type: ignore + ) + index = _get_raw_index(redis) + + # Define a reducer to count distinct message IDs and alias the result as "num_messages" + reducer = reducers.count_distinct("@message_id").alias("num_messages") + + # Build the aggregation query + query = ( + aggregations.AggregateRequest(str(filter_expression)) + .limit( + 0, + limit, + ) + # Create a new property `timestamp` rounded to the start of the interval + .apply( + timestamp=f"floor(@timestamp) - (floor(@timestamp) % {duration.value})", + ) + # Group results by interval using the new `timestamp` property + .group_by(["@timestamp"], reducer) + # Sort results by the timestamp and return at most 60 entries. We never + # get more than 60 groups because the smallest time division is 60 + # minutes according to our duration values. + .sort_by(aggregations.Asc("@timestamp"), max=60) # type: ignore + ) + + # Execute the aggregation query on the index + results = await index.aggregate(query) # type: ignore + + # Deserialize all rows as dictionaries. Each row is a flat list of key-value pairs. + return [ + models.MessageFrequency.model_validate_strings(dict(itertools.batched(row, 2))) + for row in results.rows + ] + + +async def get_average_sentiment( + redis: Redis, + *, + guild_id: str, + ids: list[str] | None = None, + scope: StatisticScope = StatisticScope.CHANNEL, + limit: int = settings.QUERY_LIMIT, +) -> list[models.SentimentResult]: + """ + Get the average sentiment of messages for the given ids and scope. + + Parameters + ---------- + redis : Redis + The Redis connection instance. + guild_id : str + The ID of the guild to make the search. + ids : list[str], optional + Optional list of IDs to search for. Defaults to None. + scope : StatisticScope, optional + The scope of additional IDs (default: StatisticScope.CHANNEL). + Ignored if it is equal to StatisticScope.GUILD. + limit : int, optional + The number of messages to aggregate over (default: settings.QUERY_LIMIT). + """ + search_scope = build_search_scope(guild_id, ids, scope) + index = _get_raw_index(redis) + + # Define reducers to calculate the average scores for each sentiment type + avg_sentiment = reducers.avg("@sentiment_compound").alias("sentiment_compound") + avg_negativity = reducers.avg("@sentiment_neg").alias("sentiment_neg") + avg_neutrality = reducers.avg("@sentiment_neu").alias("sentiment_neu") + avg_positivity = reducers.avg("@sentiment_pos").alias("sentiment_pos") + + # Build the aggregation query + query = ( + aggregations.AggregateRequest(f"{search_scope!s}") + .limit(0, limit) + .group_by( + [f"@{scope}"], + avg_sentiment, + avg_negativity, + avg_neutrality, + avg_positivity, + ) + ) + + # Execute the aggregation query on the index + results = await index.aggregate(query) # type: ignore + + # Deserialize all rows as dictionaries. Each row is a flat list of key-value pairs. + return [ + models.SentimentResult.model_validate( + { + key: float(value) + for row in results.rows + for key, value in itertools.batched(row, 2) + if value is not None + }, + ), + ] diff --git a/courageous-comets/courageous_comets/redis/schema.py b/courageous-comets/courageous_comets/redis/schema.py new file mode 100644 index 0000000..53854a7 --- /dev/null +++ b/courageous-comets/courageous_comets/redis/schema.py @@ -0,0 +1,29 @@ +from courageous_comets import settings + +MESSAGE_SCHEMA = { + "index": { + "name": "message_idx", + "prefix": f"{settings.REDIS_KEYS_PREFIX}:messages", + }, + "fields": [ + {"name": "user_id", "type": "tag"}, + {"name": "message_id", "type": "tag"}, + {"name": "channel_id", "type": "tag"}, + {"name": "guild_id", "type": "tag"}, + {"name": "timestamp", "type": "numeric", "attrs": {"sortable": True}}, + {"name": "sentiment_neg", "type": "numeric", "attrs": {"sortable": True}}, + {"name": "sentiment_neu", "type": "numeric", "attrs": {"sortable": True}}, + {"name": "sentiment_pos", "type": "numeric", "attrs": {"sortable": True}}, + {"name": "sentiment_compound", "type": "numeric", "attrs": {"sortable": True}}, + { + "name": "embedding", + "type": "vector", + "attrs": { + "dims": 384, + "distance_metric": "cosine", + "algorithm": "hnsw", + "datatype": "float32", + }, + }, + ], +} diff --git a/courageous-comets/courageous_comets/sentiment.py b/courageous-comets/courageous_comets/sentiment.py new file mode 100644 index 0000000..4d425e5 --- /dev/null +++ b/courageous-comets/courageous_comets/sentiment.py @@ -0,0 +1,29 @@ +import logging + +from nltk.sentiment import SentimentIntensityAnalyzer + +from courageous_comets.models import SentimentResult + +logger = logging.getLogger(__name__) + + +def calculate_sentiment(content: str) -> SentimentResult: + """ + Calculate the sentiment of a message. + + Uses the NLTK sentiment intensity analyzer. + + Parameters + ---------- + content : str + The message content to analyze. + + Returns + ------- + courageous_comets.models.SentimentResult + The sentiment of the message. + """ + sia = SentimentIntensityAnalyzer() + result = sia.polarity_scores(content) + + return SentimentResult.model_validate(result) diff --git a/courageous-comets/courageous_comets/settings.py b/courageous-comets/courageous_comets/settings.py new file mode 100644 index 0000000..9efb2b0 --- /dev/null +++ b/courageous-comets/courageous_comets/settings.py @@ -0,0 +1,186 @@ +"""Shared application configuration.""" + +import logging +import os +import sys +import warnings +from pathlib import Path + +import coloredlogs +from dotenv import load_dotenv + +from courageous_comets.exceptions import ConfigurationValueError + + +def read_bot_config_path() -> Path: + """ + Read the bot configuration path from the environment. + + Returns + ------- + pathlib.Path + The path to the bot configuration file. + + Raises + ------ + courageous_comets.exceptions.ConfigurationValueError + If the path does not exist. + """ + result = Path(os.getenv("BOT_CONFIG_PATH", "application.yaml")) + + if not result.exists(): + raise ConfigurationValueError( + key="BOT_CONFIG_PATH", + value=os.getenv("BOT_CONFIG_PATH"), + reason=f"No file found at path '{result.resolve()}'", + ) + + return result + + +def read_discord_token() -> str: + """ + Read the Discord token from the environment. + + Returns + ------- + str + The Discord bot token. + + Raises + ------ + courageous_comets.exceptions.ConfigurationValueError + If the token is not found + """ + result = os.getenv("DISCORD_TOKEN", "") + + if not result: + raise ConfigurationValueError( + key="DISCORD_TOKEN", + value=os.getenv("DISCORD_TOKEN"), + reason="No Discord bot token found. Set the DISCORD_TOKEN environment variable.", + ) + + return result + + +def read_int(key: str, default: int) -> int: + """ + Read an integer value from the environment. + + Parameters + ---------- + key : str + The environment variable key. + default : int + The default value to use if the environment variable is not set. + + Returns + ------- + int + The integer value. + + Raises + ------ + courageous_comets.exceptions.ConfigurationValueError + If the value is not a valid integer. + """ + try: + result = int(os.getenv(key, str(default))) + except ValueError as e: + raise ConfigurationValueError( + key=key, + value=os.getenv(key), + reason="Value must be an integer", + ) from e + + return result + + +def read_redis_port() -> int: + """ + Read the Redis port from the environment. + + Returns + ------- + int + The Redis port number. + + Raises + ------ + courageous_comets.exceptions.ConfigurationValueError + If the port is not a valid port number. + """ + result = read_int("REDIS_PORT", 6379) + + if not (0 <= result <= 65535): # noqa: PLR2004 + raise ConfigurationValueError( + key="REDIS_PORT", + value=result, + reason="Value must be a valid port number (0-65535)", + ) + + return result + + +def setup_logging() -> None: + """Set up logging for the application.""" + coloredlogs.install( + level=LOG_LEVEL, + fmt="{asctime} {levelname:<8} [{name}] {message}", + datefmt="%Y-%m-%d %H:%M:%S", + style="{", + reconfigure=True, + ) + + +# Load environment variables from a .env file. If the file does not exist, this does nothing. +load_dotenv() + +# Suppress warnings from libraries +warnings.filterwarnings("ignore") + +# Set up logging for the application +LOG_LEVEL = logging.getLevelNamesMapping().get( + os.getenv("LOG_LEVEL", "INFO"), + logging.INFO, +) + +setup_logging() + +# Determine whether the application is running in a development environment +IS_DEV = os.getenv("ENVIRONMENT", "production") == "development" + +if IS_DEV: + logging.warning("🚨 Application is running in development mode! 🚨") + +# Load configuration values from the environment +try: + DISCORD_TOKEN = read_discord_token() + BOT_CONFIG_PATH = read_bot_config_path() + DISCORD_API_CONCURRENCY = read_int("DISCORD_API_CONCURRENCY", 3) + NLTK_DATA_DIR = os.getenv("NLTK_DATA", "nltk_data") + NLTK_DOWNLOAD_CONCURRENCY = read_int("NLTK_DOWNLOAD_CONCURRENCY", 3) + PREPROCESSING_MAX_WORD_LENGTH = read_int("MAX_WORD_LENGTH", 35) + PREPROCESSING_MESSAGE_TRUNCATE_LENGTH = read_int("TRUNCATE_LENGTH", 256) + REDIS_HOST = os.getenv("REDIS_HOST", "localhost") + REDIS_PORT = read_redis_port() + REDIS_PASSWORD = os.getenv("REDIS_PASSWORD") + REDIS_KEYS_PREFIX = os.getenv("REDIS_KEYS_PREFIX", "courageous_comets") + # Maximum number of items to return from a query + QUERY_LIMIT = read_int("QUERY_LIMIT", 10) + # Huggingface environment variable for caching downloaded models. + # https://huggingface.co/docs/huggingface_hub/v0.24.0/package_reference/environment_variables#hf_home + HF_HOME = os.getenv( + "HF_HOME", + "hf_data", + ) + HF_DOWNLOAD_CONCURRENCY = read_int("HF_DOWNLOAD_CONCURRENCY", 3) +except ConfigurationValueError as e: + logging.critical( + "Cannot start the application due to configuration errors", + exc_info=e, + ) + sys.exit(1) + +logging.info("Configuration loaded successfully") diff --git a/courageous-comets/courageous_comets/transformers/__init__.py b/courageous-comets/courageous_comets/transformers/__init__.py new file mode 100644 index 0000000..8667502 --- /dev/null +++ b/courageous-comets/courageous_comets/transformers/__init__.py @@ -0,0 +1,3 @@ +from .helpers import init_transformers + +__all__ = ["init_transformers"] diff --git a/courageous-comets/courageous_comets/transformers/helpers.py b/courageous-comets/courageous_comets/transformers/helpers.py new file mode 100644 index 0000000..542c407 --- /dev/null +++ b/courageous-comets/courageous_comets/transformers/helpers.py @@ -0,0 +1,62 @@ +import asyncio +import logging +from pathlib import Path + +from transformers import AutoModel, AutoTokenizer + +from courageous_comets import settings + +logger = logging.getLogger(__name__) + + +async def download_transformer(resource: str, semaphore: asyncio.Semaphore) -> None: + """ + Download a transformer to the specified directory. + + Parameters + ---------- + resource : str + The transformer to download. + semaphore : asyncio.Semaphore + The semaphore to use for concurrency control. + """ + logger.debug("Downloading transformer %r...", resource) + + async with semaphore: + await asyncio.to_thread( + AutoTokenizer.from_pretrained, + pretrained_model_name_or_path=resource, + cache_dir=settings.HF_HOME, + ) + await asyncio.to_thread( + AutoModel.from_pretrained, + pretrained_model_name_or_path=resource, + cache_dir=settings.HF_HOME, + ) + + logger.debug("Transformer %r downloaded", resource) + + +async def init_transformers(resources: list[str]) -> None: + """ + Ensure all required transformers are downloaded. + + Parameters + ---------- + resources : list[str] + The list of transformers to download. + """ + if not any(resources): + logger.debug("No transformers to download") + return + + # Create the Huggingface data directory if it does not exist to avoid a race condition when + # running multiple download tasks concurrently + Path(settings.HF_HOME).mkdir(parents=True, exist_ok=True) + + semaphore = asyncio.Semaphore(settings.HF_DOWNLOAD_CONCURRENCY) + download_tasks = [download_transformer(resource, semaphore) for resource in resources] + + await asyncio.gather(*download_tasks) + + logger.debug("Transformers downloaded") diff --git a/courageous-comets/courageous_comets/ui/__init__.py b/courageous-comets/courageous_comets/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/courageous-comets/courageous_comets/ui/charts/__init__.py b/courageous-comets/courageous_comets/ui/charts/__init__.py new file mode 100644 index 0000000..8aa922b --- /dev/null +++ b/courageous-comets/courageous_comets/ui/charts/__init__.py @@ -0,0 +1,6 @@ +from pathlib import Path + +CACHE_ROOT = Path("artifacts/charts").resolve() +CACHE_ROOT.mkdir(parents=True, exist_ok=True) + +__all__ = ["CACHE_ROOT"] diff --git a/courageous-comets/courageous_comets/ui/charts/frequency_line.py b/courageous-comets/courageous_comets/ui/charts/frequency_line.py new file mode 100644 index 0000000..23703b4 --- /dev/null +++ b/courageous-comets/courageous_comets/ui/charts/frequency_line.py @@ -0,0 +1,73 @@ +import io + +import discord +import matplotlib.pyplot as plt +from matplotlib.dates import ( + AutoDateLocator, + ConciseDateFormatter, + DayLocator, + HourLocator, + MinuteLocator, +) + +from courageous_comets import models +from courageous_comets.enums import Duration + + +def render( + frequencies: list[models.MessageFrequency], + duration: Duration, +) -> discord.File: + """ + Plot the frequency of messages. + + Creates a line plot of number of messages over intervals and saves it to a file. + + Parameters + ---------- + frequencies: list[MessageFrequency] + A list of message frequency. + + Returns + ------- + discord.File + A plot of the frequency in memory. + + Note + ---- + Assumes list of frequencies is not empty. + """ + _, ax = plt.subplots() + + # If the there's only one point, use a bar plot, otherwise a line plot + if len(frequencies) > 1: + ax.plot( + [frequency.timestamp for frequency in frequencies], # type: ignore + [frequency.num_messages for frequency in frequencies], + ) + locator = AutoDateLocator().get_locator( + frequencies[0].timestamp, + frequencies[-1].timestamp, + ) + else: + ax.bar([frequencies[0].timestamp], [frequencies[0].num_messages], width=0.01) # type: ignore + if duration == Duration.daily: + locator = DayLocator() + elif duration == Duration.hourly: + locator = HourLocator() + elif duration == Duration.minute: + locator = MinuteLocator() + + formatter = ConciseDateFormatter(locator) + + ax.xaxis.set_major_locator(locator) + ax.xaxis.set_major_formatter(formatter) + + ax.set_ylabel("Number of messages.") + ax.set_title("Message Frequency") + + file_ = io.BytesIO() + plt.savefig(file_) + file_.seek(0) + + return discord.File(file_, filename="message_frequency.png") diff --git a/courageous-comets/courageous_comets/ui/charts/keywords_bars.py b/courageous-comets/courageous_comets/ui/charts/keywords_bars.py new file mode 100644 index 0000000..018d272 --- /dev/null +++ b/courageous-comets/courageous_comets/ui/charts/keywords_bars.py @@ -0,0 +1,31 @@ +import io +from collections import Counter + +import discord +import matplotlib.pyplot as plt + + +def render(counter: Counter[str]) -> discord.File: + """ + Render a bar chart of the top given keywords. + + Parameters + ---------- + counter: Counter[str] + The keywords and their counts. + """ + keywords, counts = zip(*counter.most_common(10), strict=True) + + _, ax = plt.subplots() + ax.bar(keywords, counts) + ax.set_ylabel("Count") + ax.set_title("Top keywords") + + # Rotate the x-axis labels 45 degrees to keep them readable + plt.xticks(rotation=45, ha="right") + + file_ = io.BytesIO() + plt.savefig(file_) + file_.seek(0) + + return discord.File(file_, "top_keywords.png") diff --git a/courageous-comets/courageous_comets/ui/charts/sentiment_bars.py b/courageous-comets/courageous_comets/ui/charts/sentiment_bars.py new file mode 100644 index 0000000..e0d01cb --- /dev/null +++ b/courageous-comets/courageous_comets/ui/charts/sentiment_bars.py @@ -0,0 +1,95 @@ +import io +from pathlib import Path + +import discord +import matplotlib.pyplot as plt + +from courageous_comets import models +from courageous_comets.ui.charts import CACHE_ROOT + +CACHE_DIR = CACHE_ROOT / "sentiment_bars" +CACHE_DIR.mkdir(parents=True, exist_ok=True) + + +def for_message( + message_id: str | int, + data: models.SentimentResult, +) -> discord.File: + """ + Plot the sentiment analysis of a message. + + Creates a bar chart of the sentiment analysis of a message and saves it to a file. + If the file already exists, it will be returned instead of being recreated. + + Parameters + ---------- + message_id : str | int + The id of the message. + analysis_result : SentimentResult + The result of sentiment analysis on a message. + + Returns + ------- + discord.File + The file containing the saved image. + + Notes + ----- + Charts are cached in the `CACHE_DIR` directory using the message id as the filename. + """ + chart_path = CACHE_DIR / f"{message_id}.png" + + if chart_path.exists(): + return discord.File(chart_path, filename=f"{message_id}.png") + + _plot(data, chart_path) + + return discord.File(chart_path, filename=f"{message_id}.png") + + +def for_user(data: models.SentimentResult) -> discord.File: + """ + Plot the sentiment analysis of a user. + + Creates a bar chart of the sentiment analysis of a user and saves it to a temporary file. + + Parameters + ---------- + analysis_result : SentimentResult + The result of sentiment analysis on a user. + + Returns + ------- + discord.File + The file containing the saved image. + """ + file_ = io.BytesIO() + _plot(data, file_) + file_.seek(0) + + return discord.File(file_, filename="user_sentiment.png") + + +def _plot(data: models.SentimentResult, target: Path | io.BytesIO) -> None: + _, ax = plt.subplots() + ax.bar( + [ + "Negative", + "Neutral", + "Positive", + ], + [ + data.neg, + data.neu, + data.pos, + ], + color=[ + "red", + "blue", + "green", + ], + ) + ax.set_ylabel("Sentiment Score") + ax.set_title("Sentiment Analysis") + + plt.savefig(target) diff --git a/courageous-comets/courageous_comets/ui/components/__init__.py b/courageous-comets/courageous_comets/ui/components/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/courageous-comets/courageous_comets/ui/components/message.py b/courageous-comets/courageous_comets/ui/components/message.py new file mode 100644 index 0000000..39d53f7 --- /dev/null +++ b/courageous-comets/courageous_comets/ui/components/message.py @@ -0,0 +1,42 @@ +import discord + +TEMPLATE = """ +{channel} {author} {timestamp}: +{content} +""" + + +def _shorten(string: str, *, limit: int = 200) -> str: + """Trim a string if necessary given `limit`.""" + if len(string) > limit: + string = string[: limit - 3] + "..." + return string + + +def render(message: discord.Message) -> str: + """ + Format a message into a string. + + Parameters + ---------- + message : discord.Message + The message to format. + + Returns + ------- + str + The formatted string. + """ + channel = ( + f"{message.channel.mention}" + if isinstance(message.channel, discord.abc.GuildChannel) + else "" + ) + return TEMPLATE.format_map( + { + "channel": channel, + "author": message.author.mention, + "timestamp": discord.utils.format_dt(message.created_at, style="R"), + "content": _shorten(message.clean_content), + }, + ) diff --git a/courageous-comets/courageous_comets/ui/components/message_list.py b/courageous-comets/courageous_comets/ui/components/message_list.py new file mode 100644 index 0000000..815a817 --- /dev/null +++ b/courageous-comets/courageous_comets/ui/components/message_list.py @@ -0,0 +1,20 @@ +import discord + +from courageous_comets.ui.components import message + + +def render(messages: list[discord.Message]) -> str: + """ + Format a list of messages into a string. + + Parameters + ---------- + messages : list[discord.Message] + The messages to format. + + Returns + ------- + str + The formatted string. + """ + return "".join(map(message.render, messages)) diff --git a/courageous-comets/courageous_comets/ui/components/search_results.py b/courageous-comets/courageous_comets/ui/components/search_results.py new file mode 100644 index 0000000..cab27ea --- /dev/null +++ b/courageous-comets/courageous_comets/ui/components/search_results.py @@ -0,0 +1,35 @@ +import discord + +from courageous_comets.ui.components import message_list + +TEMPLATE = """ +The most recent messages related to your query are: + +{results} + +You searched for: `{query}` +""" + + +def render(query: str, messages: list[discord.Message]) -> str: + """ + Render a list of messages into search results. + + Parameters + ---------- + query : str + The query used to find the messages. + messages : list[discord.Message] + The messages to render. + + Returns + ------- + str + The rendered search results + """ + return TEMPLATE.format_map( + { + "results": message_list.render(messages), + "query": query, + }, + ) diff --git a/courageous-comets/courageous_comets/ui/components/sentiment.py b/courageous-comets/courageous_comets/ui/components/sentiment.py new file mode 100644 index 0000000..e53af82 --- /dev/null +++ b/courageous-comets/courageous_comets/ui/components/sentiment.py @@ -0,0 +1,28 @@ +SENTIMENT: dict[range, str] = { + range(-100, -50): "very negative 😡", + range(-50, -10): "negative 🙁", + range(-10, 10): "neutral 🙂", + range(10, 50): "positive 😁", + range(50, 100): "very positive 😍", +} + + +def render(compound: float) -> str: + """ + Render the given sentiment score into a string. + + Parameters + ---------- + compound : float + The compound sentiment score. + + Returns + ------- + str + The rendered sentiment. + """ + for score_range, sentiment in SENTIMENT.items(): + if int(compound * 100) in score_range: + return sentiment + + return "unknown 😶" diff --git a/courageous-comets/courageous_comets/ui/embeds/__init__.py b/courageous-comets/courageous_comets/ui/embeds/__init__.py new file mode 100644 index 0000000..24aa073 --- /dev/null +++ b/courageous-comets/courageous_comets/ui/embeds/__init__.py @@ -0,0 +1,3 @@ +from .format import format_embed + +__all__ = ["format_embed"] diff --git a/courageous-comets/courageous_comets/ui/embeds/about.py b/courageous-comets/courageous_comets/ui/embeds/about.py new file mode 100644 index 0000000..349fbfb --- /dev/null +++ b/courageous-comets/courageous_comets/ui/embeds/about.py @@ -0,0 +1,34 @@ +import discord + +from courageous_comets import __version__ +from courageous_comets.ui.embeds import format_embed + +TEMPLATE = """ +{body} + +Click the link in the header to visit the documentation! +""" + + +def render(body: str) -> discord.Embed: + """ + Render the about message into an embed. + + Parameters + ---------- + body : str + The body of the about message. + + Returns + ------- + discord.Embed + The rendered embed. + """ + embed = discord.Embed( + title="About Courageous Comets", + description=TEMPLATE.format_map({"body": body}), + color=discord.Color.blurple(), + url=f"https://thijsfranck.github.io/courageous-comets/{__version__}/", + timestamp=discord.utils.utcnow(), + ) + return format_embed(embed) diff --git a/courageous-comets/courageous_comets/ui/embeds/format.py b/courageous-comets/courageous_comets/ui/embeds/format.py new file mode 100644 index 0000000..0404117 --- /dev/null +++ b/courageous-comets/courageous_comets/ui/embeds/format.py @@ -0,0 +1,24 @@ +import discord + +from courageous_comets import __version__ + + +def format_embed(embed: discord.Embed) -> discord.Embed: + """ + Format the given embed for consistent branding across interactions. + + Includes a footer with the version of the app. + + Parameters + ---------- + embed : discord.Embed + The embed to format. + + Returns + ------- + discord.Embed + The formatted embed. + """ + footer_text = f"Generated using Courageous Comets {__version__}" + embed.set_footer(text=footer_text) + return embed diff --git a/courageous-comets/courageous_comets/ui/embeds/message_frequency.py b/courageous-comets/courageous_comets/ui/embeds/message_frequency.py new file mode 100644 index 0000000..340cb1b --- /dev/null +++ b/courageous-comets/courageous_comets/ui/embeds/message_frequency.py @@ -0,0 +1,71 @@ +import discord + +from courageous_comets import models +from courageous_comets.enums import Duration +from courageous_comets.ui.embeds import format_embed + +DURATION_NAME = { + Duration.daily: "day", + Duration.hourly: "hour", + Duration.minute: "minute", +} + +TEMPLATE = """ +The chart below shows the number of messages sent by users on this server per **{duration}**. + +Times shown in the chart are in UTC. +""" + + +def render(frequencies: list[models.MessageFrequency], duration: Duration) -> discord.Embed: + """ + Render the frequency of messages. + + Creates an embed of the frequency of messages over intervals. + + Parameters + ---------- + frequencies: list[MessageFrequency] + A list of message frequency. + + Returns + ------- + discord.Embed + An embed of the frequency. + """ + template_vars = { + "duration": DURATION_NAME[duration], + } + + embed = discord.Embed( + title="Message Frequency", + description=TEMPLATE.format_map(template_vars), + color=discord.Colour.purple(), + timestamp=discord.utils.utcnow(), + ) + + total_messages = sum(frequency.num_messages for frequency in frequencies) + peak_frequency = max(frequencies, key=lambda frequency: frequency.num_messages) + + time_style = "D" if duration == Duration.daily else "t" + most_active_time = discord.utils.format_dt(peak_frequency.timestamp, style=time_style) + + embed.add_field( + name="Total", + value=total_messages, + inline=True, + ) + + embed.add_field( + name="Peak", + value=peak_frequency.num_messages, + inline=True, + ) + + embed.add_field( + name="Most active time", + value=most_active_time, + inline=True, + ) + + return format_embed(embed) diff --git a/courageous-comets/courageous_comets/ui/embeds/message_sentiment.py b/courageous-comets/courageous_comets/ui/embeds/message_sentiment.py new file mode 100644 index 0000000..ce4c836 --- /dev/null +++ b/courageous-comets/courageous_comets/ui/embeds/message_sentiment.py @@ -0,0 +1,57 @@ +import discord + +from courageous_comets.models import SentimentResult +from courageous_comets.ui.components import sentiment +from courageous_comets.ui.embeds import format_embed + +TEMPLATE = """ +Overall the sentiment of the message is **{sentiment}**. +""" + + +def render(data: SentimentResult) -> discord.Embed: + """ + Render the sentiment analysis results into an embed. + + Parameters + ---------- + data : SentimentResult + The sentiment analysis results. + + Returns + ------- + discord.Embed + The rendered embed. + """ + color = discord.Color.green() if data.compound >= 0 else discord.Color.red() + + template_vars = { + "sentiment": sentiment.render(data.compound), + } + + embed = discord.Embed( + title="Message Sentiment", + description=TEMPLATE.format_map(template_vars), + color=color, + timestamp=discord.utils.utcnow(), + ) + + embed.add_field( + name="Negative", + value=f"{int(data.neg * 100)}%", + inline=True, + ) + + embed.add_field( + name="Neutral", + value=f"{int(data.neu * 100)}%", + inline=True, + ) + + embed.add_field( + name="Positive", + value=f"{int(data.pos * 100)}%", + inline=True, + ) + + return format_embed(embed) diff --git a/courageous-comets/courageous_comets/ui/embeds/popular_topics.py b/courageous-comets/courageous_comets/ui/embeds/popular_topics.py new file mode 100644 index 0000000..3982598 --- /dev/null +++ b/courageous-comets/courageous_comets/ui/embeds/popular_topics.py @@ -0,0 +1,34 @@ +from collections import Counter + +import discord + +from courageous_comets.enums import StatisticScope +from courageous_comets.ui.embeds import format_embed + + +def render(scope: StatisticScope, keywords: Counter[str]) -> discord.Embed: + """ + Render the top keywords used in the given scope. + + Parameters + ---------- + user: StatisticsScope + The scope to show the keywords for. + keywords: Counter[str] + The keywords and their counts. + """ + embed = discord.Embed( + title="Popular Topics", + description=f"Here are the top keywords for the current **{scope.name.lower()}**.", + color=discord.Color.blurple(), + timestamp=discord.utils.utcnow(), + ) + + for keyword, count in keywords.most_common(3): + embed.add_field( + name=keyword, + value=f"{count} times", + inline=True, + ) + + return format_embed(embed) diff --git a/courageous-comets/courageous_comets/ui/embeds/search_results.py b/courageous-comets/courageous_comets/ui/embeds/search_results.py new file mode 100644 index 0000000..267bf96 --- /dev/null +++ b/courageous-comets/courageous_comets/ui/embeds/search_results.py @@ -0,0 +1,29 @@ +import discord + +from courageous_comets.ui.components import search_results +from courageous_comets.ui.embeds import format_embed + + +def render(query: str, messages: list[discord.Message]) -> discord.Embed: + """ + Render a list of messages into an embed. + + Parameters + ---------- + query : str + The query used to find the messages. + messages : list[discord.Message] + The messages to render. + + Returns + ------- + discord.Embed + The rendered embed. + """ + embed = discord.Embed( + title="Search Results 🚀", + description=search_results.render(query, messages), + colour=discord.Colour.blurple(), + timestamp=discord.utils.utcnow(), + ) + return format_embed(embed) diff --git a/courageous-comets/courageous_comets/ui/embeds/user_keywords.py b/courageous-comets/courageous_comets/ui/embeds/user_keywords.py new file mode 100644 index 0000000..3942268 --- /dev/null +++ b/courageous-comets/courageous_comets/ui/embeds/user_keywords.py @@ -0,0 +1,33 @@ +from collections import Counter + +import discord + +from courageous_comets.ui.embeds import format_embed + + +def render(user: discord.User | discord.Member, keywords: Counter[str]) -> discord.Embed: + """ + Render the top keywords used by a user. + + Parameters + ---------- + user: discord.User | discord.Member + The user to show the keywords for. + keywords: Counter[str] + The keywords and their counts. + """ + embed = discord.Embed( + title="User Interests", + description=f"Here are the top keywords that {user.mention} has used in their messages.", + color=discord.Color.blurple(), + timestamp=discord.utils.utcnow(), + ) + + for keyword, count in keywords.most_common(3): + embed.add_field( + name=keyword, + value=f"{count} times", + inline=True, + ) + + return format_embed(embed) diff --git a/courageous-comets/courageous_comets/ui/embeds/user_sentiment.py b/courageous-comets/courageous_comets/ui/embeds/user_sentiment.py new file mode 100644 index 0000000..290addb --- /dev/null +++ b/courageous-comets/courageous_comets/ui/embeds/user_sentiment.py @@ -0,0 +1,58 @@ +import discord + +from courageous_comets.models import SentimentResult +from courageous_comets.ui.components import sentiment +from courageous_comets.ui.embeds import format_embed + +TEMPLATE = """ +Overall the sentiment of {user} is **{sentiment}**. +""" + + +def render(user: discord.User | discord.Member, data: SentimentResult) -> discord.Embed: + """ + Render the sentiment analysis results into an embed. + + Parameters + ---------- + data : SentimentResult + The sentiment analysis results. + + Returns + ------- + discord.Embed + The rendered embed. + """ + color = discord.Color.green() if data.compound >= 0 else discord.Color.red() + + template_vars = { + "user": user.mention, + "sentiment": sentiment.render(data.compound), + } + + embed = discord.Embed( + title="User Sentiment", + description=TEMPLATE.format_map(template_vars), + color=color, + timestamp=discord.utils.utcnow(), + ) + + embed.add_field( + name="Negative", + value=f"{int(data.neg * 100)}%", + inline=True, + ) + + embed.add_field( + name="Neutral", + value=f"{int(data.neu * 100)}%", + inline=True, + ) + + embed.add_field( + name="Positive", + value=f"{int(data.pos * 100)}%", + inline=True, + ) + + return format_embed(embed) diff --git a/courageous-comets/courageous_comets/ui/views/__init__.py b/courageous-comets/courageous_comets/ui/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/courageous-comets/courageous_comets/ui/views/sentiment.py b/courageous-comets/courageous_comets/ui/views/sentiment.py new file mode 100644 index 0000000..a09b482 --- /dev/null +++ b/courageous-comets/courageous_comets/ui/views/sentiment.py @@ -0,0 +1,72 @@ +import random +from typing import override + +import discord + +from courageous_comets import models + +STICKERS = [ + 751606065447305216, + 754109076933443614, + 772972089963577354, + 781324722394103808, + 809207198856904764, +] + + +class SentimentView(discord.ui.View): + """ + A view for interacting with a user's sentiment analysis results. + + Attributes + ---------- + user : discord.User | discord.Member + The user whose sentiment is being analyzed. + data : models.SentimentResult + The sentiment analysis results. + """ + + def __init__(self, user: discord.User | discord.Member, data: models.SentimentResult) -> None: + super().__init__() + self.user = user + self.data = data + + self.add_item(PraiseButton(user, data)) + + +class PraiseButton(discord.ui.Button): + """ + A button to praise a user. + + Attributes + ---------- + user : discord.User | discord.Member + The user to praise. + """ + + def __init__(self, user: discord.User | discord.Member, data: models.SentimentResult) -> None: + super().__init__( + style=discord.ButtonStyle.success, + label="Send Praise", + emoji="🎉", + disabled=data.compound < 0, + ) + self.user = user + self.data = data + + @override + async def callback(self, interaction: discord.Interaction) -> None: + # Fetch a random sticker to send with the praise + sticker = await interaction.client.fetch_sticker(random.choice(STICKERS)) + + # Send the praise to the user + await self.user.send( + f"You've been praised by {interaction.user.mention}!", + stickers=[sticker], # type: ignore + ) + + # Send a confirmation message + await interaction.response.send_message( + f"Sent praise to {self.user.mention}!", + ephemeral=True, + ) diff --git a/courageous-comets/courageous_comets/utils.py b/courageous-comets/courageous_comets/utils.py new file mode 100644 index 0000000..9d3bef4 --- /dev/null +++ b/courageous-comets/courageous_comets/utils.py @@ -0,0 +1,12 @@ +import typing + + +def contextmenu(name: str) -> typing.Callable: + """Mark a function as a context menu.""" + + def wrap(func: typing.Callable) -> typing.Callable: + func.is_contextmenu = True + func.name = name + return func + + return wrap diff --git a/courageous-comets/courageous_comets/vectorizer.py b/courageous-comets/courageous_comets/vectorizer.py new file mode 100644 index 0000000..4610ba1 --- /dev/null +++ b/courageous-comets/courageous_comets/vectorizer.py @@ -0,0 +1,105 @@ +import asyncio + +import numpy as np +import torch +import torch.nn.functional as torch_nn_functional +from torch import Tensor +from transformers import AutoModel, AutoTokenizer + +from courageous_comets import settings + + +class Vectorizer: + """Convert a chunk of text to vector embedding. + + This class uses the Hugging Face sentence transformer to create vector + embeddings. + + Attributes + ---------- + TRANSFORMER_MODEL: str + The name of the model for training the transformer + tokenizer: transformers.AutoTokenizer + The Hugging Face sentence tokenizer + model: transformers.AutoModel + The sentence transformer + """ + + TRANSFORMER_MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2" + + def __init__(self) -> None: + self.tokenizer = AutoTokenizer.from_pretrained( + Vectorizer.TRANSFORMER_MODEL_NAME, + cache_dir=settings.HF_HOME, + ) + self.model = AutoModel.from_pretrained( + Vectorizer.TRANSFORMER_MODEL_NAME, + cache_dir=settings.HF_HOME, + ) + + def encode(self, message: str) -> bytes: + """ + Create vector embedding of a message. + + The encoder applies the follwowing steps: + + - Tokenize sentences + - Compute token embeddings + - Perform pooling taking into account the attention mask + - Normalize embeddings using torch.nn.functional + + Adapted from: https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2#usage-huggingface-transformers + + Parameters + ---------- + message: str + The message to generate vector embeddings + + Returns + ------- + bytes + The vector embeddings of the message + """ + + # Mean Pooling - Take attention mask into account for correct averaging + def mean_pooling(model_output: list[Tensor], attention_mask: Tensor) -> Tensor: + token_embeddings = model_output[ + 0 + ] # First element of model_output contains all token embeddings + input_mask_expanded = ( + attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float() + ) + return torch.sum(token_embeddings * input_mask_expanded, 1) / torch.clamp( + input_mask_expanded.sum(1), + min=1e-9, + ) + + # Tokenize sentences + encoded_input = self.tokenizer( + [message], + padding=True, + truncation=True, + return_tensors="pt", + ) + + # Compute token embeddings + with torch.no_grad(): + model_output = self.model(**encoded_input) + + # Perform pooling + sentence_embeddings = mean_pooling( + model_output, + encoded_input["attention_mask"], + ) + + # Normalize embeddings + return ( + torch_nn_functional.normalize(sentence_embeddings, p=2, dim=1) + .numpy() + .astype(np.float32) + .tobytes() + ) + + async def aencode(self, message: str) -> bytes: + """Create a vector embedding of message asynchronously.""" + return await asyncio.to_thread(self.encode, message) diff --git a/courageous-comets/courageous_comets/words.py b/courageous-comets/courageous_comets/words.py new file mode 100644 index 0000000..8caf111 --- /dev/null +++ b/courageous-comets/courageous_comets/words.py @@ -0,0 +1,63 @@ +from collections import Counter + +import contractions +from nltk.stem.snowball import SnowballStemmer, stopwords +from nltk.tokenize import word_tokenize + + +def tokenize_sentence(sentence: str) -> list[str]: + """ + Split a sentence into tokens. + + The tokenizer applies the following steps: + + - Expand contractions. + - Break the sentence into words. + - Stem the words. + - Remove stopwords and words with a length of 1. + + Parameters + ---------- + sentence : str + The sentence to tokenize. + + Returns + ------- + list[str] + The tokens derived from the sentence. + + Notes + ----- + The tokenizer is intended to be used with English text. + """ + # Expand contractions + expanded = contractions.fix(sentence) + + # Break the sentence into words + words = word_tokenize(expanded) + + # Stem the words + stemmer = SnowballStemmer("english") + stemmed_words = [stemmer.stem(word) for word in words] + + # Remove stopwords and words with a length of 1 + stop_words = set(stopwords.words("english")) + + return [word for word in stemmed_words if len(word) > 1 and word not in stop_words] + + +def word_frequency(words: list[str]) -> dict[str, int]: + """ + Count the number of times each word appears in the given list of words. + + Parameters + ---------- + words : list[str] + The list of words to count the frequency of. + + Returns + ------- + dict[str, int] + The frequency of each word in words. + """ + return Counter(words) diff --git a/courageous-comets/docker-compose.yaml b/courageous-comets/docker-compose.yaml new file mode 100644 index 0000000..0dda020 --- /dev/null +++ b/courageous-comets/docker-compose.yaml @@ -0,0 +1,30 @@ +services: + courageous-comets: + image: ghcr.io/thijsfranck/courageous-comets:${COURAGEOUS_COMETS_VERSION:-latest} + environment: + REDIS_HOST: redis-stack + REDIS_PORT: 6379 + env_file: + - .env + restart: always + depends_on: + - redis-stack + networks: + - comets-network + + redis-stack: + image: redis/redis-stack-server:${REDIS_STACK_VERSION:-latest} + volumes: + - redis-stack-data:/data + ports: + - "6379:6379" + restart: always + networks: + - comets-network + +volumes: + redis-stack-data: + + +networks: + comets-network: diff --git a/courageous-comets/docs/CHANGELOG.md b/courageous-comets/docs/CHANGELOG.md new file mode 100644 index 0000000..edacfba --- /dev/null +++ b/courageous-comets/docs/CHANGELOG.md @@ -0,0 +1,194 @@ +## v1.0.0 (2024-07-28) + +### BREAKING CHANGE + +- dependencies are no longer updated + +### Feat + +- lock dependencies for final version + +## v0.10.0 (2024-07-28) + +### Feat + +- frequency time divisions (#62) +- add topics command that lists the most used keywords (#61) +- add user keyword frequency (#60) + +### Fix + +- capitalize embed title +- ensure consistent styling on embed titles +- improve logging for all interactions + +### Refactor + +- log message processed as debug event +- log database event as error +- simplify messages cog +- remove redundant error class +- remove redundant decorator +- split up keywords and sentiment cogs, improve cog documentation + +## v0.9.0 (2024-07-28) + +### Feat + +- add praise interaction for user and message sentiment +- harmonize sentiment embeds +- add details to frequency embed +- refactor cogs with ui components and complete existing interactions + +### Fix + +- increase search results max preview length +- show channel in search results +- avoid error when no sentiment data is available for a user + +### Refactor + +- remove redundant code +- remove unused component +- move message processing to a separate module + +## v0.8.0 (2024-07-27) + +### Feat + +- plot message frequency graph + +### Fix + +- reference current version in documentation +- update app description and set embed footer +- handle missing sentiment data +- restore user sentiment interaction +- add exists check +- silence KeyboardInterrupt on application shutdown (#59) +- remove cog from erroneous merge conflict resolution (#58) +- remove unused context menu loader + +## v0.7.0 (2024-07-26) + +### Feat + +- add user sentiment interaction (#54) +- set up frequency cog (#53) +- add sentiment chart (#52) +- add cog that returns information about the app (#50) +- add drop_code_blocks processor +- get message rate by duration (#49) +- calculate count of tokens across messages (#48) +- allow redis search filtering across multiple ids (#47) +- add development mode (#46) + +### Fix + +- remove reference to link from general bot description and add it to the about message +- handle PackageNotFoundError +- add __version__ and log version on startup + +## v0.6.0 (2024-07-24) + +### Feat + +- store message tokens (#45) +- limit queries to most recent messsages (#43) +- store sentiment results along with message (#42) + +### Refactor + +- simplify function signatures and make it easier to build scope filters (#44) + +## v0.5.0 (2024-07-23) + +### Feat + +- add docker compose and fix production dockerfile + +## v0.4.0 (2024-07-23) + +### Feat + +- revert back to hash data type for storing messages (#41) + +### Fix + +- open app config in read mode +- trucate uses correct length +- preprocessing drops extra whitespace + +## v0.3.0 (2024-07-22) + +### Feat + +- store sentiment analysis with message data (#39) +- set up message content preprocessing (#40) +- replace sentence_transformers with transformers library (#36) +- add messages cog (#32) +- return messages based on similarity score (#30) + +### Fix + +- fix sync command signature and internal logic (#38) +- save messages with message_id on redis (#33) +- ensure consistent log output (#29) +- suppress warnings from libraries + +### Refactor + +- remove hfvectorizer (#35) +- include save_message in Messages cog (#34) +- remove throws clause from docstring + +## v0.2.0 (2024-07-21) + +### Feat + +- setup transformer models (#28) +- use pydantic to model Redis hashes (#25) +- add sentiment analysis (#24) +- add api to store word frequency on redis (#23) +- download nltk resources on startup (#22) + +### Fix + +- handle contractions in tokenizer (#26) +- control max number of concurrent downloads +- add logging and remove return section from docstring + +### Refactor + +- move startup logic into bot class (#27) +- improve docs, types and fix some linter issues +- update docstrings +- improve type hints + +## v0.1.0 (2024-07-20) + +### Feat + +- add bot boilerplate + +### Fix + +- improved error handling and settings validation +- include application config in docker image (#15) +- set redis host from environment variables + +### Refactor + +- improve error handling and logging + +## v0.0.6 (2024-07-19) + +## v0.0.5 (2024-07-17) + +## v0.0.4 (2024-07-16) + +## v0.0.3 (2024-07-16) + +## v0.0.2 (2024-07-16) + +## v0.0.1 (2024-07-15) diff --git a/courageous-comets/docs/README.md b/courageous-comets/docs/README.md new file mode 100644 index 0000000..c9666e6 --- /dev/null +++ b/courageous-comets/docs/README.md @@ -0,0 +1,70 @@ +# Courageous Comets ☄️ + +Navigating a new Discord server can be overwhelming, _but it doesn't have to be_. Introducing **Courageous Comets**, +the Discord bot that helps you feel at home in any server! + +## Features + +
+ +- :fontawesome-solid-people-group:{ .lg .middle } **Discover new communities** + + --- + + Quickly evaluate whether a new server is the right fit for you. + + [:octicons-arrow-right-24: Get to know a community](./user-guide/getting-started.md#getting-to-know-the-community) + +- :octicons-search-16:{ .lg .middle } **Find your niche** + + --- + + Easily locate the channels and users that match your interests and values. + + [:octicons-arrow-right-24: Find your way around](./user-guide/getting-started.md#finding-your-way-around) + + [:octicons-arrow-right-24: Discover like-minded people](./user-guide/getting-started.md#discovering-like-minded-people) + +- :material-human-greeting-variant:{ .lg .middle } **Foster positivity** + + --- + + Identify the most welcoming and positive communities on Discord and help them grow. + + [:octicons-arrow-right-24: Build safe environments](./user-guide/getting-started.md#contributing-to-a-safe-environment) + +- :material-scale-balance:{ .lg .middle } **Free and open-source** + + --- + + Courageous Comets is fully [open source](https://github.com/thijsfranck/courageous-comets/blob//) + and free to use for anyone! + + [:octicons-arrow-right-24: Install on your server](./user-guide/installing-the-bot.md) + + [:octicons-arrow-right-24: Deploy your own instance](./admin-guide//deployment.md) + +
+ +## Contents + +This documentation is divided into the following sections: + +- [User Guide](./user-guide/index.md): How to interact with the bot and make the most of its features. +- [Administrator Guide](./admin-guide/index.md): How to deploy, configure and manage the bot in production. +- [Contributor Guide](./contributor-guide/index.md): How to set up a development environment and contribute to + the project. + +## Authors + +Thank you for your interest in our project! It was built as part of the [Python Discord Summer Code Jam 2024](https://www.pythondiscord.com/events/code-jams/11/). + +The following team members contributed to the project: + +[elfkuzco](https://github.com/elfkuzco) +[isaa-ctaylor](https://github.com/isaa-ctaylor) +[thijsfranck](https://github.com/thijsfranck) + +## License + +This project is published under the [MIT license](https://github.com/thijsfranck/courageous-comets/blob//LICENSE). diff --git a/courageous-comets/docs/admin-guide/configuration.md b/courageous-comets/docs/admin-guide/configuration.md new file mode 100644 index 0000000..608cdf8 --- /dev/null +++ b/courageous-comets/docs/admin-guide/configuration.md @@ -0,0 +1,168 @@ +# Configuration + +The following environment variables are available to configure the application: + +| Variable | Description | Required | Default | +| --------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -------- | ------------------ | +| [`DISCORD_TOKEN`](#discord_token) | The Discord bot token. | Yes | - | +| [`BOT_CONFIG_PATH`](#bot_config_path) | The path to the bot's configuration file. | No | `application.yaml` | +| [`DISCORD_API_CONCURRENCY`](#discord_api_concurrency) | The maximum number of concurrent Discord API requests. | No | `3` | +| [`ENVIRONMENT`](#environment) | The environment in which the application is running. | No | `production` | +| [`HF_DOWNLOAD_CONCURRENCY`](#hf_download_concurrency) | The maximum number of concurrent downloads when installing transformers. | No | `3` | +| [`HF_HOME`](#hf_home) | The directory containing Huggingface Transformers data files. | No | `hf_data` | +| [`LOG_LEVEL`](#log_level) | The minimum log level. | No | `INFO` | +| [`MPLCONFIGDIR`](#mplconfigdir) | The directory containing Matplotlib configuration files. | No | `/app/matplotlib` | +| [`NLTK_DATA`](#nltk_data) | The directory containing NLTK data files. | No | `nltk_data` | +| [`NLTK_DOWNLOAD_CONCURRENCY`](#nltk_download_concurrency) | The maximum number of concurrent downloads when installing NLTK data. | No | `3` | +| [`PREPROCESSING_MAX_WORD_LENGTH`](#preprocessing_max_word_length) | The maximum word length. Longer words are dropped. | No | `35` | +| [`PREPROCESSING_MESSAGE_TRUNCATE_LENGTH`](#preprocessing_message_truncate_length) | The maximum message length. Longer messages are truncated. | No | `256` | +| [`REDIS_HOST`](#redis_host) | The Redis host. | No | `localhost` | +| [`REDIS_PORT`](#redis_port) | The Redis port. | No | `6379` | +| [`REDIS_PASSWORD`](#redis_password) | The Redis password. | No | - | + +## Required Settings + +The following settings are required to start the application: + +### `DISCORD_TOKEN` + +You can obtain a Discord bot token from the [Discord Developer Portal](https://discord.com/developers/applications). +Your token should have the following scopes: + +- `bot` + +!!! DANGER "Security Warning" + + Do not share your token with anyone! + +## Optional Settings + +The following settings are optional or have default values that can be overridden: + +### `BOT_CONFIG_PATH` + +This specifies the location of the bot's configuration file. By default, the application searches for a file named +`application.yaml` in the directory from which it is launched. In the Docker image, this file is located at `/app/application.yaml`. + +[Read more](#applicationyaml) about the `application.yaml` file. + +### `DISCORD_API_CONCURRENCY` + +The maximum number of concurrent Discord API requests. By default, this is set to `3`. + +### `ENVIRONMENT` + +The environment in which the application is running. Set this to `development` to enable development features +such as loading development cogs. By default, this is set to `production`. + +### `HF_DOWNLOAD_CONCURRENCY` + +The maximum number of concurrent downloads when installing Huggingface Transformers models. By default, this +is set to `3`. + +### `HF_HOME` + +The directory containing Huggingface Transformers data files. By default, this is set to `hf_data` in the directory +from which the application is launched. In the Docker image, this directory is located at `/app/hf_data`. + +### `LOG_LEVEL` + +The minimum log level to display. The following levels are available: + +- `DEBUG` +- `INFO` +- `WARNING` +- `ERROR` +- `CRITICAL` + +The default log level is `INFO`. + +### `MPLCONFIGDIR` + +The directory containing `matplotlib` configuration files. Uses the default `matplotlib` configuration directory +unless otherwise configured. In the Docker image, the `matplotlib` configuration files are located at `/app/matplotlib`. + +### `NLTK_DATA` + +The directory containing NLTK data files. By default, this is set to `nltk_data` in the directory from which the +application is launched. In the Docker image, this directory is located at `/app/nltk_data`. + +### `NLTK_DOWNLOAD_CONCURRENCY` + +The application automatically downloads NLTK data files on startup. This setting controls the number of concurrent +downloads. By default, this is set to `3`. + +### `PREPROCESSING_MAX_WORD_LENGTH` + +The maximum word length. Words longer than this value are dropped. By default, this is set to `35`. + +### `PREPROCESSING_MESSAGE_TRUNCATE_LENGTH` + +The maximum message length. Messages longer than this value are truncated. By default, this is set to `256`. + +### `REDIS_HOST` + +The hostname of the Redis server. Defaults to `localhost`. + +### `REDIS_PORT` + +The port of the Redis server. Defaults to `6379`. + +### `REDIS_PASSWORD` + +The password of the Redis server. Set this variable if your Redis server requires authentication. No password +is set by default. + +!!! DANGER "Security Warning" + + Do not share your Redis password with anyone! + +## `application.yaml` + +The `application.yaml` file is a configuration file that specifies the cogs to load, the NLTK datasets to download, +and the Huggingface Transformers models to install on startup. It has the following structure: + +```yaml +# List of cogs to load when the bot starts, identified by their package name. +cogs: + - + - +# List of cogs to load in development mode only, identified by their package name. +dev-cogs: + - + - +# List of NLTK datasets to download on startup. +nltk: + - + - +# List of Huggingface Transformers models to download on startup. +transformers: + - + - +``` + +!!! WARNING "Advanced Users Only" + + For standard use, the `application.yaml` file does not need to be changed. We recommend against modifying this + file unless you know what you're doing. + +One possible use case is to add additional feature or disable existing features by modifying the list of cogs. +To add a new cog, add the package name to the `cogs` list. To disable a feature, remove the package name from +the list. + +Below is a list of all cogs that are loaded by default: + +| Package Name | Description | +| ------------------------------------------------------- | -------------------------------------------------------------------------------- | +| `courageous_comets.cogs.about` | Provides information about the bot. | +| `courageous_comets.cogs.keywords.search_command` | Searches for keywords using a slash command. | +| `courageous_comets.cogs.keywords.search_context_menu` | Searches for keywords using a context menu item. | +| `courageous_comets.cogs.keywords.topics_command` | Lists the most popular keywords for a given context using a slash command. | +| `courageous_comets.cogs.keywords.user_context_menu` | Lists the most popular keywords for a particular user using a context menu item. | +| `courageous_comets.cogs.messages` | Listens for messages and passes them on for internal processing. | +| `courageous_comets.cogs.ping` | Responds to ping requests. | +| `courageous_comets.cogs.sentiment.message_context_menu` | Analyzes the sentiment of a message using a context menu item. | +| `courageous_comets.cogs.sentiment.search_command` | Searches for messages with similar sentiment using a slash command. | +| `courageous_comets.cogs.sentiment.search_context_menu` | Searches for messages with similar sentiment using a context menu item. | +| `courageous_comets.cogs.sentiment.user_context_menu` | Analyzes the sentiment of a user's messages using a context menu item. | +| `courageous_comets.cogs.frequency` | Provides an overview of the amount of recent messages for a given timespan. | diff --git a/courageous-comets/docs/admin-guide/deployment.md b/courageous-comets/docs/admin-guide/deployment.md new file mode 100644 index 0000000..4bebc50 --- /dev/null +++ b/courageous-comets/docs/admin-guide/deployment.md @@ -0,0 +1,103 @@ + + +# Deployment + +This section provides instructions on how to deploy the Courageous Comets application in a production environment. +Follow the steps below to set up the application. + +The application is deployed using [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/). +It consists of the following services: + +- [**courageous-comets**](https://github.com/thijsfranck/courageous-comets/pkgs/container/courageous-comets): + The Courageous Comets application. +- [**redis-stack**](https://hub.docker.com/r/redis/redis-stack-server): The Redis instance used to store data. + +By the end of this guide, all services will be running as Docker containers on your system. + +## Checklist + +- [ ] Set up Docker and Docker Compose on your system. +- [ ] Get the Docker Compose file. +- [ ] Set up a `.env` file. +- [ ] Configure your `DISCORD_TOKEN` in the `.env` file. +- [ ] Select the image versions (optional). +- [ ] Configure additional options (optional). +- [ ] Start the application using Docker Compose. + +## Get the Docker Compose File + +The application can be deployed using Docker Compose. You can use the `docker-compose.yaml` file provided in the +GitHub repository to start the application. + +[Get the Docker Compose :fontawesome-brands-docker:](https://github.com/thijsfranck/courageous-comets/blob//docker-compose.yaml){ .md-button .md-button--primary } + +Download the file and save it in any directory on your system. + +## Configuration + +Before starting the application, you need to configure the application settings. Create a `.env` file in the same +directory as the `docker-compose.yaml` file. + +The sections below will guide you through setting up a minimal configuration to start the application. + +??? QUESTION "What other configuration options are available?" + + Refer to the [configuration](configuration.md) section for a complete list of the available options. + +### Discord Token + +The application requires a valid Discord bot token to connect to Discord. Add the following line to the `.env` +file: + +```dotenv +DISCORD_TOKEN= +``` + +Replace `` with your Discord bot token. + +!!! DANGER "Security Warning" + + Keep your Discord bot token secure and do not share it with anyone! + +??? QUESTION "Where do I find my Discord bot token?" + + See the configuration section for instructions on [how to obtain a Discord bot token](./configuration.md#discord_token). + +### Image Versions + +By default, the application uses the latest version of each Docker image. To specify a particular version, you +can add the following variables to the `.env` file: + +```dotenv +COURAGEOUS_COMETS_VERSION= +REDIS_STACK_VERSION=latest +``` + +Replace `latest` with the tag corresponding to the version you want to use. + +??? QUESTION "Where can I find previous versions of the image?" + + Previous versions of the Courageous Comets image are available on the [GitHub Container Registry](https://github.com/thijsfranck/courageous-comets/pkgs/container/courageous-comets). + +## Start the Application + +!!! NOTE "Prerequisites" + + Please ensure that [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/) + are installed on your system, and that the Docker daemon is running. + +Once you have set up the configuration, you can start the application using Docker Compose. Open a terminal and +navigate to the directory where you saved the `docker-compose.yaml` file. Run the following command: + +```bash +docker-compose up -d +``` + +Docker Compose will start the application in the background. You can check the logs to verify that the application +has started successfully: + +```bash +docker-compose logs -f +``` + +You can now interact with the application in any Discord server where it has been installed. diff --git a/courageous-comets/docs/admin-guide/index.md b/courageous-comets/docs/admin-guide/index.md new file mode 100644 index 0000000..f6a5954 --- /dev/null +++ b/courageous-comets/docs/admin-guide/index.md @@ -0,0 +1,10 @@ +# Administrator Guide + +This guide is intended for administrators and developers. It provides instructions on how to deploy and configure +the application, as well as how to manage it in production. + +## Contents + +- [Deployment](./deployment.md): How to deploy the bot in a production environment. +- [Configuration](./configuration.md): How to configure your bot deployment. +- [Changelog](../CHANGELOG.md): The bot's changelog. diff --git a/courageous-comets/docs/assets/favicon.ico b/courageous-comets/docs/assets/favicon.ico new file mode 100644 index 0000000..5762dc3 Binary files /dev/null and b/courageous-comets/docs/assets/favicon.ico differ diff --git a/courageous-comets/docs/assets/logo-border.png b/courageous-comets/docs/assets/logo-border.png new file mode 100644 index 0000000..cef7363 Binary files /dev/null and b/courageous-comets/docs/assets/logo-border.png differ diff --git a/courageous-comets/docs/assets/logo.png b/courageous-comets/docs/assets/logo.png new file mode 100644 index 0000000..d72d213 Binary files /dev/null and b/courageous-comets/docs/assets/logo.png differ diff --git a/courageous-comets/docs/assets/user-guide/about.png b/courageous-comets/docs/assets/user-guide/about.png new file mode 100644 index 0000000..49721f4 Binary files /dev/null and b/courageous-comets/docs/assets/user-guide/about.png differ diff --git a/courageous-comets/docs/assets/user-guide/frequency.png b/courageous-comets/docs/assets/user-guide/frequency.png new file mode 100644 index 0000000..8f696cf Binary files /dev/null and b/courageous-comets/docs/assets/user-guide/frequency.png differ diff --git a/courageous-comets/docs/assets/user-guide/praise.png b/courageous-comets/docs/assets/user-guide/praise.png new file mode 100644 index 0000000..cb7d8f9 Binary files /dev/null and b/courageous-comets/docs/assets/user-guide/praise.png differ diff --git a/courageous-comets/docs/assets/user-guide/semantics-search.png b/courageous-comets/docs/assets/user-guide/semantics-search.png new file mode 100644 index 0000000..ab89fd4 Binary files /dev/null and b/courageous-comets/docs/assets/user-guide/semantics-search.png differ diff --git a/courageous-comets/docs/assets/user-guide/sentiment-search.png b/courageous-comets/docs/assets/user-guide/sentiment-search.png new file mode 100644 index 0000000..0ce957d Binary files /dev/null and b/courageous-comets/docs/assets/user-guide/sentiment-search.png differ diff --git a/courageous-comets/docs/assets/user-guide/topics.png b/courageous-comets/docs/assets/user-guide/topics.png new file mode 100644 index 0000000..1429954 Binary files /dev/null and b/courageous-comets/docs/assets/user-guide/topics.png differ diff --git a/courageous-comets/docs/assets/user-guide/user-sentiment.png b/courageous-comets/docs/assets/user-guide/user-sentiment.png new file mode 100644 index 0000000..309ada4 Binary files /dev/null and b/courageous-comets/docs/assets/user-guide/user-sentiment.png differ diff --git a/courageous-comets/docs/contributor-guide/architecture-design.md b/courageous-comets/docs/contributor-guide/architecture-design.md new file mode 100644 index 0000000..0365f5b --- /dev/null +++ b/courageous-comets/docs/contributor-guide/architecture-design.md @@ -0,0 +1,416 @@ +# Architecture & Design + +This page describes the architecture and design of the Courageous Comets application. + +## Components + +Below is a high-level view of the application components. Click on the links in the diagram to jump to the +corresponding details section. + +```mermaid +graph LR + subgraph Discord + User["User"] + end + + subgraph Courageous Comets + subgraph Bot[Bot] + subgraph Controllers[Controllers] + MessagesCog[messages] + Interactions[interactions] + end + subgraph ApplicationLogic[Application Logic] + Preprocessing{{Preprocessing}} + subgraph Processing + WordCount[Word Count] + Sentiment[Sentiment Analysis] + Vectorization[Vectorization] + end + end + end + subgraph Storage[Database] + Redis[("Redis")] + end + end + + User -->|"Messages"| MessagesCog + MessagesCog -->|"Text"| Preprocessing + Preprocessing --->|"Text"| WordCount + Preprocessing --->|"Text"| Sentiment + Preprocessing --->|"Text"| Vectorization + WordCount -->|"Token Count"| Redis + Sentiment -->|"Sentiment Scores"| Redis + Vectorization -->|"Vector Embedding"| Redis + User <-->|"Interactions"| Interactions + Interactions -->|"Queries"| Redis +``` + +### Bot + +The bot is the main component of the Courageous Comets application. It is responsible for processing messages +and performing analysis on them. + +The bot is built using the [discord.py](https://discordpy.readthedocs.io/en/stable/) library. We chose this library +since it's the most mature library for building Discord bots in Python and did not expect to need any special +features provided by other libraries. + +The application is designed to be modular and extensible, with the core features implemented on separate layers. + +#### Controllers + +Controllers are responsible for handling user input and invoking the appropriate application logic. All controllers +are implemented as Discord cogs, which are modular components that can be enabled or disabled based on the +application configuration. + +##### Messages + +The messages cog is highlighted in the diagram as it is the primary controller for processing messages. It listens +for messages sent by users, and passes them on for internal processing. + +##### Interactions + +Other cogs included with the bot are responsible for handling interactions with users, such as slash commands, +context menus, and buttons. Typically, the response to an interaction will be a UI element like an embed. Embeds +may include charts, tables, or other visualizations. + +##### Design Decisions + +Responses from the bot should typically be sent as ephemeral messages, meaning they are only visible to the user +who triggered the interaction. The exception to this is when an interaction is supposed to be visible to all users. + +When an interaction is triggered, the bot should respond as soon as possible to acknowledge the interaction. In +case of loading times, the bot should provide a loading indicator to the user. In case of errors, useful feedback +should be provided to the user in the form of an error message. + +We decided to have a dedicated cog per interaction. For example, there is a separate cog for searching for keywords +using a slash command and a separate cog for searching for keywords using a context menu item. This split makes +sure that each cog does not grow too large and remains maintainable. + +#### Application Logic + +The application logic is responsible for processing messages and performing analysis on them. The logic is divided +into several components: + +- **Preprocessing**: Cleans and normalizes the input text. +- **Word Count**: Counts the number of words in the input text. +- **Sentiment Analysis**: Analyzes the sentiment of the input text. +- **Vectorization**: Generates a vector representation to support similarity search. + +##### Preprocessing + +The preprocessing step is responsible for cleaning and normalizing the input text. This includes the following +steps: + +- Drop code blocks +- Drop links +- Replace special characters and diacritics with standard ASCII characters +- Expand contractions +- Drop punctuation +- Drop very long words +- Drop extra whitespace +- Truncate the text to a maximum length + +Preprocessing is essential to ensure that the input text is in a consistent format before further analysis is +performed and to avoid overloading the downstream components with irrelevant information. + +##### Word Count + +The word count analysis counts the number of keywords in the input message. This analysis is used to generate +the most popular topics in a channel or server. + +The first step in the word count analysis is to tokenize the input text. The text is split into individual words +using the NLTK library, which is a popular library for natural language processing in Python that provides a robust +tokenizer for English text out of the box. + +The tokenized text is then stemmed using NLTK's Snowball Stemmer. Stemming reduces words to their root form, which +helps to group similar words together. For example, the words "running" and "runs" would both be stemmed to "run". + +Next, we remove stopwords from the tokenized text. Stopwords are common words like "the", "and", and "is" that +do not carry much meaning and are typically removed from text before analysis. We also remove any words that are +single characters long, as these are unlikely to be meaningful. + +Finally, the keywords are counted and the results are stored in the database. + +##### Sentiment Analysis + +The sentiment analysis component is responsible for analyzing the attitude of an input message. This enables the +sentiment search feature and supports the moderation features of the bot. + +The sentiment analysisis performed using the NLTK library, which provides a pre-trained sentiment analysis model. +The model assigns polarity scores to the input text, which indicate the positive, negative, and neutral sentiment +of the text. + +Each polarity score ranges from 0 to 1, with 0 indicating the absence of the sentiment. All three scores sum up +to 1. + +The model also provides a compound score, which is a normalized combination of the positive, negative, and neutral +scores. Compound scores range from -1 (most negative) to 1 (most positive). + +The polarity scores and compound score for every message are stored in the database for later retrieval and analysis. + +##### Vectorization + +The vectorization component is responsible for generating a vector embedding of the input text. The vector +embeddings support similarity search, which allows users to find messages with similar meaning. + +Vectorization is done using the [all-MiniLM-L6-v2](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2) +transformer which maps sentences and paragraphs to a 384-dimensional dense vector space that captures the semantic +meaning of the input text. + +First, a tokenizer is trained using the model and used to tokenize the text. The token embeddings are computed +using Torch and then a pooling operation is applied on top of the contextualized word embeddings. +These embeddings are then normalized to generate a single embedding for the entire text. + +Finally, the embedding vector is converted to bytes and this bytes representation is stored in the database +for later retrieval and analysis. + +## Database + +The bot uses Redis as a database layer to store the results of the analysis. Redis is a fast and efficient key-value +store that offers search and query features needed to enable the application logic. Courageous Comets uses the +Redis Stack distribution, which includes plugins for full-text search and metric-based search. + +### Design Decisions + +We chose Redis over other databases like PostgreSQL or MongoDB because of its speed and simplicity. + +We knew that we'd be writing messages at a high rate and needed a database that could keep up with the volume +of data. Redis only writes to memory and periodically persists to disk, making it ideal for our workload of writing +a lot of small messages quickly. + +Further, we expected to need efficient full-text search capabilities and metric-based search capabilities. There +are plugins for Redis that provide these features. The Redis Stack distribution includes these plugins by default, +making it a good fit for our use case. + +Additionally, we expected not to require relational queries or complex joins, which are better suited for a relational +database like PostgreSQL. + +Redis is also easy to set up and configure, making it a good choice for a small-scale application like Courageous +Comets. + +There is a small risk of data loss in case of a crash. The application may lose data that Redis has not yet persisted +to disk. However, we are willing to accept this risk given that missing a few messages will not affect the overall +user experience. + +## Data Model + +The data model is designed to support a variety of analysis tasks and provide a flexible foundation for future +extensions. + +### Message + +The main entity of the data model is the `Message` object, which represents a message sent by a user. Messages +are structured as follows: + +| Name | Data Type | Index Type | Description | +| -------------------- | --------- | ---------- | -------------------------------------------------------------------------------------------- | +| `user_id` | `string` | `Tag` | The discord ID of the user who sent the message. | +| `message_id` | `string` | `Tag` | The discord message ID. | +| `channel_id` | `string` | `Tag` | The ID of the discord channel where the message was sent. | +| `guild_id` | `string` | `Tag` | The ID of the guild where the message was sent. | +| `timestamp` | `float` | `Numeric` | The UNIX timestamp of when the message was sent. | +| `sentiment_neg` | `float` | `Numeric` | The negative score of the sentiment analysis of the message. | +| `sentiment_pos` | `float` | `Numeric` | The positive score of the sentiment analysis of the message. | +| `sentiment_neu` | `float` | `Numeric` | The neutral score of the sentiment analysis of the message. | +| `sentiment_compound` | `float` | `Numeric` | The compound score of the sentiment analysis of the message. | +| `embedding` | `bytes` | `Vector` | The embedding vector of the message content | +| `tokens` | `string` | N/A | JSON object mapping each token in the message to number of times it appeared in the message. | + +### Design Decisions + +While the fields ending with `_id` are integers on Discord, they are stored as strings on Redis and indexed as +[Tags](https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/tags/) rather than +[Numeric](https://redis.io/docs/latest/develop/interact/search-and-query/basic-constructs/field-and-type-options/#numeric-fields) +because we want to make exact-match queries against these fields. Also, `Tags` are more memory-efficient and +faster to query. + +Rather than store the message as a JSON document with the sentiment-related values stored in a nested mapping, +they are stored on the same hash with a prefix of `sentiment_`. This is because JSON documents generally have +a larger memory footprint compared to the hash when searching over documents. Also, JSON documents take up more +space than the hash. For context, the JSON document representation of the message takes at least 14Kb while the +hash takes at most 4Kb. + +The [Cosine Similarity](https://en.wikipedia.org/wiki/Cosine_similarity) was chosen over the Euclidean distance +and Internal product as the distance metric for searching the vector embedding because we want to consider the +angle formed by two vectors (messages) and not their magnitude. + +The dimensions of the vector were chosen to match the dimension of the embedding generated by the [all-MiniLM-L6-v2](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2) +transformer. Ensure to set the correct dimension as the transformer model being used during index creation on Redis. + +The `tokens` field on the hash was not indexed as there was no need to search over it. Including it in the index +would increase the size of the index structure and add unnecessary overhead. However, we can still return it from +search-based queries. + +## Packages & Modules + +This section describes the packages included with the project and the underlying module structure. + +### `courageous_comets` + +The application is fully contained within the `courageous_comets` package. The package is structured as follows: + +| Module | Description | +| ------------------ | ---------------------------------------------------------------------------------------- | +| `cogs` | Provides the bot controllers (cogs) that handle user input. | +| `discord` | Implements functions for interacting with the Discord API. | +| `nltk` | Contains helpers for using the Natural Language Toolkit (NLTK) library. | +| `redis` | Provides the data access layer for interacting with Redis. | +| `transformers` | Contains helpers for working with Huggingface Transformers. | +| `ui` | Includes all UI elements for the bot (`charts`, `components`, `embeds`, and `views`). | +| `client.py` | Contains the main application client class. | +| `__init__.py` | Entrypoint for the package. Exports the application client instance. | +| `__main__.py` | Entrypoint for the application. Responsible for setup, teardown and root error handling. | +| `enums.py` | Shared enumerations used across the application. | +| `exceptions.py` | Includes the base exception class and custom exceptions used in the application. | +| `models.py` | Defines the entities used by the application using `pydantic` models. | +| `preprocessing.py` | Contains the preprocessing logic for cleaning and normalizing text. | +| `processing.py` | Implements the main processing logic for analyzing messages. | +| `sentiment.py` | Implements the sentiment analysis logic using the NLTK library. | +| `settings.py` | Provides input validation, default values and type hints for the app settings. | +| `utils.py` | Contains utility functions used across the application. | +| `vectorizer.py` | Implements the vectorization logic using the Huggingface Transformers library. | +| `words.py` | Contains the word count logic for counting the number of words in a text. | + +### `tests` + +The `tests` package organizes the test suite for the application. The package is structured as follows: + +| Module | Description | +| ------------------- | --------------------------------------------------------------------------------- | +| `conftest.py` | Contains shared fixtures and ensures NLTK and Huggingface data is loaded in CI. | +| `courageous_comets` | Includes tests that validate the behavior of each application module. | +| `integrations` | Provides tests that validate how the app interacts with Discord and the database. | + +## CI/CD Pipeline + +The CI/CD (Continuous Integration and Continuous Deployment) pipeline automates the build, test, and deployment +processes of the application. This achieves the following goals: + +1. The process ensures that code changes are consistently tested, maintaining high code quality and reliability +2. It also enables full traceability and reproducibility of any + artifact released to production. + +This section provides a comprehensive overview of each stage in the pipeline, its purpose, and its components. + +### Overview + +The diagram below illustrates the stages of the CI/CD pipeline and the flow of code changes from development to +deployment. The pipeline consists of the [development flow](#development-flow) and the release flow. + +```mermaid +graph TB + subgraph Local["Development Environment"] + subgraph Developer["Developer"] + Codebase[("Codebase")] + Commit["Commit Changes"] + Release["Create Release"] + end + subgraph Precommit["Pre-commit"] + CodeQuality["Code Quality"] + CommitQuality["Commit Quality"] + end + subgraph Commitizen["Release"] + Tag["Create Tag"] + Changelog["Generate Changelog"] + end + end + + subgraph Remote["GitHub"] + subgraph CI["Continuous Integration"] + Unit["Unit Tests"] + Integration["Integration Tests"] + PeerReview["Peer Review"] + end + subgraph Repository["Repository"] + Main[("Main Branch")] + Version[("vX.Y.Z Tag")] + end + subgraph CD["Continuous Deployment"] + Docker["Build Docker Image"] + GHCR[("GitHub Container Registry")] + MkDocs["Build Documentation"] + Pages[("GitHub Pages")] + end + end + + Codebase --> Commit + Codebase --> Release + + Commit -->|"Commit"| CodeQuality + CodeQuality --> CommitQuality + CommitQuality -->|"Push"| Unit + Unit --> Integration + Integration --> PeerReview + PeerReview -->|"Merge"| Main + + Release -->|"Bump"| Tag + Tag --> Changelog + Changelog ----->|"Push"| Version + Version --> Docker + Docker --> GHCR + Version --> MkDocs + MkDocs --> Pages +``` + +### Development Flow + +In the development flow, developers are responsible for making changes to the codebase and committing these changes +to the repository. When changes are ready, developers commit these changes, triggering the pre-commit checks. + +#### Pre-commit + +The pre-commit checks involve several steps to ensure that the codebase and the repository stay in a maintainable +state. These steps are executed locally using the [`pre-commit`](https://pre-commit.com/) framework and block +the commit if any of the checks fail. + +##### Code Quality + +First, the code is formatted and linted using [`ruff`](https://docs.astral.sh/ruff/). Next, the code is type-checked +using [`pyright`](https://github.com/microsoft/pyright). Markdown is also treated as code for this project, so +it is linted using [`markdownlint`](https://github.com/DavidAnson/markdownlint). + +##### Commit Quality + +Since commit messages are a key input to the release process, they validated using [`commitizen`](https://commitizen-tools.github.io/commitizen/). +Commitizen enforces a consistent commit message format and ensures that the messages follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) +specification. + +#### Continuous Integration + +Once changes are pushed to the repository, the continuous integration process starts. In this stage, a series +of tests are run to ensure the new code works as expected and does not introduce regressions. + +Unit tests are in place to validate the behavior of individual components, while integration tests validate the +interaction between components. We use the [`pytest`](https://docs.pytest.org/) framework to run the tests. + +All [pre-commit](#pre-commit) checks are also run in the CI pipeline to avoid any discrepancies between the local +and remote environments. + +When all tests and checks pass, there is a final peer review before the changes are merged into the main branch. + +### Release Flow + +The release flow is triggered when a developer bumps the version of the application and creates a new release. + +#### Release + +The first part of the release flow involves determining the version number for the new release. This is done using +[`Commitizen`](https://commitizen-tools.github.io/commitizen/), which automatically increments the version based +on the commit messages. Version numbers follow the [Semantic Versioning](https://semver.org/) specification. + +Secondly, a changelog is generated using [`commitizen`](https://commitizen-tools.github.io/commitizen/). The changelog +provides a summary of the changes included in the release, making it easier for users to understand what has been +updated. + +The new changelog is committed to the repository, and the version tag is created. This tag is used to trigger the +continuous deployment process. + +#### Continuous Deployment + +The continuous deployment process involves building the Docker image for the application and pushing it to the +[GitHub Container Registry](https://github.com/features/packages) (GHCR). The Docker image is versioned +on the release tag. + +Additionally, the documentation is built using [MkDocs](https://www.mkdocs.org/) and publishedb to GitHub Pages. +The documentation is also tagged with the release version using [mike](https://github.com/jimporter/mike). This +way, users can access the documentation corresponding to the version of the app they are using. diff --git a/courageous-comets/docs/contributor-guide/development-environment.md b/courageous-comets/docs/contributor-guide/development-environment.md new file mode 100644 index 0000000..c6c2a2a --- /dev/null +++ b/courageous-comets/docs/contributor-guide/development-environment.md @@ -0,0 +1,203 @@ +# Development Environment + +Follow the steps below to set up your development environment. + +!!! NOTE "Prerequisites" + + You need to have [Git](https://git-scm.com) installed on your system. + +## Environment Setup + +You can set up the development environment using either the [development container](#using-the-development-container) +or following the [manual](#manual-setup) setup process. + +### Using the Development Container + +The project includes a [development container](https://containers.dev) to automatically set up your development +environment, including the all tools and dependencies required to develop the application locally. + +!!! NOTE "Prerequisites" + + [Docker](https://www.docker.com) must be installed on your system to use the development container. + +#### Quick Start + +See the video installation guide below for a step-by-step tutorial on installing the development container with +Visual Studio Code: + + + +First, install the [Remote Development Extension Pack](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack). +Next, open Visual Studio Code and click on icon in the bottom left corner to open the command palette for remote +environments. + +Select the `Clone Repository in Container Volume` command. This will prompt you to select the +repository to clone. Choose the `thijsfranck/courageous-comets` repository or paste the repository URL. + +Once you confirm the selection, the development container will be set up automatically. + +#### Detailed Setup Guide + +For more details, refer to the setup guide for your IDE: + +- [Visual Studio Code](https://code.visualstudio.com/docs/devcontainers/tutorial) +- [PyCharm](https://www.jetbrains.com/help/pycharm/connect-to-devcontainer.html) + +#### Services + +The development container includes the following services for local development: + +| Service | Description | Address | +| ------------ | ------------ | ----------------------------------------- | +| Redis | Database | [`localhost:6379`](http://localhost:6379) | +| RedisInsight | Database GUI | [`localhost:8001`](http://localhost:8001) | + +### Manual Setup + +If you prefer to set up the development environment manually, follow the steps below. + +!!! NOTE "Prerequisites" + + Please ensure [Python 3.12](https://www.python.org) and [Poetry](https://python-poetry.org) are installed + on your system. + +#### Clone the Repository + +To clone the repository, run the following command: + +```bash +git clone https://github.com/thijsfranck/courageous-comets.git +``` + +Next, open the project in your preferred IDE or navigate to the project directory using the terminal. + +#### Install Dependencies + +Start by installing the project dependencies using Poetry: + +```bash +poetry install +``` + +This will create a virtual environment and install the required dependencies. + +#### Pre-commit Hooks + +Next, install the pre-commit hooks to ensure that your code is formatted and linted before each commit: + +```bash +poetry run pre-commit install +``` + +This will set up the pre-commit hooks to run automatically when you commit changes to the repository. + +#### Redis Database + +The application requires a Redis database to run. We recommend setting up a local Redis instance using Docker +for development purposes. + +To start a Redis instance using Docker, run the following command: + +```bash +docker run -d -p 6379:6379 -p 8001:8001 redis/redis-stack:latest +``` + +This will start a Redis server on port `6379` and a RedisInsight GUI on port `8001`. + +## Configuring your Environment + +To run the application, you will need to provide the following configurations in a `.env` file at the project +root directory. + +### Discord Token + +The application requires a Discord bot token to run. It should be stored in the `.env` file as follows: + +```dotenv +DISCORD_TOKEN= +``` + +The repository includes an encrypted `.env.lock` file with the shared Discord bot token for our team. Follow the +[Secrets Management](./secrets-management.md) guide to decrypt the file and start using the token. + +??? QUESTION "Can I use my own Discord bot token?" + + Yes, you can use your own Discord bot token. If you do so, there's need to decrypt the `.env.lock` file. + +### Redis Configuration + +If you're not using the development container, you will need to configure the Redis connection in the `.env` file: + +```dotenv +REDIS_HOST=localhost +REDIS_PORT=6379 +``` + +This configuration assumes you are running a local Redis instance on the default port. + +## Running the Application + +With your development environment set up and configured, you can run the application using the following command: + +```bash +poetry run python -m courageous_comets +``` + +The application should now be online and ready to respond to input from your Discord server. + +## Building the Docker Image + +!!! INFO "Production Builds" + + The release process is fully automated and does not require you to build the docker image locally. See the + [GitHub Actions](./version-control.md#github-actions) section of the version control guide for more information. + +Before building the Docker image, first build the Python package with Poetry: + +```bash +poetry build -f wheel +``` + +This will create a `.whl` file in the `dist` directory. Next, build the Docker image as follows: + +```bash +docker build -t ghcr.io/thijsfranck/courageous-comets:latest . +``` + +## Running the Docker Container + +Once you have [built the Docker image](#building-the-docker-image), use the following command to run the production +container locally: + +```bash +docker run -i --env-file .env ghcr.io/thijsfranck/courageous-comets:latest +``` + +This will run the application just as it would in production, using your local `.env` file and Redis instance. + +## Running the Docker Compose Stack + +To run the application in a production configuration including the Redis database, you can use the Docker Compose +stack. + +First, build the Docker image as described in [the previous section](#building-the-docker-image). Then, run the +following command: + +```bash +docker-compose up +``` + +This will start the application and the Redis database in separate containers using your local `.env` file. + +## Running the Documentation + +To view the documentation locally, you can use the following command: + +```bash +poetry run mkdocs serve +``` + +Open your browser and navigate to [`http://localhost:8000`](http://localhost:8000) to view the documentation. +The changes you make to the documentation will be automatically reflected in the browser. diff --git a/courageous-comets/docs/contributor-guide/documentation.md b/courageous-comets/docs/contributor-guide/documentation.md new file mode 100644 index 0000000..0b49af4 --- /dev/null +++ b/courageous-comets/docs/contributor-guide/documentation.md @@ -0,0 +1,82 @@ +# Documentation + +Good code documentation aids understanding and speeds up the development process. Follow the guidelines below +to document your code effectively. + +## What to Document + +Always document the following elements of your code: + +1. **Classes**, including their **attributes and public methods** +2. **Module-level functions and constants** + +Prioritize documenting public methods and attributes (those not starting with an underscore). However, private +methods with complex logic should also be documented for clarity. + +## Docstring Format + +This project uses numpy-style docstrings. Refer to the [style guide](https://numpydoc.readthedocs.io/en/latest/format.html) +for the full specification and detailed examples. + +Here are some examples of how to write good documentation for functions and classes: + +??? EXAMPLE "Function Documentation" + + ```python + def example_function(param1: int, param2: str): + """ + One-line summary of the function. + + Detailed functional description of what the function does. Can span + multiple lines. + + Parameters + ---------- + param1 : int + Description of the first parameter. + param2 : str + Description of the second parameter. + + Returns + ------- + bool + Description of the return value. + + Raises + ------ + ValueError + Description of the error. + + Examples + -------- + >>> example_function(1, "test") + True + """ + ... + ``` + +??? EXAMPLE "Class Documentation" + + ```python + class Example: + """ + Class-level docstring describing the class. + + Attributes + ---------- + attribute : int + Description of the attribute. + """ + ... + ``` + +## Type Annotations + +Python type annotations are strongly encouraged to improve code readability and maintainability. Use type annotations +for all parameters and return values, as well as class attributes. + +??? QUESTION "What are type annotations?" + + Type annotations are a way to specify the expected types of variables, function parameters, and return values + in Python code. They are used to improve code readability and catch type-related errors early. Refer to the + [official documentation](https://docs.python.org/3/library/typing.html) for more information. diff --git a/courageous-comets/docs/contributor-guide/index.md b/courageous-comets/docs/contributor-guide/index.md new file mode 100644 index 0000000..5748ee2 --- /dev/null +++ b/courageous-comets/docs/contributor-guide/index.md @@ -0,0 +1,13 @@ +# Contributor Guide + +The contributor guide is intended for developers working on this project. It provides instructions on how to +set up a new development environment, along with guidelines on version control, documentation, and testing. + +## Contents + +- [Architecture & Design](./architecture-design.md): How the application is structured and key design decisions. +- [Development Environment](./development-environment.md): How to set up your development environment. +- [Secrets Management](./secrets-management.md): How to manage secrets securely in the project. +- [Version Control](./version-control.md): How to manage changes using version control. +- [Documentation](./documentation.md): How to write good documentation. +- [Testing](./testing.md): How to test the application. diff --git a/courageous-comets/docs/contributor-guide/secrets-management.md b/courageous-comets/docs/contributor-guide/secrets-management.md new file mode 100644 index 0000000..ad5b271 --- /dev/null +++ b/courageous-comets/docs/contributor-guide/secrets-management.md @@ -0,0 +1,141 @@ +# Secrets Management + +To use our team's shared Discord bot token, you will need to retrieve it from the `.env.lock` file in the project +root directory. This section will guide you through the process of decrypting the file to access the token. + +## Install Tools + +First, you will need to install [`age`](https://github.com/FiloSottile/age) and [`SOPS`](https://github.com/getsops/sops) +on your system. Follow the instructions for your operating system below. + +=== "Windows" + + Open a PowerShell terminal and run the following command to install `SOPS`: + + ```bash + winget install -e --id Mozilla.SOPS + ``` + + To install `age`, download the [latest binary for Windows](https://github.com/FiloSottile/age/releases) and + add your `age` binary to the system `PATH`. + +=== "macOS" + + Open a terminal and run the following command: + + ```bash + brew install age sops + ``` + +=== "Linux" + + Download the [`SOPS` binary](https://github.com/getsops/sops/releases) for your platform. For instance, if + you are on an amd64 architecture: + + ```bash + curl -LO https://github.com/getsops/sops/releases/download/v3.9.0/sops-v3.9.0.linux.amd64 + ``` + + Move the binary into your `PATH`: + + ```bash + mv sops-v3.9.0.linux.amd64 /usr/local/bin/sops + ``` + + Make the binary executable: + + ```bash + chmod +x /usr/local/bin/sops + ``` + + Finally, install `age`: + + ```bash + sudo apt-get install -y age + ``` + +=== "Development Container" + + If you are using the development container, the tools are already installed! 🎉 + +## Generate Keys + +??? TIP "Using the development container" + + The development container automatically generates a key pair for you on initial setup. You public key will + be shown in the terminal output. You can also find it later in the `secrets/keys.txt` file. + +Next, you will need to generate a new key pair using `age`. Run the following command from the root directory of +the project: + +```bash +age-keygen -o > secrets/keys.txt +``` + +This will create a new key pair and save it to the `secrets/keys.txt` file. Share your public key with the team +so it can be registered. + +!!! DANGER "Security Warning" + + Only your public key can be safely shared. Do not share the private key with anyone! + +??? QUESTION "Where can I find my public key?" + + You can find your public key in the `secrets/keys.txt` file or in the terminal output after generating the + key pair. + +## Registering a new Public Key + +!!! NOTE "Prerequisite" + + This step needs to be performed by a team member who already has access to the `.env` file. + +To register a new public key, first extend the `.sops.yaml` file in the project root directory. +Add the public key to the list of `age` keys. Each key is separated by a comma and a newline. + +```yaml +creation_rules: + - age: >- + , + , + +``` + +Next, [encrypt](#encrypting-secrets) the `.env` file with the updated list of keys and push it to the repository. + +## Decrypting Secrets + +Once your public key is added to the `.env.lock` file, you can decrypt the file to access the Discord bot token. +First, pull the latest changes from the repository: + +```bash +git pull +``` + +!!! NOTE "Prerequisite" + + `SOPS` requires the `SOPS_AGE_KEY_FILE` environment variable to be set to the path of your private key file. + This is automatically set up in the development container. + +Next, run the following command to decrypt the `.env.lock` file: + +```bash +sops decrypt --input-type dotenv --output-type dotenv .env.lock > .env +``` + +This will decrypt the file and save the contents to a new `.env` file in the project root directory. You can now +access the Discord bot token. + +!!! DANGER "Security Warning" + + Do not commit your decrypted `.env` file to version control or share the contents with anyone! + +## Encrypting Secrets + +To encrypt the `.env` file after making changes, run the following command: + +```bash +sops encrypt .env > .env.lock +``` + +This will encrypt the file and save it to the `.env.lock` file. You can now commit the changes to version control. diff --git a/courageous-comets/docs/contributor-guide/testing.md b/courageous-comets/docs/contributor-guide/testing.md new file mode 100644 index 0000000..b5b8e97 --- /dev/null +++ b/courageous-comets/docs/contributor-guide/testing.md @@ -0,0 +1,112 @@ +# Testing + +Automated tests are key to our success, since they allow us to catch bugs early, run sections of code in isolation, +and accelerate our development pace. + +## Structure + +Test modules should be located in the `tests` directory at the root of the project. The `tests` directory is further +divided into subdirectories for unit tests and integration tests. Each unit tests module should have a corresponding +module in the `courageous_comets` package. + +??? EXAMPLE "Test Module Structure" + + ```plaintext + project_root/ + ├── courageous_comets/ + │ ├── __init__.py + │ └── example.py + ├── tests/ + | ├── conftest.py + │ ├── courageous_comets/ + │ │ ├── __init__.py + │ │ └── test__example.py + │ └── integrations/ + │ ├── __init__.py + │ └── test__integration.py + └── ... + ``` + +## Running Tests + +We use the [`pytest`](https://docs.pytest.org) framework for writing and running our tests. To run the tests, +use the following command from the root of the project: + +```bash +poetry run pytest +``` + +This command will discover and run all the tests modules that match the pattern `test__*.py`. + +??? TIP "Running Tests in your IDE" + + Most modern IDEs have built-in support for running tests. You can run tests directly from your IDE, which + can be more convenient than running them from the command line. + + - [Visual Studio Code](https://code.visualstudio.com/docs/python/testing) + - [PyCharm](https://www.jetbrains.com/help/pycharm/pytest.html) + + The development container is pre-configured for using `pytest` in Visual Studio Code. + +## What to Test + +Unit tests should cover the following aspects of your code: + +- Input validation +- Correctness of output (or outcome) given a valid input +- Error handling + +??? TIP "Consider Edge Cases" + + When writing tests, consider edge cases such as invalid inputs and unexpected behavior. These are often the + areas where bugs are most likely to occur. + +Some parts of the code may be more critical than others. Focus on writing tests for the most critical parts of +the codebase, such as complex algorithms, core functionality or user-facing features. + +## Writing Tests + +Each test case should be self-contained and independent of other tests. This means that each test should set up +its own data and clean up after itself. Avoid relying on the state of other tests or the order in which tests +are run. + +When writing tests, follow these guidelines: + +- Use descriptive test names that clearly indicate what is being tested. +- Limit each test to a single logical concept. +- Use the `assert` statement to check the expected outcome of the test. +- Aim for one `assert` statement per test. +- Use [fixtures](https://docs.pytest.org/en/latest/explanation/fixtures.html) to set up common data or resources. + +??? EXAMPLE "Example Tests" + + The `examples` folder includes sample tests that you can use as a base for your own test. + +## Unit Testing and Type Annotations + +You can reduce the need for unit tests by indicating the expected types of input arguments and return values as +type annotations. While they don't replace unit tests, type annotations can reduce the number of tests you might +need to write, particularly those related to input validation. + +For instance, consider the following function without type annotations: + +???+ EXAMPLE "Function Without Type Annotations" + + ```python + def add(a, b): + return a + b + ``` + +Without type annotations, you might write multiple tests to ensure that the function behaves correctly with different +types of input, like strings, integers, or floats. But with type annotations: + +???+ EXAMPLE "Function With Type Annotations" + + ```python + def add(a: int, b: int) -> int: + return a + b + ``` + +The function's expected behavior is clearer. You know that both `a` and `b` should be integers, and the return +value will also be an integer. With these type annotations in place, there's less need to write unit tests checking +for behaviors with non-integer inputs since the static type checker can catch those mistakes for you. diff --git a/courageous-comets/docs/contributor-guide/version-control.md b/courageous-comets/docs/contributor-guide/version-control.md new file mode 100644 index 0000000..e4d11eb --- /dev/null +++ b/courageous-comets/docs/contributor-guide/version-control.md @@ -0,0 +1,236 @@ +# Version Control + +When making changes to the project, follow these guidelines. + +## Branching + +Always create a new branch for your changes. This makes it easier to handle multiple contributions simultaneously. + +First, pull the latest changes from the `main` branch: + +```bash +git pull main +``` + +Next, create a new branch with the following command: + +```bash +git checkout -b "" +``` + +Replace `` with a short, descriptive name for your branch. For example, `add-uptime-command`. + +## Commits + +Commits should follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification. +This helps maintain a clean and structured commit history. + +Try to keep your commits focused on a single task. If you need to make multiple changes, create separate commits +for each change. + +??? EXAMPLE "Conventional Commit Format" + + Here's an example of a good commit message: + + ```plaintext + feat: add uptime command + + Add a new command to display the bot's uptime. + ``` + +??? TIP "Use Commitizen" + + The workspace includes [Commitizen](https://commitizen-tools.github.io/commitizen/) to help you write conventional + commit messages. Run the following command to create a commit message interactively: + + ```bash + poetry run cz commit + ``` + +### Automated Checks + +The project includes pre-commit hooks to ensure your code meets the quality standards. These hooks run automatically +before each commit. + +The pre-commit hooks include: + +- Linting and formatting with [Ruff](https://docs.astral.sh/ruff/) +- Commit message validation with [Commitizen](https://commitizen-tools.github.io/commitizen/) + +??? QUESTION "What if the pre-commit hooks fail?" + + If the pre-commit hooks fail, you will need to address the issues before committing your changes. Follow the + instructions provided by the pre-commit hooks to identify and fix the issues. + +??? QUESTION "How do I run the pre-commit hooks manually?" + + Pre-commit hooks can also be run manually using the following command: + + ```bash + poetry run pre-commit + ``` + +The pre-commit hooks are intended to help us keep the codebase maintainable. If there are rules that you believe +are too strict, please discuss them with the team. + +## Pull Requests + +Once you have completed your changes, it's time to create a pull request. A pull request allows your changes to +be reviewed and merged into the `main` branch. + +Before creating a pull request, ensure your branch is up to date with the latest changes from the `main` branch: + +```bash +git pull main +``` + +Next, push your changes to the repository: + +```bash +git push +``` + +Finally, [create a pull request on GitHub](https://github.com/thijsfranck/courageous-comets/compare). Select +your branch as the source and the `main` branch as the base. + +In the pull request description, provide a brief overview of the changes and any relevant information for reviewers. + +??? EXAMPLE "Pull Request Description" + + Here's an example of a good pull request description: + + ```plaintext + # feat: add uptime command + + This pull request adds a new uptime command to display the bot's uptime. + + ## Changes + + - Added a new command to display the bot's uptime + - Updated the help command to include information about the new command + + ## Notes + + - The new command is implemented in a separate file for better organization + - The command has been tested locally and works as expected + ``` + +### Automated Checks + +The project includes automated checks to ensure the code meets the quality standards. These checks include: + +- All [pre-commit hooks](#automated-checks) must pass +- Type checking with [Pyright](https://github.com/microsoft/pyright) +- Running all tests with [pytest](https://docs.pytest.org/en/stable/) + +??? QUESTION "What if the automated checks fail?" + + If any of the automated checks fail, please address the issues before requesting a review. Feedback from the + automated checks should be available in the pull request checks tab. + +### Code Review + +All pull requests should be reviewed by at least one other team member before merging. The reviewer will provide +feedback and suggestions for improvement. + +Once the reviewer approves the pull request, you can merge it into the `main` branch. + +??? QUESTION "How do I request a review?" + + Request a review from a team member by [assigning them as a reviewer](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/requesting-a-pull-request-review) + to your pull request. + +#### Giving Feedback + +When providing feedback on a pull request, be constructive and specific. Point out areas for improvement and suggest +possible solutions. If you have any questions or concerns, don't hesitate to ask the author for clarification. + +A code review should focus on the following aspects: + +- Correctness and functionality +- Code quality and readability +- Adherence to the project guidelines + +??? EXAMPLE "Good Code Review Feedback" + + Here are some examples of good code review feedback: + + ```plaintext + - Great work on the new command! The implementation looks good overall. + - I noticed a small typo in the docstring. Could you update it to fix the typo? + - The logic in the new command is a bit complex. Consider breaking it down into smaller functions for clarity. + - The tests cover most of the functionality, but we are missing a test case for edge case X. Could you add a test for that? + ``` + +Always be respectful and considerate when giving feedback. Remember that the goal is to improve the code and help +the author grow as a developer. + +!!! SUCCESS "Be Positive" + + Don't forget to acknowledge the positive aspects of the contribution as well! + +## Release + +Releases are managed through [Commitizen](https://commitizen-tools.github.io/commitizen/). To generate a +new release, run the following command: + +```bash +poetry run cz bump +``` + +This command will automatically determine the next version number based on the commit history and generate a +new tag. It will also update the changelog with the latest changes. To push the changes to the repository, run: + +```bash +git push && git push --tags +``` + +The release will trigger a [GitHub actions workflow](#github-actions) to build and publish a new version of the +Docker image and update the documentation. + +??? TIP "Dry Run" + + You can perform a dry run to see the changes that will be made without actually committing them: + + ```bash + poetry run cz bump --dry-run + ``` + +??? TIP "Commitizen and Conventional Commits" + + Commitizen uses the commit messages to determine the type of changes and generate the release notes. + Make sure to follow the [commit message guidelines](#commits) to ensure accurate release notes. + +### Semantic Versioning + +Tags should be unique and follow the [Semantic Versioning](https://semver.org/) format. +Semantic version numbers consist of three parts: `major.minor.patch`. For example, `1.0.0`. + +To calculate the next version number, follow these guidelines: + +- For _bug fixes_ or _minor improvements_, increment the patch version. +- For _new features_ or _significant improvements_, increment the minor version. +- For **breaking changes**, increment the major version. + +??? QUESTION "What is a breaking change?" + + A breaking change requires users to change the way they use the software. Examples include removal of features + or backwards-incompatible API changes. + +??? EXAMPLE "Semantic Versioning" + + Here are some examples of version increments: + + - Bug fixes: `1.0.0` -> `1.0.1` + - New features: `1.0.1` -> `1.1.0` + - Breaking changes: `1.1.0` -> `2.0.0` + +### GitHub Actions + +A GitHub actions workflow will automatically build and publish a new version of the Docker image when a new tag +is pushed to the repository. + +The updated image will be available on the [GitHub Container Registry](https://github.com/thijsfranck/courageous-comets/pkgs/container/courageous-comets) +with both the release tag and the `latest` tag. + +The GitHub actions workflow also updates the documentation to reflect the new release. diff --git a/courageous-comets/docs/replace-token.sh b/courageous-comets/docs/replace-token.sh new file mode 100644 index 0000000..1b75d07 --- /dev/null +++ b/courageous-comets/docs/replace-token.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# Check if the correct number of arguments is provided +if [ "$#" -ne 2 ]; then + echo "Usage: $0 " + exit 1 +fi + +# Assign arguments to variables +token=$1 +replacement=$2 + +# Get the directory where the script is located +docs_dir=$(dirname "$0") + +# Find and iterate over all .md files in the docs directory and its subdirectories +find "$docs_dir" -name "*.md" | while read -r file; +do + # Replace the token with the replacement in each file + sed -i.bak "s/$token/$replacement/g" "$file" + # Remove the backup file + rm "$file.bak" +done + +echo "Token '$token' replaced with '$replacement' in all .md files in the '$docs_dir' directory." diff --git a/courageous-comets/docs/stylesheets/table.css b/courageous-comets/docs/stylesheets/table.css new file mode 100644 index 0000000..95cff73 --- /dev/null +++ b/courageous-comets/docs/stylesheets/table.css @@ -0,0 +1,11 @@ +/* + * Make tables full width by default + */ + + .md-typeset__table { + min-width: 100%; +} + +.md-typeset table:not([class]) { + display: table; +} diff --git a/courageous-comets/docs/user-guide/data-privacy.md b/courageous-comets/docs/user-guide/data-privacy.md new file mode 100644 index 0000000..c02d71c --- /dev/null +++ b/courageous-comets/docs/user-guide/data-privacy.md @@ -0,0 +1,15 @@ +# Data Privacy + +From the moment the bot is added to the server, it scans all messages sent by users to gather the data necessary +for responding to interactions. + +While raw messages are not stored, the bot processes messages to identify keywords. The identified keywords, along +with their count and byte representation, are stored in the database. Additionally, the Discord IDs for the message, +user, channel, and server are recorded. + +For displaying messages as part of search results, the bot interacts with the Discord API to fetch the message +content. The user who requested the search results can view the message content, even if they were not part of +the original conversation. Messages are cached in the bot's memory for a limited time to improve performance. + +Apart from interactions with the Discord API, no data is shared with third parties, and all data is securely stored +on the bot's server. diff --git a/courageous-comets/docs/user-guide/getting-started.md b/courageous-comets/docs/user-guide/getting-started.md new file mode 100644 index 0000000..dc36335 --- /dev/null +++ b/courageous-comets/docs/user-guide/getting-started.md @@ -0,0 +1,153 @@ +# Getting Started + +Joining a new Discord server can sometimes feel overwhelming. With Courageous Comets, you can quickly get a sense +of what a server is all about and decide if it's the right place for you. + +Whether you're exploring a new community or trying to find where you fit in, Courageous Comets makes it easy. + +The following sections will walk you through common interactions with the bot. For a complete list of available +interactions, refer to the [Interactions Overview](#interactions-overview) at the end of this guide. + +## Finding Your Way Around + +Let's say you've just joined a new server and you are interested in web development with Django. We'll use the +search feature to find the most recent messages related to those topics. + +Type the following command in any channel: + +```plaintext +/search query:"web development with Django" +``` + +The bot will return a list of messages that are relevant to your query. The search results include references to +the channels and message authors, so you can get an idea of where the conversation is happening. + +
+ [![Search Results](../assets/user-guide/semantics-search.png)](../assets/user-guide/semantics-search.png) +
Search Results
+
+ +The search feature is also available as a context menu option when you right-click on a message. Use this when +you see an interesting message and want to find more like it. + +## Discovering Like-Minded People + +If you're looking for a new community to join, you can ask the bot to find channels and users that share your mindset. +Let's say you're excited to learn more about programming and are looking for people who feel the same way. + +Try the following command: + +```plaintext +/sentiment_search query:"excited to learn about programming" +``` + +The bot will return a list of messages that share your sentiment. Again, the results include the channels and authors +of the messages to help you explore further. + +
+ [![Sentiment Search Results](../assets/user-guide/sentiment-search.png)](../assets/user-guide/sentiment-search.png) +
Sentiment Search Results
+
+ +The sentiment search feature is also available as a context menu option when you right-click on a message. +Use this when you find a message that resonates with you and want to find more like it. + +## Getting to Know the Community + +Once you've found a community you're interested in, you might want to learn more about the people who are active +there. + +### Popular Topics + +You can use the `topics` interaction to see a summary of what's being discussed on a server, channel, or +by a specific user. + +For example, to see the most popular topics in the Python Discord server, type: + +```plaintext +/topics scope:GUILD +``` + +The bot will return an overview of the most popular topics in the server. + +
+ [![Popular Topics](../assets/user-guide/topics.png)](../assets/user-guide/topics.png) +
Popular Topics
+
+ +You can also use the `Show user interests` context menu option to see what a particular user likes to talk about. + +### Most Active Times + +If you're curious about when the server is most active, you can use the `frequency` interaction to see a graph +of message frequency over a given time period. + +For example, to see the daily message frequency for the last 7 days, type: + +```plaintext +/frequency duration:DAILY +``` + +
+ [![Message Frequency](../assets/user-guide/frequency.png)](../assets/user-guide/frequency.png) +
Most Active Times
+
+ +Alternatively you can specify `HOURLY` to get the hourly message frequency for the last 24 hours, or `MINUTE` +for the last 60 minutes. + +## Contributing to a Safe Environment + +What's better than finding a server that aligns with your interests? When that server is filled +with friendly and welcoming people! + +Courageous Comets helps moderators maintain a positive and safe environment in their servers. Use the bot to identify +the most helpful members of the community, or to detect toxic behavior and spam. + +Right-click on any user or message and use the sentiment analysis interaction to get an overview of their attitude. + +
+ [![User Sentiment](../assets/user-guide/user-sentiment.png)](../assets/user-guide/user-sentiment.png) +
User Sentiment
+
+ +If a user's attitude is consistently positive, the bot provides a mechanism to praise them for their contributions. +When you praise a user, the bot will send them a message to let them know they're appreciated! + +
+ [![Praise](../assets/user-guide/praise.png)](../assets/user-guide/praise.png) +
Praise Message
+
+ +Likewise, if the sentiment analysis indicates negative or toxic behavior, this can be addressed by the moderation +team. + +## About the Bot + +If you ever need a quick overview of the bot and its features, you can use the `about` interaction. + +```plaintext +/about +``` + +The bot will provide a brief description of its capabilities and how to use them. The message also includes a link +to this documentation for more detailed information! + +
+ [![About](../assets/user-guide/about.png)](../assets/user-guide/about.png) +
About the Bot
+
+ +## Interactions overview + +The table below lists all available interactions and in which context they can be used. + +| Interaction | Description | Available As | +| ------------------ | ----------------------------------------------------------------- | ------------------------------------ | +| Search | Find channels, users messages that match your interests. | `/search`, Message context | +| Popular Topics | Show the most popular topics for a given server, channel or user. | `/topics`, User context | +| Sentiment Search | Find channels, users and messages with similar attitudes. | `/sentiment_search`, Message context | +| Sentiment Analysis | Analyze the overall attitude of a message or a user. | User context, Message context | +| Frequency | Show the message frequency on a server over a given time period. | `/frequency` | +| About | Get a brief overview of the bot and its features. | `/about` | +| Ping | Check if the bot is online and responsive. | `/ping` | diff --git a/courageous-comets/docs/user-guide/index.md b/courageous-comets/docs/user-guide/index.md new file mode 100644 index 0000000..ca98efe --- /dev/null +++ b/courageous-comets/docs/user-guide/index.md @@ -0,0 +1,10 @@ +# User Guide + +Welcome to the user guide for the Courageous Comets Discord bot! This guide is intended for users of the app and +for those who want to learn more about its features and capabilities. + +## Contents + +- [Getting Started](./getting-started.md): An overview of the bot's basic features. +- [Installing the bot](./installing-the-bot.md): How to add the bot to your Discord server. +- [Data Privacy](./data-privacy.md): Information on how the bot handles your data. diff --git a/courageous-comets/docs/user-guide/installing-the-bot.md b/courageous-comets/docs/user-guide/installing-the-bot.md new file mode 100644 index 0000000..235418e --- /dev/null +++ b/courageous-comets/docs/user-guide/installing-the-bot.md @@ -0,0 +1,34 @@ + + +# Installing the Bot + +This guide will help you get started with the Courageous Comets Discord bot. You'll learn how to invite the bot +to your server and use its admin features. + +## Installation + +Adding the bot to your Discord server is easy - just click the button below! + +[Add to Discord :fontawesome-brands-discord:](https://discord.com/oauth2/authorize?client_id=1262672493978714174){ .md-button .md-button--primary } + +## Initial Setup + +Once the bot is added to your server, you will need to make the bot's interactions available to your users. You +can do so by mentioning the bot and using the `sync` command as shown below: + +```plaintext +@Courageous Comets sync +``` + +After running the `sync` command, the bot will confirm the number of interactions that are now available. + +??? TIP "Run the `sync` command regularly" + + It's a good idea to run the `sync` command regularly to stay up-to-date with the latest features and interactions. + +You're all set! The bot is now ready to use on your server. + +!!! SUCCESS "Results may vary initially" + + The quality of responses will improve over time as the bot collects more data. The more active your server + is, the faster the bot will learn. diff --git a/courageous-comets/examples/cog.py b/courageous-comets/examples/cog.py new file mode 100644 index 0000000..aff40de --- /dev/null +++ b/courageous-comets/examples/cog.py @@ -0,0 +1,34 @@ +import discord +from discord import app_commands +from discord.ext import commands + + +# Boilerplate cog code. +# To build a cog: +# 1. Copy this boilerplate to another file in the cogs directory +# 2. Rename the file and class +# 3. Add the file name to config.yaml +class Cog(commands.Cog): + """A boilerplate cog.""" + + def __init__(self, bot: commands.Bot) -> None: + self.bot = bot + + # Slash command handler + # In order for these to be seen in the discord UI, + # they must be synced (sunk?!) using the sync command + @app_commands.command(name="COMMAND_NAME") + async def _command_name(self, interaction: discord.Interaction) -> None: + """Command description.""" + + # Listen for events from discord + # Event reference can be found here: + # https://discordpy.readthedocs.io/en/stable/api.html#discord-api-events + @commands.Cog.listener(name="EVENT_NAME") + async def _event_name(self) -> None: + """Event handler description.""" + + +async def setup(bot: commands.Bot) -> None: + """Load the cog.""" + await bot.add_cog(Cog(bot)) diff --git a/courageous-comets/examples/test__example.py b/courageous-comets/examples/test__example.py new file mode 100644 index 0000000..0242673 --- /dev/null +++ b/courageous-comets/examples/test__example.py @@ -0,0 +1,76 @@ +""" +The `test__example` module contains example tests for a Python project. + +For guidelines on writing good tests, see the contributor guide. +""" + +import pytest + + +@pytest.fixture() +def example_data() -> list[int]: + """ + Fixture that returns a list of integers for testing. + + Returns + ------- + list[int] + A list of integers. + """ + return [1, 2, 3, 4, 5] + + +def test__addition() -> None: + """ + Example of a basic test case. + + Asserts + ------- + - 1 + 1 is equal to 2. + """ + assert 1 + 1 == 2 + + +def test__sum_with_fixture(example_data: list[int]) -> None: + """ + Example of a test case that uses a fixture. + + Parameters + ---------- + example_data: list[int] + A list of integers. + + Asserts + ------- + - The sum of the example data is 15. + """ + assert sum(example_data) == 15 + + +@pytest.mark.parametrize( + ("input_data", "expected_output"), + [ + ([1, 2, 3], 6), + ([4, 5, 6], 15), + ([], 0), + ], +) +def test__sum_with_parameterization( + input_data: list[int], + expected_output: int, +) -> None: + """ + Example of a parameterized test case. + + Parameters + ---------- + input_data: list[int] + A list of integers to sum. + expected_output: int + The expected sum of the input list. + + Asserts + ------- + - The sum of the input data is equal to the expected output. + """ + assert sum(input_data) == expected_output diff --git a/courageous-comets/mkdocs.yaml b/courageous-comets/mkdocs.yaml new file mode 100644 index 0000000..d2e702b --- /dev/null +++ b/courageous-comets/mkdocs.yaml @@ -0,0 +1,98 @@ +copyright: © 2024 Courageous Comets ☄️ +repo_name: thijsfranck/courageous-comets +repo_url: https://github.com/thijsfranck/courageous-comets +site_name: Courageous Comets +site_url: https://thijsfranck.github.io/courageous-comets/ + +theme: + favicon: assets/favicon.ico + logo: assets/logo.png + name: material + search: true + + features: + - content.code.copy + - navigation.indexes + - navigation.instant + - navigation.instant.progress + - navigation.sections + + icon: + repo: fontawesome/brands/github + + palette: + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Switch to light mode + - media: "(prefers-color-scheme: light)" + scheme: default + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: material/brightness-4 + name: Switch to system preference + +nav: + - Users: + - user-guide/index.md + - Getting Started: user-guide/getting-started.md + - Installing the Bot: user-guide/installing-the-bot.md + - Data Privacy: user-guide/data-privacy.md + - Administrators: + - admin-guide/index.md + - Deployment: admin-guide/deployment.md + - Configuration: admin-guide/configuration.md + - Changelog: CHANGELOG.md + - Contributors: + - contributor-guide/index.md + - Architecture & Design: contributor-guide/architecture-design.md + - Development Environment: contributor-guide/development-environment.md + - Secrets Management: contributor-guide/secrets-management.md + - Version Control: contributor-guide/version-control.md + - Documentation: contributor-guide/documentation.md + - Testing: contributor-guide/testing.md + +markdown_extensions: + - admonition + - attr_list + - md_in_html + - pymdownx.details + - pymdownx.snippets + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.tabbed: + alternate_style: true + - pymdownx.tasklist: + clickable_checkbox: true + custom_checkbox: true + - toc: + permalink: true + +plugins: + - mike + - search + +exclude_docs: | + *.sh + +extra: + generator: false + social: + - icon: fontawesome/brands/github + link: https://github.com/thijsfranck/courageous-comets + version: + alias: true + provider: mike + +extra_css: + - stylesheets/table.css diff --git a/courageous-comets/poetry.lock b/courageous-comets/poetry.lock new file mode 100644 index 0000000..cc99f8b --- /dev/null +++ b/courageous-comets/poetry.lock @@ -0,0 +1,3341 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "aiohttp" +version = "3.9.5" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fcde4c397f673fdec23e6b05ebf8d4751314fa7c24f93334bf1f1364c1c69ac7"}, + {file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d6b3f1fabe465e819aed2c421a6743d8debbde79b6a8600739300630a01bf2c"}, + {file = "aiohttp-3.9.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ae79c1bc12c34082d92bf9422764f799aee4746fd7a392db46b7fd357d4a17a"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d3ebb9e1316ec74277d19c5f482f98cc65a73ccd5430540d6d11682cd857430"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84dabd95154f43a2ea80deffec9cb44d2e301e38a0c9d331cc4aa0166fe28ae3"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a02fbeca6f63cb1f0475c799679057fc9268b77075ab7cf3f1c600e81dd46b"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c26959ca7b75ff768e2776d8055bf9582a6267e24556bb7f7bd29e677932be72"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:714d4e5231fed4ba2762ed489b4aec07b2b9953cf4ee31e9871caac895a839c0"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7a6a8354f1b62e15d48e04350f13e726fa08b62c3d7b8401c0a1314f02e3558"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c413016880e03e69d166efb5a1a95d40f83d5a3a648d16486592c49ffb76d0db"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ff84aeb864e0fac81f676be9f4685f0527b660f1efdc40dcede3c251ef1e867f"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ad7f2919d7dac062f24d6f5fe95d401597fbb015a25771f85e692d043c9d7832"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:702e2c7c187c1a498a4e2b03155d52658fdd6fda882d3d7fbb891a5cf108bb10"}, + {file = "aiohttp-3.9.5-cp310-cp310-win32.whl", hash = "sha256:67c3119f5ddc7261d47163ed86d760ddf0e625cd6246b4ed852e82159617b5fb"}, + {file = "aiohttp-3.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:471f0ef53ccedec9995287f02caf0c068732f026455f07db3f01a46e49d76bbb"}, + {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0ae53e33ee7476dd3d1132f932eeb39bf6125083820049d06edcdca4381f342"}, + {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c088c4d70d21f8ca5c0b8b5403fe84a7bc8e024161febdd4ef04575ef35d474d"}, + {file = "aiohttp-3.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:639d0042b7670222f33b0028de6b4e2fad6451462ce7df2af8aee37dcac55424"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f26383adb94da5e7fb388d441bf09c61e5e35f455a3217bfd790c6b6bc64b2ee"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66331d00fb28dc90aa606d9a54304af76b335ae204d1836f65797d6fe27f1ca2"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ff550491f5492ab5ed3533e76b8567f4b37bd2995e780a1f46bca2024223233"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f22eb3a6c1080d862befa0a89c380b4dafce29dc6cd56083f630073d102eb595"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a81b1143d42b66ffc40a441379387076243ef7b51019204fd3ec36b9f69e77d6"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f64fd07515dad67f24b6ea4a66ae2876c01031de91c93075b8093f07c0a2d93d"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:93e22add827447d2e26d67c9ac0161756007f152fdc5210277d00a85f6c92323"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:55b39c8684a46e56ef8c8d24faf02de4a2b2ac60d26cee93bc595651ff545de9"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4715a9b778f4293b9f8ae7a0a7cef9829f02ff8d6277a39d7f40565c737d3771"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:afc52b8d969eff14e069a710057d15ab9ac17cd4b6753042c407dcea0e40bf75"}, + {file = "aiohttp-3.9.5-cp311-cp311-win32.whl", hash = "sha256:b3df71da99c98534be076196791adca8819761f0bf6e08e07fd7da25127150d6"}, + {file = "aiohttp-3.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:88e311d98cc0bf45b62fc46c66753a83445f5ab20038bcc1b8a1cc05666f428a"}, + {file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c7a4b7a6cf5b6eb11e109a9755fd4fda7d57395f8c575e166d363b9fc3ec4678"}, + {file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0a158704edf0abcac8ac371fbb54044f3270bdbc93e254a82b6c82be1ef08f3c"}, + {file = "aiohttp-3.9.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d153f652a687a8e95ad367a86a61e8d53d528b0530ef382ec5aaf533140ed00f"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82a6a97d9771cb48ae16979c3a3a9a18b600a8505b1115cfe354dfb2054468b4"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60cdbd56f4cad9f69c35eaac0fbbdf1f77b0ff9456cebd4902f3dd1cf096464c"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8676e8fd73141ded15ea586de0b7cda1542960a7b9ad89b2b06428e97125d4fa"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da00da442a0e31f1c69d26d224e1efd3a1ca5bcbf210978a2ca7426dfcae9f58"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18f634d540dd099c262e9f887c8bbacc959847cfe5da7a0e2e1cf3f14dbf2daf"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:320e8618eda64e19d11bdb3bd04ccc0a816c17eaecb7e4945d01deee2a22f95f"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:2faa61a904b83142747fc6a6d7ad8fccff898c849123030f8e75d5d967fd4a81"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:8c64a6dc3fe5db7b1b4d2b5cb84c4f677768bdc340611eca673afb7cf416ef5a"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:393c7aba2b55559ef7ab791c94b44f7482a07bf7640d17b341b79081f5e5cd1a"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c671dc117c2c21a1ca10c116cfcd6e3e44da7fcde37bf83b2be485ab377b25da"}, + {file = "aiohttp-3.9.5-cp312-cp312-win32.whl", hash = "sha256:5a7ee16aab26e76add4afc45e8f8206c95d1d75540f1039b84a03c3b3800dd59"}, + {file = "aiohttp-3.9.5-cp312-cp312-win_amd64.whl", hash = "sha256:5ca51eadbd67045396bc92a4345d1790b7301c14d1848feaac1d6a6c9289e888"}, + {file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:694d828b5c41255e54bc2dddb51a9f5150b4eefa9886e38b52605a05d96566e8"}, + {file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0605cc2c0088fcaae79f01c913a38611ad09ba68ff482402d3410bf59039bfb8"}, + {file = "aiohttp-3.9.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4558e5012ee03d2638c681e156461d37b7a113fe13970d438d95d10173d25f78"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbc053ac75ccc63dc3a3cc547b98c7258ec35a215a92bd9f983e0aac95d3d5b"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4109adee842b90671f1b689901b948f347325045c15f46b39797ae1bf17019de"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6ea1a5b409a85477fd8e5ee6ad8f0e40bf2844c270955e09360418cfd09abac"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3c2890ca8c59ee683fd09adf32321a40fe1cf164e3387799efb2acebf090c11"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3916c8692dbd9d55c523374a3b8213e628424d19116ac4308e434dbf6d95bbdd"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8d1964eb7617907c792ca00b341b5ec3e01ae8c280825deadbbd678447b127e1"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5ab8e1f6bee051a4bf6195e38a5c13e5e161cb7bad83d8854524798bd9fcd6e"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:52c27110f3862a1afbcb2af4281fc9fdc40327fa286c4625dfee247c3ba90156"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7f64cbd44443e80094309875d4f9c71d0401e966d191c3d469cde4642bc2e031"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8b4f72fbb66279624bfe83fd5eb6aea0022dad8eec62b71e7bf63ee1caadeafe"}, + {file = "aiohttp-3.9.5-cp38-cp38-win32.whl", hash = "sha256:6380c039ec52866c06d69b5c7aad5478b24ed11696f0e72f6b807cfb261453da"}, + {file = "aiohttp-3.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:da22dab31d7180f8c3ac7c7635f3bcd53808f374f6aa333fe0b0b9e14b01f91a"}, + {file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1732102949ff6087589408d76cd6dea656b93c896b011ecafff418c9661dc4ed"}, + {file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6021d296318cb6f9414b48e6a439a7f5d1f665464da507e8ff640848ee2a58a"}, + {file = "aiohttp-3.9.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:239f975589a944eeb1bad26b8b140a59a3a320067fb3cd10b75c3092405a1372"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b7b30258348082826d274504fbc7c849959f1989d86c29bc355107accec6cfb"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2adf5c87ff6d8b277814a28a535b59e20bfea40a101db6b3bdca7e9926bc24"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9a3d838441bebcf5cf442700e3963f58b5c33f015341f9ea86dcd7d503c07e2"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e3a1ae66e3d0c17cf65c08968a5ee3180c5a95920ec2731f53343fac9bad106"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c69e77370cce2d6df5d12b4e12bdcca60c47ba13d1cbbc8645dd005a20b738b"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf56238f4bbf49dab8c2dc2e6b1b68502b1e88d335bea59b3f5b9f4c001475"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d1469f228cd9ffddd396d9948b8c9cd8022b6d1bf1e40c6f25b0fb90b4f893ed"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:45731330e754f5811c314901cebdf19dd776a44b31927fa4b4dbecab9e457b0c"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:3fcb4046d2904378e3aeea1df51f697b0467f2aac55d232c87ba162709478c46"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8cf142aa6c1a751fcb364158fd710b8a9be874b81889c2bd13aa8893197455e2"}, + {file = "aiohttp-3.9.5-cp39-cp39-win32.whl", hash = "sha256:7b179eea70833c8dee51ec42f3b4097bd6370892fa93f510f76762105568cf09"}, + {file = "aiohttp-3.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:38d80498e2e169bc61418ff36170e0aad0cd268da8b38a17c4cf29d254a8b3f1"}, + {file = "aiohttp-3.9.5.tar.gz", hash = "sha256:edea7d15772ceeb29db4aff55e482d4bcfb6ae160ce144f2682de02f6d693551"}, +] + +[package.dependencies] +aiosignal = ">=1.1.2" +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +yarl = ">=1.0,<2.0" + +[package.extras] +speedups = ["Brotli", "aiodns", "brotlicffi"] + +[[package]] +name = "aiosignal" +version = "1.3.1" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.7" +files = [ + {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, + {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "anyascii" +version = "0.3.2" +description = "Unicode to ASCII transliteration" +optional = false +python-versions = ">=3.3" +files = [ + {file = "anyascii-0.3.2-py3-none-any.whl", hash = "sha256:3b3beef6fc43d9036d3b0529050b0c48bfad8bc960e9e562d7223cfb94fe45d4"}, + {file = "anyascii-0.3.2.tar.gz", hash = "sha256:9d5d32ef844fe225b8bc7cba7f950534fae4da27a9bf3a6bea2cb0ea46ce4730"}, +] + +[[package]] +name = "argcomplete" +version = "3.3.0" +description = "Bash tab completion for argparse" +optional = false +python-versions = ">=3.8" +files = [ + {file = "argcomplete-3.3.0-py3-none-any.whl", hash = "sha256:c168c3723482c031df3c207d4ba8fa702717ccb9fc0bfe4117166c1f537b4a54"}, + {file = "argcomplete-3.3.0.tar.gz", hash = "sha256:fd03ff4a5b9e6580569d34b273f741e85cd9e072f3feeeee3eba4891c70eda62"}, +] + +[package.extras] +test = ["coverage", "mypy", "pexpect", "ruff", "wheel"] + +[[package]] +name = "astunparse" +version = "1.6.3" +description = "An AST unparser for Python" +optional = false +python-versions = "*" +files = [ + {file = "astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8"}, + {file = "astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872"}, +] + +[package.dependencies] +six = ">=1.6.1,<2.0" +wheel = ">=0.23.0,<1.0" + +[[package]] +name = "asyncache" +version = "0.3.1" +description = "Helpers to use cachetools with async code." +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "asyncache-0.3.1-py3-none-any.whl", hash = "sha256:ef20a1024d265090dd1e0785c961cf98b9c32cc7d9478973dcf25ac1b80011f5"}, + {file = "asyncache-0.3.1.tar.gz", hash = "sha256:9a1e60a75668e794657489bdea6540ee7e3259c483517b934670db7600bf5035"}, +] + +[package.dependencies] +cachetools = ">=5.2.0,<6.0.0" + +[[package]] +name = "attrs" +version = "23.2.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] +tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] + +[[package]] +name = "babel" +version = "2.15.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.8" +files = [ + {file = "Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb"}, + {file = "babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413"}, +] + +[package.extras] +dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] + +[[package]] +name = "braceexpand" +version = "0.1.7" +description = "Bash-style brace expansion for Python" +optional = false +python-versions = "*" +files = [ + {file = "braceexpand-0.1.7-py2.py3-none-any.whl", hash = "sha256:91332d53de7828103dcae5773fb43bc34950b0c8160e35e0f44c4427a3b85014"}, + {file = "braceexpand-0.1.7.tar.gz", hash = "sha256:e6e539bd20eaea53547472ff94f4fb5c3d3bf9d0a89388c4b56663aba765f705"}, +] + +[[package]] +name = "cachetools" +version = "5.4.0" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.7" +files = [ + {file = "cachetools-5.4.0-py3-none-any.whl", hash = "sha256:3ae3b49a3d5e28a77a0be2b37dbcb89005058959cb2323858c2657c4a8cab474"}, + {file = "cachetools-5.4.0.tar.gz", hash = "sha256:b8adc2e7c07f105ced7bc56dbb6dfbe7c4a00acce20e2227b3f355be89bc6827"}, +] + +[[package]] +name = "certifi" +version = "2024.7.4" +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"}, +] + +[[package]] +name = "cffi" +version = "1.16.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coloredlogs" +version = "15.0.1" +description = "Colored terminal output for Python's logging module" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"}, + {file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"}, +] + +[package.dependencies] +humanfriendly = ">=9.1" + +[package.extras] +cron = ["capturer (>=2.4)"] + +[[package]] +name = "commitizen" +version = "3.27.0" +description = "Python commitizen client tool" +optional = false +python-versions = ">=3.8" +files = [ + {file = "commitizen-3.27.0-py3-none-any.whl", hash = "sha256:11948fa563d5ad5464baf09eaacff3cf8cbade1ca029ed9c4978f2227f033130"}, + {file = "commitizen-3.27.0.tar.gz", hash = "sha256:5874d0c7e8e1be3b75b1b0a2269cffe3dd5c843b860d84b0bdbb9ea86e3474b8"}, +] + +[package.dependencies] +argcomplete = ">=1.12.1,<3.4" +charset-normalizer = ">=2.1.0,<4" +colorama = ">=0.4.1,<0.5.0" +decli = ">=0.6.0,<0.7.0" +importlib_metadata = ">=4.13,<8" +jinja2 = ">=2.10.3" +packaging = ">=19" +pyyaml = ">=3.08" +questionary = ">=2.0,<3.0" +termcolor = ">=1.1,<3" +tomlkit = ">=0.5.3,<1.0.0" + +[[package]] +name = "contourpy" +version = "1.2.1" +description = "Python library for calculating contours of 2D quadrilateral grids" +optional = false +python-versions = ">=3.9" +files = [ + {file = "contourpy-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bd7c23df857d488f418439686d3b10ae2fbf9bc256cd045b37a8c16575ea1040"}, + {file = "contourpy-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5b9eb0ca724a241683c9685a484da9d35c872fd42756574a7cfbf58af26677fd"}, + {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c75507d0a55378240f781599c30e7776674dbaf883a46d1c90f37e563453480"}, + {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11959f0ce4a6f7b76ec578576a0b61a28bdc0696194b6347ba3f1c53827178b9"}, + {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb3315a8a236ee19b6df481fc5f997436e8ade24a9f03dfdc6bd490fea20c6da"}, + {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39f3ecaf76cd98e802f094e0d4fbc6dc9c45a8d0c4d185f0f6c2234e14e5f75b"}, + {file = "contourpy-1.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:94b34f32646ca0414237168d68a9157cb3889f06b096612afdd296003fdd32fd"}, + {file = "contourpy-1.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:457499c79fa84593f22454bbd27670227874cd2ff5d6c84e60575c8b50a69619"}, + {file = "contourpy-1.2.1-cp310-cp310-win32.whl", hash = "sha256:ac58bdee53cbeba2ecad824fa8159493f0bf3b8ea4e93feb06c9a465d6c87da8"}, + {file = "contourpy-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:9cffe0f850e89d7c0012a1fb8730f75edd4320a0a731ed0c183904fe6ecfc3a9"}, + {file = "contourpy-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6022cecf8f44e36af10bd9118ca71f371078b4c168b6e0fab43d4a889985dbb5"}, + {file = "contourpy-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef5adb9a3b1d0c645ff694f9bca7702ec2c70f4d734f9922ea34de02294fdf72"}, + {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6150ffa5c767bc6332df27157d95442c379b7dce3a38dff89c0f39b63275696f"}, + {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c863140fafc615c14a4bf4efd0f4425c02230eb8ef02784c9a156461e62c965"}, + {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:00e5388f71c1a0610e6fe56b5c44ab7ba14165cdd6d695429c5cd94021e390b2"}, + {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4492d82b3bc7fbb7e3610747b159869468079fe149ec5c4d771fa1f614a14df"}, + {file = "contourpy-1.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:49e70d111fee47284d9dd867c9bb9a7058a3c617274900780c43e38d90fe1205"}, + {file = "contourpy-1.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b59c0ffceff8d4d3996a45f2bb6f4c207f94684a96bf3d9728dbb77428dd8cb8"}, + {file = "contourpy-1.2.1-cp311-cp311-win32.whl", hash = "sha256:7b4182299f251060996af5249c286bae9361fa8c6a9cda5efc29fe8bfd6062ec"}, + {file = "contourpy-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2855c8b0b55958265e8b5888d6a615ba02883b225f2227461aa9127c578a4922"}, + {file = "contourpy-1.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:62828cada4a2b850dbef89c81f5a33741898b305db244904de418cc957ff05dc"}, + {file = "contourpy-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:309be79c0a354afff9ff7da4aaed7c3257e77edf6c1b448a779329431ee79d7e"}, + {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e785e0f2ef0d567099b9ff92cbfb958d71c2d5b9259981cd9bee81bd194c9a4"}, + {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cac0a8f71a041aa587410424ad46dfa6a11f6149ceb219ce7dd48f6b02b87a7"}, + {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af3f4485884750dddd9c25cb7e3915d83c2db92488b38ccb77dd594eac84c4a0"}, + {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ce6889abac9a42afd07a562c2d6d4b2b7134f83f18571d859b25624a331c90b"}, + {file = "contourpy-1.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a1eea9aecf761c661d096d39ed9026574de8adb2ae1c5bd7b33558af884fb2ce"}, + {file = "contourpy-1.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:187fa1d4c6acc06adb0fae5544c59898ad781409e61a926ac7e84b8f276dcef4"}, + {file = "contourpy-1.2.1-cp312-cp312-win32.whl", hash = "sha256:c2528d60e398c7c4c799d56f907664673a807635b857df18f7ae64d3e6ce2d9f"}, + {file = "contourpy-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:1a07fc092a4088ee952ddae19a2b2a85757b923217b7eed584fdf25f53a6e7ce"}, + {file = "contourpy-1.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bb6834cbd983b19f06908b45bfc2dad6ac9479ae04abe923a275b5f48f1a186b"}, + {file = "contourpy-1.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1d59e739ab0e3520e62a26c60707cc3ab0365d2f8fecea74bfe4de72dc56388f"}, + {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd3db01f59fdcbce5b22afad19e390260d6d0222f35a1023d9adc5690a889364"}, + {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a12a813949e5066148712a0626895c26b2578874e4cc63160bb007e6df3436fe"}, + {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe0ccca550bb8e5abc22f530ec0466136379c01321fd94f30a22231e8a48d985"}, + {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1d59258c3c67c865435d8fbeb35f8c59b8bef3d6f46c1f29f6123556af28445"}, + {file = "contourpy-1.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f32c38afb74bd98ce26de7cc74a67b40afb7b05aae7b42924ea990d51e4dac02"}, + {file = "contourpy-1.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d31a63bc6e6d87f77d71e1abbd7387ab817a66733734883d1fc0021ed9bfa083"}, + {file = "contourpy-1.2.1-cp39-cp39-win32.whl", hash = "sha256:ddcb8581510311e13421b1f544403c16e901c4e8f09083c881fab2be80ee31ba"}, + {file = "contourpy-1.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:10a37ae557aabf2509c79715cd20b62e4c7c28b8cd62dd7d99e5ed3ce28c3fd9"}, + {file = "contourpy-1.2.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a31f94983fecbac95e58388210427d68cd30fe8a36927980fab9c20062645609"}, + {file = "contourpy-1.2.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef2b055471c0eb466033760a521efb9d8a32b99ab907fc8358481a1dd29e3bd3"}, + {file = "contourpy-1.2.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b33d2bc4f69caedcd0a275329eb2198f560b325605810895627be5d4b876bf7f"}, + {file = "contourpy-1.2.1.tar.gz", hash = "sha256:4d8908b3bee1c889e547867ca4cdc54e5ab6be6d3e078556814a22457f49423c"}, +] + +[package.dependencies] +numpy = ">=1.20" + +[package.extras] +bokeh = ["bokeh", "selenium"] +docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] +mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.8.0)", "types-Pillow"] +test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] +test-no-images = ["pytest", "pytest-cov", "pytest-xdist", "wurlitzer"] + +[[package]] +name = "contractions" +version = "0.1.73" +description = "Fixes contractions such as `you're` to you `are`" +optional = false +python-versions = "*" +files = [ + {file = "contractions-0.1.73-py2.py3-none-any.whl", hash = "sha256:398cee3b69c37307a50dce4930d961a0f42b48fdae9562df73bed5683008d3bc"}, +] + +[package.dependencies] +textsearch = ">=0.0.21" + +[[package]] +name = "cycler" +version = "0.12.1" +description = "Composable style cycles" +optional = false +python-versions = ">=3.8" +files = [ + {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, + {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, +] + +[package.extras] +docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] +tests = ["pytest", "pytest-cov", "pytest-xdist"] + +[[package]] +name = "decli" +version = "0.6.2" +description = "Minimal, easy-to-use, declarative cli tool" +optional = false +python-versions = ">=3.7" +files = [ + {file = "decli-0.6.2-py3-none-any.whl", hash = "sha256:2fc84106ce9a8f523ed501ca543bdb7e416c064917c12a59ebdc7f311a97b7ed"}, + {file = "decli-0.6.2.tar.gz", hash = "sha256:36f71eb55fd0093895efb4f416ec32b7f6e00147dda448e3365cf73ceab42d6f"}, +] + +[[package]] +name = "discord-py" +version = "2.4.0" +description = "A Python wrapper for the Discord API" +optional = false +python-versions = ">=3.8" +files = [ + {file = "discord.py-2.4.0-py3-none-any.whl", hash = "sha256:b8af6711c70f7e62160bfbecb55be699b5cb69d007426759ab8ab06b1bd77d1d"}, + {file = "discord_py-2.4.0.tar.gz", hash = "sha256:d07cb2a223a185873a1d0ee78b9faa9597e45b3f6186df21a95cec1e9bcdc9a5"}, +] + +[package.dependencies] +aiohttp = ">=3.7.4,<4" + +[package.extras] +docs = ["sphinx (==4.4.0)", "sphinx-inline-tabs (==2023.4.21)", "sphinxcontrib-applehelp (==1.0.4)", "sphinxcontrib-devhelp (==1.0.2)", "sphinxcontrib-htmlhelp (==2.0.1)", "sphinxcontrib-jsmath (==1.0.1)", "sphinxcontrib-qthelp (==1.0.3)", "sphinxcontrib-serializinghtml (==1.1.5)", "sphinxcontrib-trio (==1.1.2)", "sphinxcontrib-websupport (==1.2.4)", "typing-extensions (>=4.3,<5)"] +speed = ["Brotli", "aiodns (>=1.1)", "cchardet (==2.1.7)", "orjson (>=3.5.4)"] +test = ["coverage[toml]", "pytest", "pytest-asyncio", "pytest-cov", "pytest-mock", "typing-extensions (>=4.3,<5)", "tzdata"] +voice = ["PyNaCl (>=1.3.0,<1.6)"] + +[[package]] +name = "distlib" +version = "0.3.8" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + +[[package]] +name = "faker" +version = "26.0.0" +description = "Faker is a Python package that generates fake data for you." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Faker-26.0.0-py3-none-any.whl", hash = "sha256:886ee28219be96949cd21ecc96c4c742ee1680e77f687b095202c8def1a08f06"}, + {file = "Faker-26.0.0.tar.gz", hash = "sha256:0f60978314973de02c00474c2ae899785a42b2cf4f41b7987e93c132a2b8a4a9"}, +] + +[package.dependencies] +python-dateutil = ">=2.4" + +[[package]] +name = "filelock" +version = "3.15.4" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, + {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] +typing = ["typing-extensions (>=4.8)"] + +[[package]] +name = "fonttools" +version = "4.53.1" +description = "Tools to manipulate font files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fonttools-4.53.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0679a30b59d74b6242909945429dbddb08496935b82f91ea9bf6ad240ec23397"}, + {file = "fonttools-4.53.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8bf06b94694251861ba7fdeea15c8ec0967f84c3d4143ae9daf42bbc7717fe3"}, + {file = "fonttools-4.53.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b96cd370a61f4d083c9c0053bf634279b094308d52fdc2dd9a22d8372fdd590d"}, + {file = "fonttools-4.53.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1c7c5aa18dd3b17995898b4a9b5929d69ef6ae2af5b96d585ff4005033d82f0"}, + {file = "fonttools-4.53.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e013aae589c1c12505da64a7d8d023e584987e51e62006e1bb30d72f26522c41"}, + {file = "fonttools-4.53.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9efd176f874cb6402e607e4cc9b4a9cd584d82fc34a4b0c811970b32ba62501f"}, + {file = "fonttools-4.53.1-cp310-cp310-win32.whl", hash = "sha256:c8696544c964500aa9439efb6761947393b70b17ef4e82d73277413f291260a4"}, + {file = "fonttools-4.53.1-cp310-cp310-win_amd64.whl", hash = "sha256:8959a59de5af6d2bec27489e98ef25a397cfa1774b375d5787509c06659b3671"}, + {file = "fonttools-4.53.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:da33440b1413bad53a8674393c5d29ce64d8c1a15ef8a77c642ffd900d07bfe1"}, + {file = "fonttools-4.53.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ff7e5e9bad94e3a70c5cd2fa27f20b9bb9385e10cddab567b85ce5d306ea923"}, + {file = "fonttools-4.53.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6e7170d675d12eac12ad1a981d90f118c06cf680b42a2d74c6c931e54b50719"}, + {file = "fonttools-4.53.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bee32ea8765e859670c4447b0817514ca79054463b6b79784b08a8df3a4d78e3"}, + {file = "fonttools-4.53.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6e08f572625a1ee682115223eabebc4c6a2035a6917eac6f60350aba297ccadb"}, + {file = "fonttools-4.53.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b21952c092ffd827504de7e66b62aba26fdb5f9d1e435c52477e6486e9d128b2"}, + {file = "fonttools-4.53.1-cp311-cp311-win32.whl", hash = "sha256:9dfdae43b7996af46ff9da520998a32b105c7f098aeea06b2226b30e74fbba88"}, + {file = "fonttools-4.53.1-cp311-cp311-win_amd64.whl", hash = "sha256:d4d0096cb1ac7a77b3b41cd78c9b6bc4a400550e21dc7a92f2b5ab53ed74eb02"}, + {file = "fonttools-4.53.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d92d3c2a1b39631a6131c2fa25b5406855f97969b068e7e08413325bc0afba58"}, + {file = "fonttools-4.53.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3b3c8ebafbee8d9002bd8f1195d09ed2bd9ff134ddec37ee8f6a6375e6a4f0e8"}, + {file = "fonttools-4.53.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32f029c095ad66c425b0ee85553d0dc326d45d7059dbc227330fc29b43e8ba60"}, + {file = "fonttools-4.53.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10f5e6c3510b79ea27bb1ebfcc67048cde9ec67afa87c7dd7efa5c700491ac7f"}, + {file = "fonttools-4.53.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f677ce218976496a587ab17140da141557beb91d2a5c1a14212c994093f2eae2"}, + {file = "fonttools-4.53.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9e6ceba2a01b448e36754983d376064730690401da1dd104ddb543519470a15f"}, + {file = "fonttools-4.53.1-cp312-cp312-win32.whl", hash = "sha256:791b31ebbc05197d7aa096bbc7bd76d591f05905d2fd908bf103af4488e60670"}, + {file = "fonttools-4.53.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ed170b5e17da0264b9f6fae86073be3db15fa1bd74061c8331022bca6d09bab"}, + {file = "fonttools-4.53.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c818c058404eb2bba05e728d38049438afd649e3c409796723dfc17cd3f08749"}, + {file = "fonttools-4.53.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:651390c3b26b0c7d1f4407cad281ee7a5a85a31a110cbac5269de72a51551ba2"}, + {file = "fonttools-4.53.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e54f1bba2f655924c1138bbc7fa91abd61f45c68bd65ab5ed985942712864bbb"}, + {file = "fonttools-4.53.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9cd19cf4fe0595ebdd1d4915882b9440c3a6d30b008f3cc7587c1da7b95be5f"}, + {file = "fonttools-4.53.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2af40ae9cdcb204fc1d8f26b190aa16534fcd4f0df756268df674a270eab575d"}, + {file = "fonttools-4.53.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:35250099b0cfb32d799fb5d6c651220a642fe2e3c7d2560490e6f1d3f9ae9169"}, + {file = "fonttools-4.53.1-cp38-cp38-win32.whl", hash = "sha256:f08df60fbd8d289152079a65da4e66a447efc1d5d5a4d3f299cdd39e3b2e4a7d"}, + {file = "fonttools-4.53.1-cp38-cp38-win_amd64.whl", hash = "sha256:7b6b35e52ddc8fb0db562133894e6ef5b4e54e1283dff606fda3eed938c36fc8"}, + {file = "fonttools-4.53.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:75a157d8d26c06e64ace9df037ee93a4938a4606a38cb7ffaf6635e60e253b7a"}, + {file = "fonttools-4.53.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4824c198f714ab5559c5be10fd1adf876712aa7989882a4ec887bf1ef3e00e31"}, + {file = "fonttools-4.53.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:becc5d7cb89c7b7afa8321b6bb3dbee0eec2b57855c90b3e9bf5fb816671fa7c"}, + {file = "fonttools-4.53.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84ec3fb43befb54be490147b4a922b5314e16372a643004f182babee9f9c3407"}, + {file = "fonttools-4.53.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:73379d3ffdeecb376640cd8ed03e9d2d0e568c9d1a4e9b16504a834ebadc2dfb"}, + {file = "fonttools-4.53.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:02569e9a810f9d11f4ae82c391ebc6fb5730d95a0657d24d754ed7763fb2d122"}, + {file = "fonttools-4.53.1-cp39-cp39-win32.whl", hash = "sha256:aae7bd54187e8bf7fd69f8ab87b2885253d3575163ad4d669a262fe97f0136cb"}, + {file = "fonttools-4.53.1-cp39-cp39-win_amd64.whl", hash = "sha256:e5b708073ea3d684235648786f5f6153a48dc8762cdfe5563c57e80787c29fbb"}, + {file = "fonttools-4.53.1-py3-none-any.whl", hash = "sha256:f1f8758a2ad110bd6432203a344269f445a2907dc24ef6bccfd0ac4e14e0d71d"}, + {file = "fonttools-4.53.1.tar.gz", hash = "sha256:e128778a8e9bc11159ce5447f76766cefbd876f44bd79aff030287254e4752c4"}, +] + +[package.extras] +all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0)", "xattr", "zopfli (>=0.1.4)"] +graphite = ["lz4 (>=1.7.4.2)"] +interpolatable = ["munkres", "pycairo", "scipy"] +lxml = ["lxml (>=4.0)"] +pathops = ["skia-pathops (>=0.5.0)"] +plot = ["matplotlib"] +repacker = ["uharfbuzz (>=0.23.0)"] +symfont = ["sympy"] +type1 = ["xattr"] +ufo = ["fs (>=2.2.0,<3)"] +unicode = ["unicodedata2 (>=15.1.0)"] +woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] + +[[package]] +name = "frozenlist" +version = "1.4.1" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.8" +files = [ + {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"}, + {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"}, + {file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"}, + {file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"}, + {file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"}, + {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"}, + {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"}, + {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"}, + {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"}, + {file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"}, + {file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"}, + {file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"}, + {file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"}, + {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"}, + {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, +] + +[[package]] +name = "fsspec" +version = "2024.6.1" +description = "File-system specification" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fsspec-2024.6.1-py3-none-any.whl", hash = "sha256:3cb443f8bcd2efb31295a5b9fdb02aee81d8452c80d28f97a6d0959e6cee101e"}, + {file = "fsspec-2024.6.1.tar.gz", hash = "sha256:fad7d7e209dd4c1208e3bbfda706620e0da5142bebbd9c384afb95b07e798e49"}, +] + +[package.extras] +abfs = ["adlfs"] +adl = ["adlfs"] +arrow = ["pyarrow (>=1)"] +dask = ["dask", "distributed"] +dev = ["pre-commit", "ruff"] +doc = ["numpydoc", "sphinx", "sphinx-design", "sphinx-rtd-theme", "yarl"] +dropbox = ["dropbox", "dropboxdrivefs", "requests"] +full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "dask", "distributed", "dropbox", "dropboxdrivefs", "fusepy", "gcsfs", "libarchive-c", "ocifs", "panel", "paramiko", "pyarrow (>=1)", "pygit2", "requests", "s3fs", "smbprotocol", "tqdm"] +fuse = ["fusepy"] +gcs = ["gcsfs"] +git = ["pygit2"] +github = ["requests"] +gs = ["gcsfs"] +gui = ["panel"] +hdfs = ["pyarrow (>=1)"] +http = ["aiohttp (!=4.0.0a0,!=4.0.0a1)"] +libarchive = ["libarchive-c"] +oci = ["ocifs"] +s3 = ["s3fs"] +sftp = ["paramiko"] +smb = ["smbprotocol"] +ssh = ["paramiko"] +test = ["aiohttp (!=4.0.0a0,!=4.0.0a1)", "numpy", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "requests"] +test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask-expr", "dask[dataframe,test]", "moto[server] (>4,<5)", "pytest-timeout", "xarray"] +test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr", "zstandard"] +tqdm = ["tqdm"] + +[[package]] +name = "ghp-import" +version = "2.1.0" +description = "Copy your docs directly to the gh-pages branch." +optional = false +python-versions = "*" +files = [ + {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, + {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, +] + +[package.dependencies] +python-dateutil = ">=2.8.1" + +[package.extras] +dev = ["flake8", "markdown", "twine", "wheel"] + +[[package]] +name = "hiredis" +version = "3.0.0" +description = "Python wrapper for hiredis" +optional = false +python-versions = ">=3.8" +files = [ + {file = "hiredis-3.0.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:4b182791c41c5eb1d9ed736f0ff81694b06937ca14b0d4dadde5dadba7ff6dae"}, + {file = "hiredis-3.0.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:13c275b483a052dd645eb2cb60d6380f1f5215e4c22d6207e17b86be6dd87ffa"}, + {file = "hiredis-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1018cc7f12824506f165027eabb302735b49e63af73eb4d5450c66c88f47026"}, + {file = "hiredis-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83a29cc7b21b746cb6a480189e49f49b2072812c445e66a9e38d2004d496b81c"}, + {file = "hiredis-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e241fab6332e8fb5f14af00a4a9c6aefa22f19a336c069b7ddbf28ef8341e8d6"}, + {file = "hiredis-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1fb8de899f0145d6c4d5d4bd0ee88a78eb980a7ffabd51e9889251b8f58f1785"}, + {file = "hiredis-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b23291951959141173eec10f8573538e9349fa27f47a0c34323d1970bf891ee5"}, + {file = "hiredis-3.0.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e421ac9e4b5efc11705a0d5149e641d4defdc07077f748667f359e60dc904420"}, + {file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:77c8006c12154c37691b24ff293c077300c22944018c3ff70094a33e10c1d795"}, + {file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:41afc0d3c18b59eb50970479a9c0e5544fb4b95e3a79cf2fbaece6ddefb926fe"}, + {file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:04ccae6dcd9647eae6025425ab64edb4d79fde8b9e6e115ebfabc6830170e3b2"}, + {file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fe91d62b0594db5ea7d23fc2192182b1a7b6973f628a9b8b2e0a42a2be721ac6"}, + {file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:99516d99316062824a24d145d694f5b0d030c80da693ea6f8c4ecf71a251d8bb"}, + {file = "hiredis-3.0.0-cp310-cp310-win32.whl", hash = "sha256:562eaf820de045eb487afaa37e6293fe7eceb5b25e158b5a1974b7e40bf04543"}, + {file = "hiredis-3.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:a1c81c89ed765198da27412aa21478f30d54ef69bf5e4480089d9c3f77b8f882"}, + {file = "hiredis-3.0.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:4664dedcd5933364756d7251a7ea86d60246ccf73a2e00912872dacbfcef8978"}, + {file = "hiredis-3.0.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:47de0bbccf4c8a9f99d82d225f7672b9dd690d8fd872007b933ef51a302c9fa6"}, + {file = "hiredis-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e43679eca508ba8240d016d8cca9d27342d70184773c15bea78a23c87a1922f1"}, + {file = "hiredis-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13c345e7278c210317e77e1934b27b61394fee0dec2e8bd47e71570900f75823"}, + {file = "hiredis-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00018f22f38530768b73ea86c11f47e8d4df65facd4e562bd78773bd1baef35e"}, + {file = "hiredis-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ea3a86405baa8eb0d3639ced6926ad03e07113de54cb00fd7510cb0db76a89d"}, + {file = "hiredis-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c073848d2b1d5561f3903879ccf4e1a70c9b1e7566c7bdcc98d082fa3e7f0a1d"}, + {file = "hiredis-3.0.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a8dffb5f5b3415a4669d25de48b617fd9d44b0bccfc4c2ab24b06406ecc9ecb"}, + {file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:22c17c96143c2a62dfd61b13803bc5de2ac526b8768d2141c018b965d0333b66"}, + {file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c3ece960008dab66c6b8bb3a1350764677ee7c74ccd6270aaf1b1caf9ccebb46"}, + {file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f75999ae00a920f7dce6ecae76fa5e8674a3110e5a75f12c7a2c75ae1af53396"}, + {file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e069967cbd5e1900aafc4b5943888f6d34937fc59bf8918a1a546cb729b4b1e4"}, + {file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0aacc0a78e1d94d843a6d191f224a35893e6bdfeb77a4a89264155015c65f126"}, + {file = "hiredis-3.0.0-cp311-cp311-win32.whl", hash = "sha256:719c32147ba29528cb451f037bf837dcdda4ff3ddb6cdb12c4216b0973174718"}, + {file = "hiredis-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:bdc144d56333c52c853c31b4e2e52cfbdb22d3da4374c00f5f3d67c42158970f"}, + {file = "hiredis-3.0.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:484025d2eb8f6348f7876fc5a2ee742f568915039fcb31b478fd5c242bb0fe3a"}, + {file = "hiredis-3.0.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:fcdb552ffd97151dab8e7bc3ab556dfa1512556b48a367db94b5c20253a35ee1"}, + {file = "hiredis-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bb6f9fd92f147ba11d338ef5c68af4fd2908739c09e51f186e1d90958c68cc1"}, + {file = "hiredis-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa86bf9a0ed339ec9e8a9a9d0ae4dccd8671625c83f9f9f2640729b15e07fbfd"}, + {file = "hiredis-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e194a0d5df9456995d8f510eab9f529213e7326af6b94770abf8f8b7952ddcaa"}, + {file = "hiredis-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a1df39d74ec507d79c7a82c8063eee60bf80537cdeee652f576059b9cdd15c"}, + {file = "hiredis-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f91456507427ba36fd81b2ca11053a8e112c775325acc74e993201ea912d63e9"}, + {file = "hiredis-3.0.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9862db92ef67a8a02e0d5370f07d380e14577ecb281b79720e0d7a89aedb9ee5"}, + {file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d10fcd9e0eeab835f492832b2a6edb5940e2f1230155f33006a8dfd3bd2c94e4"}, + {file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:48727d7d405d03977d01885f317328dc21d639096308de126c2c4e9950cbd3c9"}, + {file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e0bb6102ebe2efecf8a3292c6660a0e6fac98176af6de67f020bea1c2343717"}, + {file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:df274e3abb4df40f4c7274dd3e587dfbb25691826c948bc98d5fead019dfb001"}, + {file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:034925b5fb514f7b11aac38cd55b3fd7e9d3af23bd6497f3f20aa5b8ba58e232"}, + {file = "hiredis-3.0.0-cp312-cp312-win32.whl", hash = "sha256:120f2dda469b28d12ccff7c2230225162e174657b49cf4cd119db525414ae281"}, + {file = "hiredis-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:e584fe5f4e6681d8762982be055f1534e0170f6308a7a90f58d737bab12ff6a8"}, + {file = "hiredis-3.0.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:122171ff47d96ed8dd4bba6c0e41d8afaba3e8194949f7720431a62aa29d8895"}, + {file = "hiredis-3.0.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:ba9fc605ac558f0de67463fb588722878641e6fa1dabcda979e8e69ff581d0bd"}, + {file = "hiredis-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a631e2990b8be23178f655cae8ac6c7422af478c420dd54e25f2e26c29e766f1"}, + {file = "hiredis-3.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63482db3fadebadc1d01ad33afa6045ebe2ea528eb77ccaabd33ee7d9c2bad48"}, + {file = "hiredis-3.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f669212c390eebfbe03c4e20181f5970b82c5d0a0ad1df1785f7ffbe7d61150"}, + {file = "hiredis-3.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a49ef161739f8018c69b371528bdb47d7342edfdee9ddc75a4d8caddf45a6e"}, + {file = "hiredis-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98a152052b8878e5e43a2e3a14075218adafc759547c98668a21e9485882696c"}, + {file = "hiredis-3.0.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50a196af0ce657fcde9bf8a0bbe1032e22c64d8fcec2bc926a35e7ff68b3a166"}, + {file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f2f312eef8aafc2255e3585dcf94d5da116c43ef837db91db9ecdc1bc930072d"}, + {file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:6ca41fa40fa019cde42c21add74aadd775e71458051a15a352eabeb12eb4d084"}, + {file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:6eecb343c70629f5af55a8b3e53264e44fa04e155ef7989de13668a0cb102a90"}, + {file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:c3fdad75e7837a475900a1d3a5cc09aa024293c3b0605155da2d42f41bc0e482"}, + {file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8854969e7480e8d61ed7549eb232d95082a743e94138d98d7222ba4e9f7ecacd"}, + {file = "hiredis-3.0.0-cp38-cp38-win32.whl", hash = "sha256:f114a6c86edbf17554672b050cce72abf489fe58d583c7921904d5f1c9691605"}, + {file = "hiredis-3.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:7d99b91e42217d7b4b63354b15b41ce960e27d216783e04c4a350224d55842a4"}, + {file = "hiredis-3.0.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:4c6efcbb5687cf8d2aedcc2c3ed4ac6feae90b8547427d417111194873b66b06"}, + {file = "hiredis-3.0.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:5b5cff42a522a0d81c2ae7eae5e56d0ee7365e0c4ad50c4de467d8957aff4414"}, + {file = "hiredis-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:82f794d564f4bc76b80c50b03267fe5d6589e93f08e66b7a2f674faa2fa76ebc"}, + {file = "hiredis-3.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7a4c1791d7aa7e192f60fe028ae409f18ccdd540f8b1e6aeb0df7816c77e4a4"}, + {file = "hiredis-3.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2537b2cd98192323fce4244c8edbf11f3cac548a9d633dbbb12b48702f379f4"}, + {file = "hiredis-3.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fed69bbaa307040c62195a269f82fc3edf46b510a17abb6b30a15d7dab548df"}, + {file = "hiredis-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869f6d5537d243080f44253491bb30aa1ec3c21754003b3bddeadedeb65842b0"}, + {file = "hiredis-3.0.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d435ae89073d7cd51e6b6bf78369c412216261c9c01662e7008ff00978153729"}, + {file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:204b79b30a0e6be0dc2301a4d385bb61472809f09c49f400497f1cdd5a165c66"}, + {file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3ea635101b739c12effd189cc19b2671c268abb03013fd1f6321ca29df3ca625"}, + {file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f359175197fd833c8dd7a8c288f1516be45415bb5c939862ab60c2918e1e1943"}, + {file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ac6d929cb33dd12ad3424b75725975f0a54b5b12dbff95f2a2d660c510aa106d"}, + {file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:100431e04d25a522ef2c3b94f294c4219c4de3bfc7d557b6253296145a144c11"}, + {file = "hiredis-3.0.0-cp39-cp39-win32.whl", hash = "sha256:e1a9c14ae9573d172dc050a6f63a644457df5d01ec4d35a6a0f097f812930f83"}, + {file = "hiredis-3.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:54a6dd7b478e6eb01ce15b3bb5bf771e108c6c148315bf194eb2ab776a3cac4d"}, + {file = "hiredis-3.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:50da7a9edf371441dfcc56288d790985ee9840d982750580710a9789b8f4a290"}, + {file = "hiredis-3.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9b285ef6bf1581310b0d5e8f6ce64f790a1c40e89c660e1320b35f7515433672"}, + {file = "hiredis-3.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0dcfa684966f25b335072115de2f920228a3c2caf79d4bfa2b30f6e4f674a948"}, + {file = "hiredis-3.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a41be8af1fd78ca97bc948d789a09b730d1e7587d07ca53af05758f31f4b985d"}, + {file = "hiredis-3.0.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:038756db735e417ab36ee6fd7725ce412385ed2bd0767e8179a4755ea11b804f"}, + {file = "hiredis-3.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:fcecbd39bd42cef905c0b51c9689c39d0cc8b88b1671e7f40d4fb213423aef3a"}, + {file = "hiredis-3.0.0-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a131377493a59fb0f5eaeb2afd49c6540cafcfba5b0b3752bed707be9e7c4eaf"}, + {file = "hiredis-3.0.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:3d22c53f0ec5c18ecb3d92aa9420563b1c5d657d53f01356114978107b00b860"}, + {file = "hiredis-3.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8a91e9520fbc65a799943e5c970ffbcd67905744d8becf2e75f9f0a5e8414f0"}, + {file = "hiredis-3.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3dc8043959b50141df58ab4f398e8ae84c6f9e673a2c9407be65fc789138f4a6"}, + {file = "hiredis-3.0.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51b99cfac514173d7b8abdfe10338193e8a0eccdfe1870b646009d2fb7cbe4b5"}, + {file = "hiredis-3.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:fa1fcad89d8a41d8dc10b1e54951ec1e161deabd84ed5a2c95c3c7213bdb3514"}, + {file = "hiredis-3.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:898636a06d9bf575d2c594129085ad6b713414038276a4bfc5db7646b8a5be78"}, + {file = "hiredis-3.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:466f836dbcf86de3f9692097a7a01533dc9926986022c6617dc364a402b265c5"}, + {file = "hiredis-3.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23142a8af92a13fc1e3f2ca1d940df3dcf2af1d176be41fe8d89e30a837a0b60"}, + {file = "hiredis-3.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:793c80a3d6b0b0e8196a2d5de37a08330125668c8012922685e17aa9108c33ac"}, + {file = "hiredis-3.0.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:467d28112c7faa29b7db743f40803d927c8591e9da02b6ce3d5fadc170a542a2"}, + {file = "hiredis-3.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:dc384874a719c767b50a30750f937af18842ee5e288afba95a5a3ed703b1515a"}, + {file = "hiredis-3.0.0.tar.gz", hash = "sha256:fed8581ae26345dea1f1e0d1a96e05041a727a45e7d8d459164583e23c6ac441"}, +] + +[[package]] +name = "huggingface-hub" +version = "0.24.2" +description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "huggingface_hub-0.24.2-py3-none-any.whl", hash = "sha256:abdf3244d3a274c4b1fbc5c4a1ef700032b3f60ba93cc63e4f036fd082aa2805"}, + {file = "huggingface_hub-0.24.2.tar.gz", hash = "sha256:92be892405d2f6a7a8479016f9a5662354f202b2c6c1ff499609621aed1fae10"}, +] + +[package.dependencies] +filelock = "*" +fsspec = ">=2023.5.0" +packaging = ">=20.9" +pyyaml = ">=5.1" +requests = "*" +tqdm = ">=4.42.1" +typing-extensions = ">=3.7.4.3" + +[package.extras] +all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio", "jedi", "minijinja (>=1.0)", "mypy (==1.5.1)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.5.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] +cli = ["InquirerPy (==0.3.4)"] +dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio", "jedi", "minijinja (>=1.0)", "mypy (==1.5.1)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.5.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] +fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"] +hf-transfer = ["hf-transfer (>=0.1.4)"] +inference = ["aiohttp", "minijinja (>=1.0)"] +quality = ["mypy (==1.5.1)", "ruff (>=0.5.0)"] +tensorflow = ["graphviz", "pydot", "tensorflow"] +tensorflow-testing = ["keras (<3.0)", "tensorflow"] +testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio", "jedi", "minijinja (>=1.0)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"] +torch = ["safetensors[torch]", "torch"] +typing = ["types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)"] + +[[package]] +name = "humanfriendly" +version = "10.0" +description = "Human friendly output for text interfaces using Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"}, + {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, +] + +[package.dependencies] +pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_version >= \"3.8\""} + +[[package]] +name = "identify" +version = "2.6.0" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"}, + {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.7" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, +] + +[[package]] +name = "import-expression" +version = "1.1.5" +description = "Parses a superset of Python allowing for inline module import expressions" +optional = false +python-versions = "*" +files = [ + {file = "import_expression-1.1.5-py3-none-any.whl", hash = "sha256:f60c3765dbf2f41928b9c6ef79d632209b6705fc8f30e281ed1a492ed026b10f"}, + {file = "import_expression-1.1.5.tar.gz", hash = "sha256:9959588fcfc8dcb144a0725176cfef6c28c7db1fc2d683625025e687516d40c1"}, +] + +[package.dependencies] +astunparse = ">=1.6.3,<2.0.0" + +[package.extras] +test = ["pytest", "pytest-cov"] + +[[package]] +name = "importlib-metadata" +version = "7.2.1" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_metadata-7.2.1-py3-none-any.whl", hash = "sha256:ffef94b0b66046dd8ea2d619b701fe978d9264d38f3998bc4c27ec3b146a87c8"}, + {file = "importlib_metadata-7.2.1.tar.gz", hash = "sha256:509ecb2ab77071db5137c655e24ceb3eee66e7bbc6574165d0d114d9fc4bbe68"}, +] + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "importlib-resources" +version = "6.4.0" +description = "Read resources from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_resources-6.4.0-py3-none-any.whl", hash = "sha256:50d10f043df931902d4194ea07ec57960f66a80449ff867bfe782b4c486ba78c"}, + {file = "importlib_resources-6.4.0.tar.gz", hash = "sha256:cdb2b453b8046ca4e3798eb1d84f3cce1446a0e8e7b5ef4efb600f19fc398145"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["jaraco.test (>=5.4)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "intel-openmp" +version = "2021.4.0" +description = "Intel OpenMP* Runtime Library" +optional = false +python-versions = "*" +files = [ + {file = "intel_openmp-2021.4.0-py2.py3-none-macosx_10_15_x86_64.macosx_11_0_x86_64.whl", hash = "sha256:41c01e266a7fdb631a7609191709322da2bbf24b252ba763f125dd651bcc7675"}, + {file = "intel_openmp-2021.4.0-py2.py3-none-manylinux1_i686.whl", hash = "sha256:3b921236a38384e2016f0f3d65af6732cf2c12918087128a9163225451e776f2"}, + {file = "intel_openmp-2021.4.0-py2.py3-none-manylinux1_x86_64.whl", hash = "sha256:e2240ab8d01472fed04f3544a878cda5da16c26232b7ea1b59132dbfb48b186e"}, + {file = "intel_openmp-2021.4.0-py2.py3-none-win32.whl", hash = "sha256:6e863d8fd3d7e8ef389d52cf97a50fe2afe1a19247e8c0d168ce021546f96fc9"}, + {file = "intel_openmp-2021.4.0-py2.py3-none-win_amd64.whl", hash = "sha256:eef4c8bcc8acefd7f5cd3b9384dbf73d59e2c99fc56545712ded913f43c4a94f"}, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "jishaku" +version = "2.5.2" +description = "A discord.py extension including useful tools for bot development and debugging." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "jishaku-2.5.2-py3-none-any.whl", hash = "sha256:87f34942ee44865f5ce08e36723b7c74a313d8a13a4db8a6b7cc12618cc3496c"}, + {file = "jishaku-2.5.2.tar.gz", hash = "sha256:56d38c333036e37481df5e3c9e81d6033b5097738f0d171a81e2752124f0df5c"}, +] + +[package.dependencies] +braceexpand = ">=0.1.7" +click = ">=8.0.1" +import-expression = ">=1.0.0,<2.0.0" + +[package.extras] +discordpy = ["discord.py (>=1.7.3)"] +docs = ["Sphinx (>=4.4.0)", "sphinxcontrib-trio (>=1.1.2)"] +procinfo = ["psutil (>=5.8.0)"] +profiling = ["line-profiler (>=3.5.1)"] +publish = ["Jinja2 (>=3.0.3)"] +test = ["coverage (>=6.3.2)", "flake8 (>=4.0.1)", "isort (>=5.10.1)", "pylint (>=2.11.1)", "pytest (>=7.0.1)", "pytest-asyncio (>=0.18.1)", "pytest-cov (>=3.0.0)", "pytest-mock (>=3.7.0)"] +voice = ["yt-dlp (>=2022.3.8)"] + +[[package]] +name = "joblib" +version = "1.4.2" +description = "Lightweight pipelining with Python functions" +optional = false +python-versions = ">=3.8" +files = [ + {file = "joblib-1.4.2-py3-none-any.whl", hash = "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6"}, + {file = "joblib-1.4.2.tar.gz", hash = "sha256:2382c5816b2636fbd20a09e0f4e9dad4736765fdfb7dca582943b9c1366b3f0e"}, +] + +[[package]] +name = "kiwisolver" +version = "1.4.5" +description = "A fast implementation of the Cassowary constraint solver" +optional = false +python-versions = ">=3.7" +files = [ + {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:05703cf211d585109fcd72207a31bb170a0f22144d68298dc5e61b3c946518af"}, + {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:146d14bebb7f1dc4d5fbf74f8a6cb15ac42baadee8912eb84ac0b3b2a3dc6ac3"}, + {file = "kiwisolver-1.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ef7afcd2d281494c0a9101d5c571970708ad911d028137cd558f02b851c08b4"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9eaa8b117dc8337728e834b9c6e2611f10c79e38f65157c4c38e9400286f5cb1"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec20916e7b4cbfb1f12380e46486ec4bcbaa91a9c448b97023fde0d5bbf9e4ff"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b42c68602539407884cf70d6a480a469b93b81b7701378ba5e2328660c847a"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa12042de0171fad672b6c59df69106d20d5596e4f87b5e8f76df757a7c399aa"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a40773c71d7ccdd3798f6489aaac9eee213d566850a9533f8d26332d626b82c"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:19df6e621f6d8b4b9c4d45f40a66839294ff2bb235e64d2178f7522d9170ac5b"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:83d78376d0d4fd884e2c114d0621624b73d2aba4e2788182d286309ebdeed770"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e391b1f0a8a5a10ab3b9bb6afcfd74f2175f24f8975fb87ecae700d1503cdee0"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:852542f9481f4a62dbb5dd99e8ab7aedfeb8fb6342349a181d4036877410f525"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59edc41b24031bc25108e210c0def6f6c2191210492a972d585a06ff246bb79b"}, + {file = "kiwisolver-1.4.5-cp310-cp310-win32.whl", hash = "sha256:a6aa6315319a052b4ee378aa171959c898a6183f15c1e541821c5c59beaa0238"}, + {file = "kiwisolver-1.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:d0ef46024e6a3d79c01ff13801cb19d0cad7fd859b15037aec74315540acc276"}, + {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:11863aa14a51fd6ec28688d76f1735f8f69ab1fabf388851a595d0721af042f5"}, + {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ab3919a9997ab7ef2fbbed0cc99bb28d3c13e6d4b1ad36e97e482558a91be90"}, + {file = "kiwisolver-1.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fcc700eadbbccbf6bc1bcb9dbe0786b4b1cb91ca0dcda336eef5c2beed37b797"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfdd7c0b105af050eb3d64997809dc21da247cf44e63dc73ff0fd20b96be55a9"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76c6a5964640638cdeaa0c359382e5703e9293030fe730018ca06bc2010c4437"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbea0db94288e29afcc4c28afbf3a7ccaf2d7e027489c449cf7e8f83c6346eb9"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ceec1a6bc6cab1d6ff5d06592a91a692f90ec7505d6463a88a52cc0eb58545da"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:040c1aebeda72197ef477a906782b5ab0d387642e93bda547336b8957c61022e"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f91de7223d4c7b793867797bacd1ee53bfe7359bd70d27b7b58a04efbb9436c8"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:faae4860798c31530dd184046a900e652c95513796ef51a12bc086710c2eec4d"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b0157420efcb803e71d1b28e2c287518b8808b7cf1ab8af36718fd0a2c453eb0"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:06f54715b7737c2fecdbf140d1afb11a33d59508a47bf11bb38ecf21dc9ab79f"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fdb7adb641a0d13bdcd4ef48e062363d8a9ad4a182ac7647ec88f695e719ae9f"}, + {file = "kiwisolver-1.4.5-cp311-cp311-win32.whl", hash = "sha256:bb86433b1cfe686da83ce32a9d3a8dd308e85c76b60896d58f082136f10bffac"}, + {file = "kiwisolver-1.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c08e1312a9cf1074d17b17728d3dfce2a5125b2d791527f33ffbe805200a355"}, + {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:32d5cf40c4f7c7b3ca500f8985eb3fb3a7dfc023215e876f207956b5ea26632a"}, + {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f846c260f483d1fd217fe5ed7c173fb109efa6b1fc8381c8b7552c5781756192"}, + {file = "kiwisolver-1.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ff5cf3571589b6d13bfbfd6bcd7a3f659e42f96b5fd1c4830c4cf21d4f5ef45"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7269d9e5f1084a653d575c7ec012ff57f0c042258bf5db0954bf551c158466e7"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da802a19d6e15dffe4b0c24b38b3af68e6c1a68e6e1d8f30148c83864f3881db"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3aba7311af82e335dd1e36ffff68aaca609ca6290c2cb6d821a39aa075d8e3ff"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763773d53f07244148ccac5b084da5adb90bfaee39c197554f01b286cf869228"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2270953c0d8cdab5d422bee7d2007f043473f9d2999631c86a223c9db56cbd16"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d099e745a512f7e3bbe7249ca835f4d357c586d78d79ae8f1dcd4d8adeb9bda9"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:74db36e14a7d1ce0986fa104f7d5637aea5c82ca6326ed0ec5694280942d1162"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e5bab140c309cb3a6ce373a9e71eb7e4873c70c2dda01df6820474f9889d6d4"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0f114aa76dc1b8f636d077979c0ac22e7cd8f3493abbab152f20eb8d3cda71f3"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:88a2df29d4724b9237fc0c6eaf2a1adae0cdc0b3e9f4d8e7dc54b16812d2d81a"}, + {file = "kiwisolver-1.4.5-cp312-cp312-win32.whl", hash = "sha256:72d40b33e834371fd330fb1472ca19d9b8327acb79a5821d4008391db8e29f20"}, + {file = "kiwisolver-1.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:2c5674c4e74d939b9d91dda0fae10597ac7521768fec9e399c70a1f27e2ea2d9"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3a2b053a0ab7a3960c98725cfb0bf5b48ba82f64ec95fe06f1d06c99b552e130"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cd32d6c13807e5c66a7cbb79f90b553642f296ae4518a60d8d76243b0ad2898"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59ec7b7c7e1a61061850d53aaf8e93db63dce0c936db1fda2658b70e4a1be709"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da4cfb373035def307905d05041c1d06d8936452fe89d464743ae7fb8371078b"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2400873bccc260b6ae184b2b8a4fec0e4082d30648eadb7c3d9a13405d861e89"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1b04139c4236a0f3aff534479b58f6f849a8b351e1314826c2d230849ed48985"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:4e66e81a5779b65ac21764c295087de82235597a2293d18d943f8e9e32746265"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7931d8f1f67c4be9ba1dd9c451fb0eeca1a25b89e4d3f89e828fe12a519b782a"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b3f7e75f3015df442238cca659f8baa5f42ce2a8582727981cbfa15fee0ee205"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:bbf1d63eef84b2e8c89011b7f2235b1e0bf7dacc11cac9431fc6468e99ac77fb"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4c380469bd3f970ef677bf2bcba2b6b0b4d5c75e7a020fb863ef75084efad66f"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-win32.whl", hash = "sha256:9408acf3270c4b6baad483865191e3e582b638b1654a007c62e3efe96f09a9a3"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-win_amd64.whl", hash = "sha256:5b94529f9b2591b7af5f3e0e730a4e0a41ea174af35a4fd067775f9bdfeee01a"}, + {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:11c7de8f692fc99816e8ac50d1d1aef4f75126eefc33ac79aac02c099fd3db71"}, + {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:53abb58632235cd154176ced1ae8f0d29a6657aa1aa9decf50b899b755bc2b93"}, + {file = "kiwisolver-1.4.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:88b9f257ca61b838b6f8094a62418421f87ac2a1069f7e896c36a7d86b5d4c29"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3195782b26fc03aa9c6913d5bad5aeb864bdc372924c093b0f1cebad603dd712"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc579bf0f502e54926519451b920e875f433aceb4624a3646b3252b5caa9e0b6"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a580c91d686376f0f7c295357595c5a026e6cbc3d77b7c36e290201e7c11ecb"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cfe6ab8da05c01ba6fbea630377b5da2cd9bcbc6338510116b01c1bc939a2c18"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d2e5a98f0ec99beb3c10e13b387f8db39106d53993f498b295f0c914328b1333"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a51a263952b1429e429ff236d2f5a21c5125437861baeed77f5e1cc2d2c7c6da"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3edd2fa14e68c9be82c5b16689e8d63d89fe927e56debd6e1dbce7a26a17f81b"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:74d1b44c6cfc897df648cc9fdaa09bc3e7679926e6f96df05775d4fb3946571c"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:76d9289ed3f7501012e05abb8358bbb129149dbd173f1f57a1bf1c22d19ab7cc"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:92dea1ffe3714fa8eb6a314d2b3c773208d865a0e0d35e713ec54eea08a66250"}, + {file = "kiwisolver-1.4.5-cp38-cp38-win32.whl", hash = "sha256:5c90ae8c8d32e472be041e76f9d2f2dbff4d0b0be8bd4041770eddb18cf49a4e"}, + {file = "kiwisolver-1.4.5-cp38-cp38-win_amd64.whl", hash = "sha256:c7940c1dc63eb37a67721b10d703247552416f719c4188c54e04334321351ced"}, + {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9407b6a5f0d675e8a827ad8742e1d6b49d9c1a1da5d952a67d50ef5f4170b18d"}, + {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15568384086b6df3c65353820a4473575dbad192e35010f622c6ce3eebd57af9"}, + {file = "kiwisolver-1.4.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0dc9db8e79f0036e8173c466d21ef18e1befc02de8bf8aa8dc0813a6dc8a7046"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cdc8a402aaee9a798b50d8b827d7ecf75edc5fb35ea0f91f213ff927c15f4ff0"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6c3bd3cde54cafb87d74d8db50b909705c62b17c2099b8f2e25b461882e544ff"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:955e8513d07a283056b1396e9a57ceddbd272d9252c14f154d450d227606eb54"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:346f5343b9e3f00b8db8ba359350eb124b98c99efd0b408728ac6ebf38173958"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9098e0049e88c6a24ff64545cdfc50807818ba6c1b739cae221bbbcbc58aad3"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:00bd361b903dc4bbf4eb165f24d1acbee754fce22ded24c3d56eec268658a5cf"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7b8b454bac16428b22560d0a1cf0a09875339cab69df61d7805bf48919415901"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f1d072c2eb0ad60d4c183f3fb44ac6f73fb7a8f16a2694a91f988275cbf352f9"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:31a82d498054cac9f6d0b53d02bb85811185bcb477d4b60144f915f3b3126342"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6512cb89e334e4700febbffaaa52761b65b4f5a3cf33f960213d5656cea36a77"}, + {file = "kiwisolver-1.4.5-cp39-cp39-win32.whl", hash = "sha256:9db8ea4c388fdb0f780fe91346fd438657ea602d58348753d9fb265ce1bca67f"}, + {file = "kiwisolver-1.4.5-cp39-cp39-win_amd64.whl", hash = "sha256:59415f46a37f7f2efeec758353dd2eae1b07640d8ca0f0c42548ec4125492635"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5c7b3b3a728dc6faf3fc372ef24f21d1e3cee2ac3e9596691d746e5a536de920"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:620ced262a86244e2be10a676b646f29c34537d0d9cc8eb26c08f53d98013390"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:378a214a1e3bbf5ac4a8708304318b4f890da88c9e6a07699c4ae7174c09a68d"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf7be1207676ac608a50cd08f102f6742dbfc70e8d60c4db1c6897f62f71523"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ba55dce0a9b8ff59495ddd050a0225d58bd0983d09f87cfe2b6aec4f2c1234e4"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fd32ea360bcbb92d28933fc05ed09bffcb1704ba3fc7942e81db0fd4f81a7892"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5e7139af55d1688f8b960ee9ad5adafc4ac17c1c473fe07133ac092310d76544"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dced8146011d2bc2e883f9bd68618b8247387f4bbec46d7392b3c3b032640126"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9bf3325c47b11b2e51bca0824ea217c7cd84491d8ac4eefd1e409705ef092bd"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5794cf59533bc3f1b1c821f7206a3617999db9fbefc345360aafe2e067514929"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e368f200bbc2e4f905b8e71eb38b3c04333bddaa6a2464a6355487b02bb7fb09"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5d706eba36b4c4d5bc6c6377bb6568098765e990cfc21ee16d13963fab7b3e7"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85267bd1aa8880a9c88a8cb71e18d3d64d2751a790e6ca6c27b8ccc724bcd5ad"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:210ef2c3a1f03272649aff1ef992df2e724748918c4bc2d5a90352849eb40bea"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:11d011a7574eb3b82bcc9c1a1d35c1d7075677fdd15de527d91b46bd35e935ee"}, + {file = "kiwisolver-1.4.5.tar.gz", hash = "sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec"}, +] + +[[package]] +name = "markdown" +version = "3.6" +description = "Python implementation of John Gruber's Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Markdown-3.6-py3-none-any.whl", hash = "sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f"}, + {file = "Markdown-3.6.tar.gz", hash = "sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224"}, +] + +[package.extras] +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] +testing = ["coverage", "pyyaml"] + +[[package]] +name = "markupsafe" +version = "2.1.5" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +] + +[[package]] +name = "matplotlib" +version = "3.9.1" +description = "Python plotting package" +optional = false +python-versions = ">=3.9" +files = [ + {file = "matplotlib-3.9.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7ccd6270066feb9a9d8e0705aa027f1ff39f354c72a87efe8fa07632f30fc6bb"}, + {file = "matplotlib-3.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:591d3a88903a30a6d23b040c1e44d1afdd0d778758d07110eb7596f811f31842"}, + {file = "matplotlib-3.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd2a59ff4b83d33bca3b5ec58203cc65985367812cb8c257f3e101632be86d92"}, + {file = "matplotlib-3.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc001516ffcf1a221beb51198b194d9230199d6842c540108e4ce109ac05cc0"}, + {file = "matplotlib-3.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:83c6a792f1465d174c86d06f3ae85a8fe36e6f5964633ae8106312ec0921fdf5"}, + {file = "matplotlib-3.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:421851f4f57350bcf0811edd754a708d2275533e84f52f6760b740766c6747a7"}, + {file = "matplotlib-3.9.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b3fce58971b465e01b5c538f9d44915640c20ec5ff31346e963c9e1cd66fa812"}, + {file = "matplotlib-3.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a973c53ad0668c53e0ed76b27d2eeeae8799836fd0d0caaa4ecc66bf4e6676c0"}, + {file = "matplotlib-3.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd5acf8f3ef43f7532c2f230249720f5dc5dd40ecafaf1c60ac8200d46d7eb"}, + {file = "matplotlib-3.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab38a4f3772523179b2f772103d8030215b318fef6360cb40558f585bf3d017f"}, + {file = "matplotlib-3.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2315837485ca6188a4b632c5199900e28d33b481eb083663f6a44cfc8987ded3"}, + {file = "matplotlib-3.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:a0c977c5c382f6696caf0bd277ef4f936da7e2aa202ff66cad5f0ac1428ee15b"}, + {file = "matplotlib-3.9.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:565d572efea2b94f264dd86ef27919515aa6d629252a169b42ce5f570db7f37b"}, + {file = "matplotlib-3.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d397fd8ccc64af2ec0af1f0efc3bacd745ebfb9d507f3f552e8adb689ed730a"}, + {file = "matplotlib-3.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26040c8f5121cd1ad712abffcd4b5222a8aec3a0fe40bc8542c94331deb8780d"}, + {file = "matplotlib-3.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12cb1837cffaac087ad6b44399d5e22b78c729de3cdae4629e252067b705e2b"}, + {file = "matplotlib-3.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0e835c6988edc3d2d08794f73c323cc62483e13df0194719ecb0723b564e0b5c"}, + {file = "matplotlib-3.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:44a21d922f78ce40435cb35b43dd7d573cf2a30138d5c4b709d19f00e3907fd7"}, + {file = "matplotlib-3.9.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:0c584210c755ae921283d21d01f03a49ef46d1afa184134dd0f95b0202ee6f03"}, + {file = "matplotlib-3.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11fed08f34fa682c2b792942f8902e7aefeed400da71f9e5816bea40a7ce28fe"}, + {file = "matplotlib-3.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0000354e32efcfd86bda75729716b92f5c2edd5b947200be9881f0a671565c33"}, + {file = "matplotlib-3.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4db17fea0ae3aceb8e9ac69c7e3051bae0b3d083bfec932240f9bf5d0197a049"}, + {file = "matplotlib-3.9.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:208cbce658b72bf6a8e675058fbbf59f67814057ae78165d8a2f87c45b48d0ff"}, + {file = "matplotlib-3.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:dc23f48ab630474264276be156d0d7710ac6c5a09648ccdf49fef9200d8cbe80"}, + {file = "matplotlib-3.9.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3fda72d4d472e2ccd1be0e9ccb6bf0d2eaf635e7f8f51d737ed7e465ac020cb3"}, + {file = "matplotlib-3.9.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:84b3ba8429935a444f1fdc80ed930babbe06725bcf09fbeb5c8757a2cd74af04"}, + {file = "matplotlib-3.9.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b918770bf3e07845408716e5bbda17eadfc3fcbd9307dc67f37d6cf834bb3d98"}, + {file = "matplotlib-3.9.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f1f2e5d29e9435c97ad4c36fb6668e89aee13d48c75893e25cef064675038ac9"}, + {file = "matplotlib-3.9.1.tar.gz", hash = "sha256:de06b19b8db95dd33d0dc17c926c7c9ebed9f572074b6fac4f65068a6814d010"}, +] + +[package.dependencies] +contourpy = ">=1.0.1" +cycler = ">=0.10" +fonttools = ">=4.22.0" +kiwisolver = ">=1.3.1" +numpy = ">=1.23" +packaging = ">=20.0" +pillow = ">=8" +pyparsing = ">=2.3.1" +python-dateutil = ">=2.7" + +[package.extras] +dev = ["meson-python (>=0.13.1)", "numpy (>=1.25)", "pybind11 (>=2.6)", "setuptools (>=64)", "setuptools_scm (>=7)"] + +[[package]] +name = "mergedeep" +version = "1.3.4" +description = "A deep merge function for 🐍." +optional = false +python-versions = ">=3.6" +files = [ + {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, + {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, +] + +[[package]] +name = "mike" +version = "2.1.2" +description = "Manage multiple versions of your MkDocs-powered documentation" +optional = false +python-versions = "*" +files = [ + {file = "mike-2.1.2-py3-none-any.whl", hash = "sha256:d61d9b423ab412d634ca2bd520136d5114e3cc73f4bbd1aa6a0c6625c04918c0"}, + {file = "mike-2.1.2.tar.gz", hash = "sha256:d59cc8054c50f9c8a046cfd47f9b700cf9ff1b2b19f420bd8812ca6f94fa8bd3"}, +] + +[package.dependencies] +importlib-metadata = "*" +importlib-resources = "*" +jinja2 = ">=2.7" +mkdocs = ">=1.0" +pyparsing = ">=3.0" +pyyaml = ">=5.1" +pyyaml-env-tag = "*" +verspec = "*" + +[package.extras] +dev = ["coverage", "flake8 (>=3.0)", "flake8-quotes", "shtab"] +test = ["coverage", "flake8 (>=3.0)", "flake8-quotes", "shtab"] + +[[package]] +name = "mkdocs" +version = "1.6.0" +description = "Project documentation with Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs-1.6.0-py3-none-any.whl", hash = "sha256:1eb5cb7676b7d89323e62b56235010216319217d4af5ddc543a91beb8d125ea7"}, + {file = "mkdocs-1.6.0.tar.gz", hash = "sha256:a73f735824ef83a4f3bcb7a231dcab23f5a838f88b7efc54a0eef5fbdbc3c512"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} +ghp-import = ">=1.0" +jinja2 = ">=2.11.1" +markdown = ">=3.3.6" +markupsafe = ">=2.0.1" +mergedeep = ">=1.3.4" +mkdocs-get-deps = ">=0.2.0" +packaging = ">=20.5" +pathspec = ">=0.11.1" +pyyaml = ">=5.1" +pyyaml-env-tag = ">=0.1" +watchdog = ">=2.0" + +[package.extras] +i18n = ["babel (>=2.9.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.4)", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, + {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, +] + +[package.dependencies] +mergedeep = ">=1.3.4" +platformdirs = ">=2.2.0" +pyyaml = ">=5.1" + +[[package]] +name = "mkdocs-material" +version = "9.5.29" +description = "Documentation that simply works" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_material-9.5.29-py3-none-any.whl", hash = "sha256:afc1f508e2662ded95f0a35a329e8a5acd73ee88ca07ba73836eb6fcdae5d8b4"}, + {file = "mkdocs_material-9.5.29.tar.gz", hash = "sha256:3e977598ec15a4ddad5c4dfc9e08edab6023edb51e88f0729bd27be77e3d322a"}, +] + +[package.dependencies] +babel = ">=2.10,<3.0" +colorama = ">=0.4,<1.0" +jinja2 = ">=3.0,<4.0" +markdown = ">=3.2,<4.0" +mkdocs = ">=1.6,<2.0" +mkdocs-material-extensions = ">=1.3,<2.0" +paginate = ">=0.5,<1.0" +pygments = ">=2.16,<3.0" +pymdown-extensions = ">=10.2,<11.0" +regex = ">=2022.4" +requests = ">=2.26,<3.0" + +[package.extras] +git = ["mkdocs-git-committers-plugin-2 (>=1.1,<2.0)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"] +imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=10.2,<11.0)"] +recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +description = "Extension pack for Python Markdown and MkDocs Material." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, + {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, +] + +[[package]] +name = "mkl" +version = "2021.4.0" +description = "Intel® oneAPI Math Kernel Library" +optional = false +python-versions = "*" +files = [ + {file = "mkl-2021.4.0-py2.py3-none-macosx_10_15_x86_64.macosx_11_0_x86_64.whl", hash = "sha256:67460f5cd7e30e405b54d70d1ed3ca78118370b65f7327d495e9c8847705e2fb"}, + {file = "mkl-2021.4.0-py2.py3-none-manylinux1_i686.whl", hash = "sha256:636d07d90e68ccc9630c654d47ce9fdeb036bb46e2b193b3a9ac8cfea683cce5"}, + {file = "mkl-2021.4.0-py2.py3-none-manylinux1_x86_64.whl", hash = "sha256:398dbf2b0d12acaf54117a5210e8f191827f373d362d796091d161f610c1ebfb"}, + {file = "mkl-2021.4.0-py2.py3-none-win32.whl", hash = "sha256:439c640b269a5668134e3dcbcea4350459c4a8bc46469669b2d67e07e3d330e8"}, + {file = "mkl-2021.4.0-py2.py3-none-win_amd64.whl", hash = "sha256:ceef3cafce4c009dd25f65d7ad0d833a0fbadc3d8903991ec92351fe5de1e718"}, +] + +[package.dependencies] +intel-openmp = "==2021.*" +tbb = "==2021.*" + +[[package]] +name = "mpmath" +version = "1.3.0" +description = "Python library for arbitrary-precision floating-point arithmetic" +optional = false +python-versions = "*" +files = [ + {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"}, + {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"}, +] + +[package.extras] +develop = ["codecov", "pycodestyle", "pytest (>=4.6)", "pytest-cov", "wheel"] +docs = ["sphinx"] +gmpy = ["gmpy2 (>=2.1.0a4)"] +tests = ["pytest (>=4.6)"] + +[[package]] +name = "multidict" +version = "6.0.5" +description = "multidict implementation" +optional = false +python-versions = ">=3.7" +files = [ + {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"}, + {file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"}, + {file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"}, + {file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"}, + {file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"}, + {file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"}, + {file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"}, + {file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"}, + {file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"}, + {file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"}, + {file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"}, + {file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"}, + {file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"}, + {file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"}, + {file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"}, + {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, +] + +[[package]] +name = "networkx" +version = "3.3" +description = "Python package for creating and manipulating graphs and networks" +optional = false +python-versions = ">=3.10" +files = [ + {file = "networkx-3.3-py3-none-any.whl", hash = "sha256:28575580c6ebdaf4505b22c6256a2b9de86b316dc63ba9e93abde3d78dfdbcf2"}, + {file = "networkx-3.3.tar.gz", hash = "sha256:0c127d8b2f4865f59ae9cb8aafcd60b5c70f3241ebd66f7defad7c4ab90126c9"}, +] + +[package.extras] +default = ["matplotlib (>=3.6)", "numpy (>=1.23)", "pandas (>=1.4)", "scipy (>=1.9,!=1.11.0,!=1.11.1)"] +developer = ["changelist (==0.5)", "mypy (>=1.1)", "pre-commit (>=3.2)", "rtoml"] +doc = ["myst-nb (>=1.0)", "numpydoc (>=1.7)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.14)", "sphinx (>=7)", "sphinx-gallery (>=0.14)", "texext (>=0.6.7)"] +extra = ["lxml (>=4.6)", "pydot (>=2.0)", "pygraphviz (>=1.12)", "sympy (>=1.10)"] +test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"] + +[[package]] +name = "nltk" +version = "3.8.1" +description = "Natural Language Toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "nltk-3.8.1-py3-none-any.whl", hash = "sha256:fd5c9109f976fa86bcadba8f91e47f5e9293bd034474752e92a520f81c93dda5"}, + {file = "nltk-3.8.1.zip", hash = "sha256:1834da3d0682cba4f2cede2f9aad6b0fafb6461ba451db0efb6f9c39798d64d3"}, +] + +[package.dependencies] +click = "*" +joblib = "*" +regex = ">=2021.8.3" +tqdm = "*" + +[package.extras] +all = ["matplotlib", "numpy", "pyparsing", "python-crfsuite", "requests", "scikit-learn", "scipy", "twython"] +corenlp = ["requests"] +machine-learning = ["numpy", "python-crfsuite", "scikit-learn", "scipy"] +plot = ["matplotlib"] +tgrep = ["pyparsing"] +twitter = ["twython"] + +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "numpy" +version = "2.0.1" +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"}, +] + +[[package]] +name = "packaging" +version = "24.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] + +[[package]] +name = "paginate" +version = "0.5.6" +description = "Divides large result sets into pages for easier browsing" +optional = false +python-versions = "*" +files = [ + {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"}, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "pillow" +version = "10.4.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"}, + {file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc"}, + {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e"}, + {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46"}, + {file = "pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984"}, + {file = "pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141"}, + {file = "pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1"}, + {file = "pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c"}, + {file = "pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319"}, + {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d"}, + {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696"}, + {file = "pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496"}, + {file = "pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91"}, + {file = "pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22"}, + {file = "pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94"}, + {file = "pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a"}, + {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b"}, + {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9"}, + {file = "pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42"}, + {file = "pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a"}, + {file = "pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9"}, + {file = "pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3"}, + {file = "pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc"}, + {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a"}, + {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309"}, + {file = "pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060"}, + {file = "pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea"}, + {file = "pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d"}, + {file = "pillow-10.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736"}, + {file = "pillow-10.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd"}, + {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84"}, + {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0"}, + {file = "pillow-10.4.0-cp38-cp38-win32.whl", hash = "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e"}, + {file = "pillow-10.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab"}, + {file = "pillow-10.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d"}, + {file = "pillow-10.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c"}, + {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1"}, + {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df"}, + {file = "pillow-10.4.0-cp39-cp39-win32.whl", hash = "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef"}, + {file = "pillow-10.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5"}, + {file = "pillow-10.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3"}, + {file = "pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +typing = ["typing-extensions"] +xmp = ["defusedxml"] + +[[package]] +name = "platformdirs" +version = "4.2.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, + {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +type = ["mypy (>=1.8)"] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "3.7.1" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pre_commit-3.7.1-py2.py3-none-any.whl", hash = "sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5"}, + {file = "pre_commit-3.7.1.tar.gz", hash = "sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "prompt-toolkit" +version = "3.0.36" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.6.2" +files = [ + {file = "prompt_toolkit-3.0.36-py3-none-any.whl", hash = "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305"}, + {file = "prompt_toolkit-3.0.36.tar.gz", hash = "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63"}, +] + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "pyahocorasick" +version = "2.1.0" +description = "pyahocorasick is a fast and memory efficient library for exact or approximate multi-pattern string search. With the ``ahocorasick.Automaton`` class, you can find multiple key string occurrences at once in some input text. You can use it as a plain dict-like Trie or convert a Trie to an automaton for efficient Aho-Corasick search. And pickle to disk for easy reuse of large automatons. Implemented in C and tested on Python 3.6+. Works on Linux, macOS and Windows. BSD-3-Cause license." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyahocorasick-2.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c46288044c4f71392efb4f5da0cb8abd160787a8b027afc85079e9c3d7551eb"}, + {file = "pyahocorasick-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1f15529c83b8c6e0548d7d3c5631fefa23fba5190e67be49d6c9e24a6358ff9c"}, + {file = "pyahocorasick-2.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:581e3d85043f1797543796f021e8d7d48c18e594529b72d86f70ea78abc88fff"}, + {file = "pyahocorasick-2.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c860ad9cb59e56c31aed8a5d1ee9d83a0151277b09198d027ffce213697716ed"}, + {file = "pyahocorasick-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:4f8eba88fce34a1d8020638a4a8732c6241a5d85fe12be8669b7495d99d36b6a"}, + {file = "pyahocorasick-2.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d6e0da0a8fc78c694778dced537c1bfb8b2f178ec92a82d81539d2e35a15cba0"}, + {file = "pyahocorasick-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:658d55e51c7588a5dba57de674241a16a3c94bf57f3bfd70022c4d7defe2b0f4"}, + {file = "pyahocorasick-2.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9f2728ac77bab807ba65c6ef41be30358ef0c9bb6960c9fe070d43f7024cb91"}, + {file = "pyahocorasick-2.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a58c44c407a45155dc7a3253274b5fd78ab00b579bd5685059610867cdb37142"}, + {file = "pyahocorasick-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:d8254d6333df5eb400ed3ec8b24da9e3f5da8e28b94a71392391703a7aac568d"}, + {file = "pyahocorasick-2.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:82b0d20e82cc282fd29324e8df93809cebbffb345055214ce4b7873698df02c8"}, + {file = "pyahocorasick-2.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6dedb9fed92705b742d6aa3d87abb1ec999f57310ef32b962f65f4e42182fe0a"}, + {file = "pyahocorasick-2.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f209796e7d354734781dd883c333596e482c70136fa76a4cb169f383e6c40bca"}, + {file = "pyahocorasick-2.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8337af64c649223cff548c7204dda823e83622d63e5449bc51ae069efb2f240f"}, + {file = "pyahocorasick-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:5ebe0d1e15afb782477e3d0aa1dce28ab9dad1200211fb785b9c1cc1208e6f04"}, + {file = "pyahocorasick-2.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:7454ba5fa528958ca9a1bc3143f8e980bd7817ea481f46495e6ffa89675ab93b"}, + {file = "pyahocorasick-2.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3795ac922d21fbfea40a6b3a330762e8b38ce8ba511b1eb15bf9eeb9303b2662"}, + {file = "pyahocorasick-2.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8e92150849a3c13da37e37ca6374fa55960fd5c845029eca02d9b5846b26fe48"}, + {file = "pyahocorasick-2.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:23b183600e2087f16f6c5e6185d61525ad74335f2a5b693dd6d66bba2f6a4b05"}, + {file = "pyahocorasick-2.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:7034b26e145518610651339b8701568a3533a3114b00cf55f22bca80bff58e6d"}, + {file = "pyahocorasick-2.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:36491675a13fe4181a6b3bccfc9032a1a5d03bd3b0a151c06f8865c16ba44b42"}, + {file = "pyahocorasick-2.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:895ab1ff5384ee5325c74cbacafc419e534f1f110b9fb3c544cc56832ecce082"}, + {file = "pyahocorasick-2.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:bf4a4b19ac37e9a7087646b8bcc306acd7a91649355d59b866b756068e35d018"}, + {file = "pyahocorasick-2.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f44f96496aa773fc5bf302ddf968dd6b920fab34522f944392af8bde13cbe805"}, + {file = "pyahocorasick-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:05b7c2ef52da247efec6fb5a011113b7e943e961e22aaaf757cb9c15083440c9"}, + {file = "pyahocorasick-2.1.0.tar.gz", hash = "sha256:4df4845c1149e9fa4aa33f0f0aa35f5a42957a43a3d6e447c9b44e679e2672ea"}, +] + +[package.extras] +testing = ["pytest", "setuptools", "twine", "wheel"] + +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + +[[package]] +name = "pydantic" +version = "2.8.2" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, + {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.20.1" +typing-extensions = {version = ">=4.6.1", markers = "python_version < \"3.13\""} + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.20.1" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, + {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, + {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, + {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, + {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, + {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, + {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, + {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, + {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, + {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, + {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, + {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, + {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, + {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, + {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, + {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, + {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, + {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, + {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"}, + {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"}, + {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"}, + {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"}, + {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"}, + {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"}, + {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"}, + {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"}, + {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"}, + {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"}, + {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"}, + {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, + {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pygments" +version = "2.18.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pymdown-extensions" +version = "10.8.1" +description = "Extension pack for Python Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pymdown_extensions-10.8.1-py3-none-any.whl", hash = "sha256:f938326115884f48c6059c67377c46cf631c733ef3629b6eed1349989d1b30cb"}, + {file = "pymdown_extensions-10.8.1.tar.gz", hash = "sha256:3ab1db5c9e21728dabf75192d71471f8e50f216627e9a1fa9535ecb0231b9940"}, +] + +[package.dependencies] +markdown = ">=3.6" +pyyaml = "*" + +[package.extras] +extra = ["pygments (>=2.12)"] + +[[package]] +name = "pynacl" +version = "1.5.0" +description = "Python binding to the Networking and Cryptography (NaCl) library" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, + {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, +] + +[package.dependencies] +cffi = ">=1.4.1" + +[package.extras] +docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] +tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] + +[[package]] +name = "pyparsing" +version = "3.1.2" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.6.8" +files = [ + {file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"}, + {file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pyreadline3" +version = "3.4.1" +description = "A python implementation of GNU readline." +optional = false +python-versions = "*" +files = [ + {file = "pyreadline3-3.4.1-py3-none-any.whl", hash = "sha256:b0efb6516fd4fb07b45949053826a62fa4cb353db5be2bbb4a7aa1fdd1e345fb"}, + {file = "pyreadline3-3.4.1.tar.gz", hash = "sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae"}, +] + +[[package]] +name = "pyright" +version = "1.1.373" +description = "Command line wrapper for pyright" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyright-1.1.373-py3-none-any.whl", hash = "sha256:b805413227f2c209f27b14b55da27fe5e9fb84129c9f1eb27708a5d12f6f000e"}, + {file = "pyright-1.1.373.tar.gz", hash = "sha256:f41bcfc8b9d1802b09921a394d6ae1ce19694957b628bc657629688daf8a83ff"}, +] + +[package.dependencies] +nodeenv = ">=1.6.0" + +[package.extras] +all = ["twine (>=3.4.1)"] +dev = ["twine (>=3.4.1)"] + +[[package]] +name = "pytest" +version = "8.2.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, + {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2.0" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.23.7" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest_asyncio-0.23.7-py3-none-any.whl", hash = "sha256:009b48127fbe44518a547bddd25611551b0e43ccdbf1e67d12479f569832c20b"}, + {file = "pytest_asyncio-0.23.7.tar.gz", hash = "sha256:5f5c72948f4c49e7db4f29f2521d4031f1c27f86e57b046126654083d4770268"}, +] + +[package.dependencies] +pytest = ">=7.0.0,<9" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + +[[package]] +name = "pytest-mock" +version = "3.14.0" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, + {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, +] + +[package.dependencies] +pytest = ">=6.2.5" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + +[[package]] +name = "pytest-sugar" +version = "1.0.0" +description = "pytest-sugar is a plugin for pytest that changes the default look and feel of pytest (e.g. progressbar, show tests that fail instantly)." +optional = false +python-versions = "*" +files = [ + {file = "pytest-sugar-1.0.0.tar.gz", hash = "sha256:6422e83258f5b0c04ce7c632176c7732cab5fdb909cb39cca5c9139f81276c0a"}, + {file = "pytest_sugar-1.0.0-py3-none-any.whl", hash = "sha256:70ebcd8fc5795dc457ff8b69d266a4e2e8a74ae0c3edc749381c64b5246c8dfd"}, +] + +[package.dependencies] +packaging = ">=21.3" +pytest = ">=6.2.0" +termcolor = ">=2.1.0" + +[package.extras] +dev = ["black", "flake8", "pre-commit"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-dotenv" +version = "1.0.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +description = "A custom YAML tag for referencing environment variables in YAML files. " +optional = false +python-versions = ">=3.6" +files = [ + {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, + {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, +] + +[package.dependencies] +pyyaml = "*" + +[[package]] +name = "questionary" +version = "2.0.1" +description = "Python library to build pretty command line user prompts ⭐️" +optional = false +python-versions = ">=3.8" +files = [ + {file = "questionary-2.0.1-py3-none-any.whl", hash = "sha256:8ab9a01d0b91b68444dff7f6652c1e754105533f083cbe27597c8110ecc230a2"}, + {file = "questionary-2.0.1.tar.gz", hash = "sha256:bcce898bf3dbb446ff62830c86c5c6fb9a22a54146f0f5597d3da43b10d8fc8b"}, +] + +[package.dependencies] +prompt_toolkit = ">=2.0,<=3.0.36" + +[[package]] +name = "redis" +version = "5.0.7" +description = "Python client for Redis database and key-value store" +optional = false +python-versions = ">=3.7" +files = [ + {file = "redis-5.0.7-py3-none-any.whl", hash = "sha256:0e479e24da960c690be5d9b96d21f7b918a98c0cf49af3b6fafaa0753f93a0db"}, + {file = "redis-5.0.7.tar.gz", hash = "sha256:8f611490b93c8109b50adc317b31bfd84fff31def3475b92e7e80bf39f48175b"}, +] + +[package.dependencies] +hiredis = {version = ">=1.0.0", optional = true, markers = "extra == \"hiredis\""} + +[package.extras] +hiredis = ["hiredis (>=1.0.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] + +[[package]] +name = "redisvl" +version = "0.2.3" +description = "Python client library and CLI for using Redis as a vector database" +optional = false +python-versions = "<4.0,>=3.9" +files = [ + {file = "redisvl-0.2.3-py3-none-any.whl", hash = "sha256:0739889de62f45345b501fbd6aea03b7350e25dc078c9154c0bc9c32d67e5eb1"}, + {file = "redisvl-0.2.3.tar.gz", hash = "sha256:13e07a3f70550f74b424ab3734171661a9199c9d1a9f6e001c38ccc422e71f83"}, +] + +[package.dependencies] +coloredlogs = "*" +numpy = "*" +pydantic = ">=2,<3" +pyyaml = "*" +redis = ">=5.0.0" +tabulate = ">=0.9.0,<1" +tenacity = ">=8.2.2" + +[package.extras] +cohere = ["cohere (>=4.44)"] +google-cloud-aiplatform = ["google-cloud-aiplatform (>=1.26)"] +mistralai = ["mistralai (>=0.2.0)"] +openai = ["openai (>=1.13.0)"] +sentence-transformers = ["sentence-transformers (>=2.2.2)"] + +[[package]] +name = "regex" +version = "2024.7.24" +description = "Alternative regular expression module, to replace re." +optional = false +python-versions = ">=3.8" +files = [ + {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]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "ruff" +version = "0.5.2" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.5.2-py3-none-linux_armv6l.whl", hash = "sha256:7bab8345df60f9368d5f4594bfb8b71157496b44c30ff035d1d01972e764d3be"}, + {file = "ruff-0.5.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:1aa7acad382ada0189dbe76095cf0a36cd0036779607c397ffdea16517f535b1"}, + {file = "ruff-0.5.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:aec618d5a0cdba5592c60c2dee7d9c865180627f1a4a691257dea14ac1aa264d"}, + {file = "ruff-0.5.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0b62adc5ce81780ff04077e88bac0986363e4a3260ad3ef11ae9c14aa0e67ef"}, + {file = "ruff-0.5.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dc42ebf56ede83cb080a50eba35a06e636775649a1ffd03dc986533f878702a3"}, + {file = "ruff-0.5.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c15c6e9f88c67ffa442681365d11df38afb11059fc44238e71a9d9f1fd51de70"}, + {file = "ruff-0.5.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d3de9a5960f72c335ef00763d861fc5005ef0644cb260ba1b5a115a102157251"}, + {file = "ruff-0.5.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe5a968ae933e8f7627a7b2fc8893336ac2be0eb0aace762d3421f6e8f7b7f83"}, + {file = "ruff-0.5.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a04f54a9018f75615ae52f36ea1c5515e356e5d5e214b22609ddb546baef7132"}, + {file = "ruff-0.5.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed02fb52e3741f0738db5f93e10ae0fb5c71eb33a4f2ba87c9a2fa97462a649"}, + {file = "ruff-0.5.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3cf8fe659f6362530435d97d738eb413e9f090e7e993f88711b0377fbdc99f60"}, + {file = "ruff-0.5.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:237a37e673e9f3cbfff0d2243e797c4862a44c93d2f52a52021c1a1b0899f846"}, + {file = "ruff-0.5.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2a2949ce7c1cbd8317432ada80fe32156df825b2fd611688814c8557824ef060"}, + {file = "ruff-0.5.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:481af57c8e99da92ad168924fd82220266043c8255942a1cb87958b108ac9335"}, + {file = "ruff-0.5.2-py3-none-win32.whl", hash = "sha256:f1aea290c56d913e363066d83d3fc26848814a1fed3d72144ff9c930e8c7c718"}, + {file = "ruff-0.5.2-py3-none-win_amd64.whl", hash = "sha256:8532660b72b5d94d2a0a7a27ae7b9b40053662d00357bb2a6864dd7e38819084"}, + {file = "ruff-0.5.2-py3-none-win_arm64.whl", hash = "sha256:73439805c5cb68f364d826a5c5c4b6c798ded6b7ebaa4011f01ce6c94e4d5583"}, + {file = "ruff-0.5.2.tar.gz", hash = "sha256:2c0df2d2de685433794a14d8d2e240df619b748fbe3367346baa519d8e6f1ca2"}, +] + +[[package]] +name = "safetensors" +version = "0.4.3" +description = "" +optional = false +python-versions = ">=3.7" +files = [ + {file = "safetensors-0.4.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:dcf5705cab159ce0130cd56057f5f3425023c407e170bca60b4868048bae64fd"}, + {file = "safetensors-0.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bb4f8c5d0358a31e9a08daeebb68f5e161cdd4018855426d3f0c23bb51087055"}, + {file = "safetensors-0.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70a5319ef409e7f88686a46607cbc3c428271069d8b770076feaf913664a07ac"}, + {file = "safetensors-0.4.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb9c65bd82f9ef3ce4970dc19ee86be5f6f93d032159acf35e663c6bea02b237"}, + {file = "safetensors-0.4.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:edb5698a7bc282089f64c96c477846950358a46ede85a1c040e0230344fdde10"}, + {file = "safetensors-0.4.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:efcc860be094b8d19ac61b452ec635c7acb9afa77beb218b1d7784c6d41fe8ad"}, + {file = "safetensors-0.4.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d88b33980222085dd6001ae2cad87c6068e0991d4f5ccf44975d216db3b57376"}, + {file = "safetensors-0.4.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5fc6775529fb9f0ce2266edd3e5d3f10aab068e49f765e11f6f2a63b5367021d"}, + {file = "safetensors-0.4.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9c6ad011c1b4e3acff058d6b090f1da8e55a332fbf84695cf3100c649cc452d1"}, + {file = "safetensors-0.4.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8c496c5401c1b9c46d41a7688e8ff5b0310a3b9bae31ce0f0ae870e1ea2b8caf"}, + {file = "safetensors-0.4.3-cp310-none-win32.whl", hash = "sha256:38e2a8666178224a51cca61d3cb4c88704f696eac8f72a49a598a93bbd8a4af9"}, + {file = "safetensors-0.4.3-cp310-none-win_amd64.whl", hash = "sha256:393e6e391467d1b2b829c77e47d726f3b9b93630e6a045b1d1fca67dc78bf632"}, + {file = "safetensors-0.4.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:22f3b5d65e440cec0de8edaa672efa888030802e11c09b3d6203bff60ebff05a"}, + {file = "safetensors-0.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c4fa560ebd4522adddb71dcd25d09bf211b5634003f015a4b815b7647d62ebe"}, + {file = "safetensors-0.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9afd5358719f1b2cf425fad638fc3c887997d6782da317096877e5b15b2ce93"}, + {file = "safetensors-0.4.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d8c5093206ef4b198600ae484230402af6713dab1bd5b8e231905d754022bec7"}, + {file = "safetensors-0.4.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0b2104df1579d6ba9052c0ae0e3137c9698b2d85b0645507e6fd1813b70931a"}, + {file = "safetensors-0.4.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8cf18888606dad030455d18f6c381720e57fc6a4170ee1966adb7ebc98d4d6a3"}, + {file = "safetensors-0.4.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0bf4f9d6323d9f86eef5567eabd88f070691cf031d4c0df27a40d3b4aaee755b"}, + {file = "safetensors-0.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:585c9ae13a205807b63bef8a37994f30c917ff800ab8a1ca9c9b5d73024f97ee"}, + {file = "safetensors-0.4.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faefeb3b81bdfb4e5a55b9bbdf3d8d8753f65506e1d67d03f5c851a6c87150e9"}, + {file = "safetensors-0.4.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:befdf0167ad626f22f6aac6163477fcefa342224a22f11fdd05abb3995c1783c"}, + {file = "safetensors-0.4.3-cp311-none-win32.whl", hash = "sha256:a7cef55929dcbef24af3eb40bedec35d82c3c2fa46338bb13ecf3c5720af8a61"}, + {file = "safetensors-0.4.3-cp311-none-win_amd64.whl", hash = "sha256:840b7ac0eff5633e1d053cc9db12fdf56b566e9403b4950b2dc85393d9b88d67"}, + {file = "safetensors-0.4.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:22d21760dc6ebae42e9c058d75aa9907d9f35e38f896e3c69ba0e7b213033856"}, + {file = "safetensors-0.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d22c1a10dff3f64d0d68abb8298a3fd88ccff79f408a3e15b3e7f637ef5c980"}, + {file = "safetensors-0.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1648568667f820b8c48317c7006221dc40aced1869908c187f493838a1362bc"}, + {file = "safetensors-0.4.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:446e9fe52c051aeab12aac63d1017e0f68a02a92a027b901c4f8e931b24e5397"}, + {file = "safetensors-0.4.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fef5d70683643618244a4f5221053567ca3e77c2531e42ad48ae05fae909f542"}, + {file = "safetensors-0.4.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a1f4430cc0c9d6afa01214a4b3919d0a029637df8e09675ceef1ca3f0dfa0df"}, + {file = "safetensors-0.4.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d603846a8585b9432a0fd415db1d4c57c0f860eb4aea21f92559ff9902bae4d"}, + {file = "safetensors-0.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a844cdb5d7cbc22f5f16c7e2a0271170750763c4db08381b7f696dbd2c78a361"}, + {file = "safetensors-0.4.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:88887f69f7a00cf02b954cdc3034ffb383b2303bc0ab481d4716e2da51ddc10e"}, + {file = "safetensors-0.4.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ee463219d9ec6c2be1d331ab13a8e0cd50d2f32240a81d498266d77d07b7e71e"}, + {file = "safetensors-0.4.3-cp312-none-win32.whl", hash = "sha256:d0dd4a1db09db2dba0f94d15addc7e7cd3a7b0d393aa4c7518c39ae7374623c3"}, + {file = "safetensors-0.4.3-cp312-none-win_amd64.whl", hash = "sha256:d14d30c25897b2bf19b6fb5ff7e26cc40006ad53fd4a88244fdf26517d852dd7"}, + {file = "safetensors-0.4.3-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:d1456f814655b224d4bf6e7915c51ce74e389b413be791203092b7ff78c936dd"}, + {file = "safetensors-0.4.3-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:455d538aa1aae4a8b279344a08136d3f16334247907b18a5c3c7fa88ef0d3c46"}, + {file = "safetensors-0.4.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf476bca34e1340ee3294ef13e2c625833f83d096cfdf69a5342475602004f95"}, + {file = "safetensors-0.4.3-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:02ef3a24face643456020536591fbd3c717c5abaa2737ec428ccbbc86dffa7a4"}, + {file = "safetensors-0.4.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7de32d0d34b6623bb56ca278f90db081f85fb9c5d327e3c18fd23ac64f465768"}, + {file = "safetensors-0.4.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a0deb16a1d3ea90c244ceb42d2c6c276059616be21a19ac7101aa97da448faf"}, + {file = "safetensors-0.4.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c59d51f182c729f47e841510b70b967b0752039f79f1de23bcdd86462a9b09ee"}, + {file = "safetensors-0.4.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1f598b713cc1a4eb31d3b3203557ac308acf21c8f41104cdd74bf640c6e538e3"}, + {file = "safetensors-0.4.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5757e4688f20df083e233b47de43845d1adb7e17b6cf7da5f8444416fc53828d"}, + {file = "safetensors-0.4.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:fe746d03ed8d193674a26105e4f0fe6c726f5bb602ffc695b409eaf02f04763d"}, + {file = "safetensors-0.4.3-cp37-none-win32.whl", hash = "sha256:0d5ffc6a80f715c30af253e0e288ad1cd97a3d0086c9c87995e5093ebc075e50"}, + {file = "safetensors-0.4.3-cp37-none-win_amd64.whl", hash = "sha256:a11c374eb63a9c16c5ed146457241182f310902bd2a9c18255781bb832b6748b"}, + {file = "safetensors-0.4.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b1e31be7945f66be23f4ec1682bb47faa3df34cb89fc68527de6554d3c4258a4"}, + {file = "safetensors-0.4.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:03a4447c784917c9bf01d8f2ac5080bc15c41692202cd5f406afba16629e84d6"}, + {file = "safetensors-0.4.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d244bcafeb1bc06d47cfee71727e775bca88a8efda77a13e7306aae3813fa7e4"}, + {file = "safetensors-0.4.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53c4879b9c6bd7cd25d114ee0ef95420e2812e676314300624594940a8d6a91f"}, + {file = "safetensors-0.4.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74707624b81f1b7f2b93f5619d4a9f00934d5948005a03f2c1845ffbfff42212"}, + {file = "safetensors-0.4.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d52c958dc210265157573f81d34adf54e255bc2b59ded6218500c9b15a750eb"}, + {file = "safetensors-0.4.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f9568f380f513a60139971169c4a358b8731509cc19112369902eddb33faa4d"}, + {file = "safetensors-0.4.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0d9cd8e1560dfc514b6d7859247dc6a86ad2f83151a62c577428d5102d872721"}, + {file = "safetensors-0.4.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:89f9f17b0dacb913ed87d57afbc8aad85ea42c1085bd5de2f20d83d13e9fc4b2"}, + {file = "safetensors-0.4.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1139eb436fd201c133d03c81209d39ac57e129f5e74e34bb9ab60f8d9b726270"}, + {file = "safetensors-0.4.3-cp38-none-win32.whl", hash = "sha256:d9c289f140a9ae4853fc2236a2ffc9a9f2d5eae0cb673167e0f1b8c18c0961ac"}, + {file = "safetensors-0.4.3-cp38-none-win_amd64.whl", hash = "sha256:622afd28968ef3e9786562d352659a37de4481a4070f4ebac883f98c5836563e"}, + {file = "safetensors-0.4.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8651c7299cbd8b4161a36cd6a322fa07d39cd23535b144d02f1c1972d0c62f3c"}, + {file = "safetensors-0.4.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e375d975159ac534c7161269de24ddcd490df2157b55c1a6eeace6cbb56903f0"}, + {file = "safetensors-0.4.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:084fc436e317f83f7071fc6a62ca1c513b2103db325cd09952914b50f51cf78f"}, + {file = "safetensors-0.4.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:41a727a7f5e6ad9f1db6951adee21bbdadc632363d79dc434876369a17de6ad6"}, + {file = "safetensors-0.4.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e7dbbde64b6c534548696808a0e01276d28ea5773bc9a2dfb97a88cd3dffe3df"}, + {file = "safetensors-0.4.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bbae3b4b9d997971431c346edbfe6e41e98424a097860ee872721e176040a893"}, + {file = "safetensors-0.4.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01e4b22e3284cd866edeabe4f4d896229495da457229408d2e1e4810c5187121"}, + {file = "safetensors-0.4.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dd37306546b58d3043eb044c8103a02792cc024b51d1dd16bd3dd1f334cb3ed"}, + {file = "safetensors-0.4.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d8815b5e1dac85fc534a97fd339e12404db557878c090f90442247e87c8aeaea"}, + {file = "safetensors-0.4.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e011cc162503c19f4b1fd63dfcddf73739c7a243a17dac09b78e57a00983ab35"}, + {file = "safetensors-0.4.3-cp39-none-win32.whl", hash = "sha256:01feb3089e5932d7e662eda77c3ecc389f97c0883c4a12b5cfdc32b589a811c3"}, + {file = "safetensors-0.4.3-cp39-none-win_amd64.whl", hash = "sha256:3f9cdca09052f585e62328c1c2923c70f46814715c795be65f0b93f57ec98a02"}, + {file = "safetensors-0.4.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1b89381517891a7bb7d1405d828b2bf5d75528299f8231e9346b8eba092227f9"}, + {file = "safetensors-0.4.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:cd6fff9e56df398abc5866b19a32124815b656613c1c5ec0f9350906fd798aac"}, + {file = "safetensors-0.4.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:840caf38d86aa7014fe37ade5d0d84e23dcfbc798b8078015831996ecbc206a3"}, + {file = "safetensors-0.4.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9650713b2cfa9537a2baf7dd9fee458b24a0aaaa6cafcea8bdd5fb2b8efdc34"}, + {file = "safetensors-0.4.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e4119532cd10dba04b423e0f86aecb96cfa5a602238c0aa012f70c3a40c44b50"}, + {file = "safetensors-0.4.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e066e8861eef6387b7c772344d1fe1f9a72800e04ee9a54239d460c400c72aab"}, + {file = "safetensors-0.4.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:90964917f5b0fa0fa07e9a051fbef100250c04d150b7026ccbf87a34a54012e0"}, + {file = "safetensors-0.4.3-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c41e1893d1206aa7054029681778d9a58b3529d4c807002c156d58426c225173"}, + {file = "safetensors-0.4.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae7613a119a71a497d012ccc83775c308b9c1dab454806291427f84397d852fd"}, + {file = "safetensors-0.4.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9bac020faba7f5dc481e881b14b6425265feabb5bfc552551d21189c0eddc3"}, + {file = "safetensors-0.4.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:420a98f593ff9930f5822560d14c395ccbc57342ddff3b463bc0b3d6b1951550"}, + {file = "safetensors-0.4.3-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f5e6883af9a68c0028f70a4c19d5a6ab6238a379be36ad300a22318316c00cb0"}, + {file = "safetensors-0.4.3-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:cdd0a3b5da66e7f377474599814dbf5cbf135ff059cc73694de129b58a5e8a2c"}, + {file = "safetensors-0.4.3-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9bfb92f82574d9e58401d79c70c716985dc049b635fef6eecbb024c79b2c46ad"}, + {file = "safetensors-0.4.3-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:3615a96dd2dcc30eb66d82bc76cda2565f4f7bfa89fcb0e31ba3cea8a1a9ecbb"}, + {file = "safetensors-0.4.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:868ad1b6fc41209ab6bd12f63923e8baeb1a086814cb2e81a65ed3d497e0cf8f"}, + {file = "safetensors-0.4.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7ffba80aa49bd09195145a7fd233a7781173b422eeb995096f2b30591639517"}, + {file = "safetensors-0.4.3-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0acbe31340ab150423347e5b9cc595867d814244ac14218932a5cf1dd38eb39"}, + {file = "safetensors-0.4.3-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19bbdf95de2cf64f25cd614c5236c8b06eb2cfa47cbf64311f4b5d80224623a3"}, + {file = "safetensors-0.4.3-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b852e47eb08475c2c1bd8131207b405793bfc20d6f45aff893d3baaad449ed14"}, + {file = "safetensors-0.4.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5d07cbca5b99babb692d76d8151bec46f461f8ad8daafbfd96b2fca40cadae65"}, + {file = "safetensors-0.4.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1ab6527a20586d94291c96e00a668fa03f86189b8a9defa2cdd34a1a01acc7d5"}, + {file = "safetensors-0.4.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02318f01e332cc23ffb4f6716e05a492c5f18b1d13e343c49265149396284a44"}, + {file = "safetensors-0.4.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec4b52ce9a396260eb9731eb6aea41a7320de22ed73a1042c2230af0212758ce"}, + {file = "safetensors-0.4.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:018b691383026a2436a22b648873ed11444a364324e7088b99cd2503dd828400"}, + {file = "safetensors-0.4.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:309b10dbcab63269ecbf0e2ca10ce59223bb756ca5d431ce9c9eeabd446569da"}, + {file = "safetensors-0.4.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b277482120df46e27a58082df06a15aebda4481e30a1c21eefd0921ae7e03f65"}, + {file = "safetensors-0.4.3.tar.gz", hash = "sha256:2f85fc50c4e07a21e95c24e07460fe6f7e2859d0ce88092838352b798ce711c2"}, +] + +[package.extras] +all = ["safetensors[jax]", "safetensors[numpy]", "safetensors[paddlepaddle]", "safetensors[pinned-tf]", "safetensors[quality]", "safetensors[testing]", "safetensors[torch]"] +dev = ["safetensors[all]"] +jax = ["flax (>=0.6.3)", "jax (>=0.3.25)", "jaxlib (>=0.3.25)", "safetensors[numpy]"] +mlx = ["mlx (>=0.0.9)"] +numpy = ["numpy (>=1.21.6)"] +paddlepaddle = ["paddlepaddle (>=2.4.1)", "safetensors[numpy]"] +pinned-tf = ["safetensors[numpy]", "tensorflow (==2.11.0)"] +quality = ["black (==22.3)", "click (==8.0.4)", "flake8 (>=3.8.3)", "isort (>=5.5.4)"] +tensorflow = ["safetensors[numpy]", "tensorflow (>=2.11.0)"] +testing = ["h5py (>=3.7.0)", "huggingface-hub (>=0.12.1)", "hypothesis (>=6.70.2)", "pytest (>=7.2.0)", "pytest-benchmark (>=4.0.0)", "safetensors[numpy]", "setuptools-rust (>=1.5.2)"] +torch = ["safetensors[numpy]", "torch (>=1.10)"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "sympy" +version = "1.13.1" +description = "Computer algebra system (CAS) in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "sympy-1.13.1-py3-none-any.whl", hash = "sha256:db36cdc64bf61b9b24578b6f7bab1ecdd2452cf008f34faa33776680c26d66f8"}, + {file = "sympy-1.13.1.tar.gz", hash = "sha256:9cebf7e04ff162015ce31c9c6c9144daa34a93bd082f54fd8f12deca4f47515f"}, +] + +[package.dependencies] +mpmath = ">=1.1.0,<1.4" + +[package.extras] +dev = ["hypothesis (>=6.70.0)", "pytest (>=7.1.0)"] + +[[package]] +name = "tabulate" +version = "0.9.0" +description = "Pretty-print tabular data" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, + {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, +] + +[package.extras] +widechars = ["wcwidth"] + +[[package]] +name = "tbb" +version = "2021.13.0" +description = "Intel® oneAPI Threading Building Blocks (oneTBB)" +optional = false +python-versions = "*" +files = [ + {file = "tbb-2021.13.0-py2.py3-none-manylinux1_i686.whl", hash = "sha256:a2567725329639519d46d92a2634cf61e76601dac2f777a05686fea546c4fe4f"}, + {file = "tbb-2021.13.0-py2.py3-none-manylinux1_x86_64.whl", hash = "sha256:aaf667e92849adb012b8874d6393282afc318aca4407fc62f912ee30a22da46a"}, + {file = "tbb-2021.13.0-py3-none-win32.whl", hash = "sha256:6669d26703e9943f6164c6407bd4a237a45007e79b8d3832fe6999576eaaa9ef"}, + {file = "tbb-2021.13.0-py3-none-win_amd64.whl", hash = "sha256:3528a53e4bbe64b07a6112b4c5a00ff3c61924ee46c9c68e004a1ac7ad1f09c3"}, +] + +[[package]] +name = "tenacity" +version = "8.5.0" +description = "Retry code until it succeeds" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tenacity-8.5.0-py3-none-any.whl", hash = "sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687"}, + {file = "tenacity-8.5.0.tar.gz", hash = "sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78"}, +] + +[package.extras] +doc = ["reno", "sphinx"] +test = ["pytest", "tornado (>=4.5)", "typeguard"] + +[[package]] +name = "termcolor" +version = "2.4.0" +description = "ANSI color formatting for output in terminal" +optional = false +python-versions = ">=3.8" +files = [ + {file = "termcolor-2.4.0-py3-none-any.whl", hash = "sha256:9297c0df9c99445c2412e832e882a7884038a25617c60cea2ad69488d4040d63"}, + {file = "termcolor-2.4.0.tar.gz", hash = "sha256:aab9e56047c8ac41ed798fa36d892a37aca6b3e9159f3e0c24bc64a9b3ac7b7a"}, +] + +[package.extras] +tests = ["pytest", "pytest-cov"] + +[[package]] +name = "textsearch" +version = "0.0.24" +description = "Find strings/words in text; convenience and C speed" +optional = false +python-versions = "*" +files = [ + {file = "textsearch-0.0.24-py2.py3-none-any.whl", hash = "sha256:1bbc4cc36300fbf0bbaa865500f84e907c85f6a48faf37da6e098407b405ed09"}, + {file = "textsearch-0.0.24.tar.gz", hash = "sha256:2d23b5c3116715b65bccc18bc870ecc236ec8480d48cd5f257cc60bf66bb241a"}, +] + +[package.dependencies] +anyascii = "*" +pyahocorasick = "*" + +[[package]] +name = "tokenizers" +version = "0.19.1" +description = "" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tokenizers-0.19.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:952078130b3d101e05ecfc7fc3640282d74ed26bcf691400f872563fca15ac97"}, + {file = "tokenizers-0.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:82c8b8063de6c0468f08e82c4e198763e7b97aabfe573fd4cf7b33930ca4df77"}, + {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f03727225feaf340ceeb7e00604825addef622d551cbd46b7b775ac834c1e1c4"}, + {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:453e4422efdfc9c6b6bf2eae00d5e323f263fff62b29a8c9cd526c5003f3f642"}, + {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:02e81bf089ebf0e7f4df34fa0207519f07e66d8491d963618252f2e0729e0b46"}, + {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b07c538ba956843833fee1190cf769c60dc62e1cf934ed50d77d5502194d63b1"}, + {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28cab1582e0eec38b1f38c1c1fb2e56bce5dc180acb1724574fc5f47da2a4fe"}, + {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b01afb7193d47439f091cd8f070a1ced347ad0f9144952a30a41836902fe09e"}, + {file = "tokenizers-0.19.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7fb297edec6c6841ab2e4e8f357209519188e4a59b557ea4fafcf4691d1b4c98"}, + {file = "tokenizers-0.19.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2e8a3dd055e515df7054378dc9d6fa8c8c34e1f32777fb9a01fea81496b3f9d3"}, + {file = "tokenizers-0.19.1-cp310-none-win32.whl", hash = "sha256:7ff898780a155ea053f5d934925f3902be2ed1f4d916461e1a93019cc7250837"}, + {file = "tokenizers-0.19.1-cp310-none-win_amd64.whl", hash = "sha256:bea6f9947e9419c2fda21ae6c32871e3d398cba549b93f4a65a2d369662d9403"}, + {file = "tokenizers-0.19.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5c88d1481f1882c2e53e6bb06491e474e420d9ac7bdff172610c4f9ad3898059"}, + {file = "tokenizers-0.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddf672ed719b4ed82b51499100f5417d7d9f6fb05a65e232249268f35de5ed14"}, + {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:dadc509cc8a9fe460bd274c0e16ac4184d0958117cf026e0ea8b32b438171594"}, + {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfedf31824ca4915b511b03441784ff640378191918264268e6923da48104acc"}, + {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac11016d0a04aa6487b1513a3a36e7bee7eec0e5d30057c9c0408067345c48d2"}, + {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76951121890fea8330d3a0df9a954b3f2a37e3ec20e5b0530e9a0044ca2e11fe"}, + {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b342d2ce8fc8d00f376af068e3274e2e8649562e3bc6ae4a67784ded6b99428d"}, + {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d16ff18907f4909dca9b076b9c2d899114dd6abceeb074eca0c93e2353f943aa"}, + {file = "tokenizers-0.19.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:706a37cc5332f85f26efbe2bdc9ef8a9b372b77e4645331a405073e4b3a8c1c6"}, + {file = "tokenizers-0.19.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:16baac68651701364b0289979ecec728546133e8e8fe38f66fe48ad07996b88b"}, + {file = "tokenizers-0.19.1-cp311-none-win32.whl", hash = "sha256:9ed240c56b4403e22b9584ee37d87b8bfa14865134e3e1c3fb4b2c42fafd3256"}, + {file = "tokenizers-0.19.1-cp311-none-win_amd64.whl", hash = "sha256:ad57d59341710b94a7d9dbea13f5c1e7d76fd8d9bcd944a7a6ab0b0da6e0cc66"}, + {file = "tokenizers-0.19.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:621d670e1b1c281a1c9698ed89451395d318802ff88d1fc1accff0867a06f153"}, + {file = "tokenizers-0.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d924204a3dbe50b75630bd16f821ebda6a5f729928df30f582fb5aade90c818a"}, + {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4f3fefdc0446b1a1e6d81cd4c07088ac015665d2e812f6dbba4a06267d1a2c95"}, + {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9620b78e0b2d52ef07b0d428323fb34e8ea1219c5eac98c2596311f20f1f9266"}, + {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04ce49e82d100594715ac1b2ce87d1a36e61891a91de774755f743babcd0dd52"}, + {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5c2ff13d157afe413bf7e25789879dd463e5a4abfb529a2d8f8473d8042e28f"}, + {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3174c76efd9d08f836bfccaca7cfec3f4d1c0a4cf3acbc7236ad577cc423c840"}, + {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c9d5b6c0e7a1e979bec10ff960fae925e947aab95619a6fdb4c1d8ff3708ce3"}, + {file = "tokenizers-0.19.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a179856d1caee06577220ebcfa332af046d576fb73454b8f4d4b0ba8324423ea"}, + {file = "tokenizers-0.19.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:952b80dac1a6492170f8c2429bd11fcaa14377e097d12a1dbe0ef2fb2241e16c"}, + {file = "tokenizers-0.19.1-cp312-none-win32.whl", hash = "sha256:01d62812454c188306755c94755465505836fd616f75067abcae529c35edeb57"}, + {file = "tokenizers-0.19.1-cp312-none-win_amd64.whl", hash = "sha256:b70bfbe3a82d3e3fb2a5e9b22a39f8d1740c96c68b6ace0086b39074f08ab89a"}, + {file = "tokenizers-0.19.1-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:bb9dfe7dae85bc6119d705a76dc068c062b8b575abe3595e3c6276480e67e3f1"}, + {file = "tokenizers-0.19.1-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:1f0360cbea28ea99944ac089c00de7b2e3e1c58f479fb8613b6d8d511ce98267"}, + {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:71e3ec71f0e78780851fef28c2a9babe20270404c921b756d7c532d280349214"}, + {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b82931fa619dbad979c0ee8e54dd5278acc418209cc897e42fac041f5366d626"}, + {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e8ff5b90eabdcdaa19af697885f70fe0b714ce16709cf43d4952f1f85299e73a"}, + {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e742d76ad84acbdb1a8e4694f915fe59ff6edc381c97d6dfdd054954e3478ad4"}, + {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d8c5d59d7b59885eab559d5bc082b2985555a54cda04dda4c65528d90ad252ad"}, + {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b2da5c32ed869bebd990c9420df49813709e953674c0722ff471a116d97b22d"}, + {file = "tokenizers-0.19.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:638e43936cc8b2cbb9f9d8dde0fe5e7e30766a3318d2342999ae27f68fdc9bd6"}, + {file = "tokenizers-0.19.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:78e769eb3b2c79687d9cb0f89ef77223e8e279b75c0a968e637ca7043a84463f"}, + {file = "tokenizers-0.19.1-cp37-none-win32.whl", hash = "sha256:72791f9bb1ca78e3ae525d4782e85272c63faaef9940d92142aa3eb79f3407a3"}, + {file = "tokenizers-0.19.1-cp37-none-win_amd64.whl", hash = "sha256:f3bbb7a0c5fcb692950b041ae11067ac54826204318922da754f908d95619fbc"}, + {file = "tokenizers-0.19.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:07f9295349bbbcedae8cefdbcfa7f686aa420be8aca5d4f7d1ae6016c128c0c5"}, + {file = "tokenizers-0.19.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:10a707cc6c4b6b183ec5dbfc5c34f3064e18cf62b4a938cb41699e33a99e03c1"}, + {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6309271f57b397aa0aff0cbbe632ca9d70430839ca3178bf0f06f825924eca22"}, + {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ad23d37d68cf00d54af184586d79b84075ada495e7c5c0f601f051b162112dc"}, + {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:427c4f0f3df9109314d4f75b8d1f65d9477033e67ffaec4bca53293d3aca286d"}, + {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e83a31c9cf181a0a3ef0abad2b5f6b43399faf5da7e696196ddd110d332519ee"}, + {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c27b99889bd58b7e301468c0838c5ed75e60c66df0d4db80c08f43462f82e0d3"}, + {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bac0b0eb952412b0b196ca7a40e7dce4ed6f6926489313414010f2e6b9ec2adf"}, + {file = "tokenizers-0.19.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8a6298bde623725ca31c9035a04bf2ef63208d266acd2bed8c2cb7d2b7d53ce6"}, + {file = "tokenizers-0.19.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:08a44864e42fa6d7d76d7be4bec62c9982f6f6248b4aa42f7302aa01e0abfd26"}, + {file = "tokenizers-0.19.1-cp38-none-win32.whl", hash = "sha256:1de5bc8652252d9357a666e609cb1453d4f8e160eb1fb2830ee369dd658e8975"}, + {file = "tokenizers-0.19.1-cp38-none-win_amd64.whl", hash = "sha256:0bcce02bf1ad9882345b34d5bd25ed4949a480cf0e656bbd468f4d8986f7a3f1"}, + {file = "tokenizers-0.19.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:0b9394bd204842a2a1fd37fe29935353742be4a3460b6ccbaefa93f58a8df43d"}, + {file = "tokenizers-0.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4692ab92f91b87769d950ca14dbb61f8a9ef36a62f94bad6c82cc84a51f76f6a"}, + {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6258c2ef6f06259f70a682491c78561d492e885adeaf9f64f5389f78aa49a051"}, + {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c85cf76561fbd01e0d9ea2d1cbe711a65400092bc52b5242b16cfd22e51f0c58"}, + {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:670b802d4d82bbbb832ddb0d41df7015b3e549714c0e77f9bed3e74d42400fbe"}, + {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:85aa3ab4b03d5e99fdd31660872249df5e855334b6c333e0bc13032ff4469c4a"}, + {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbf001afbbed111a79ca47d75941e9e5361297a87d186cbfc11ed45e30b5daba"}, + {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4c89aa46c269e4e70c4d4f9d6bc644fcc39bb409cb2a81227923404dd6f5227"}, + {file = "tokenizers-0.19.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:39c1ec76ea1027438fafe16ecb0fb84795e62e9d643444c1090179e63808c69d"}, + {file = "tokenizers-0.19.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c2a0d47a89b48d7daa241e004e71fb5a50533718897a4cd6235cb846d511a478"}, + {file = "tokenizers-0.19.1-cp39-none-win32.whl", hash = "sha256:61b7fe8886f2e104d4caf9218b157b106207e0f2a4905c9c7ac98890688aabeb"}, + {file = "tokenizers-0.19.1-cp39-none-win_amd64.whl", hash = "sha256:f97660f6c43efd3e0bfd3f2e3e5615bf215680bad6ee3d469df6454b8c6e8256"}, + {file = "tokenizers-0.19.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3b11853f17b54c2fe47742c56d8a33bf49ce31caf531e87ac0d7d13d327c9334"}, + {file = "tokenizers-0.19.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d26194ef6c13302f446d39972aaa36a1dda6450bc8949f5eb4c27f51191375bd"}, + {file = "tokenizers-0.19.1-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e8d1ed93beda54bbd6131a2cb363a576eac746d5c26ba5b7556bc6f964425594"}, + {file = "tokenizers-0.19.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca407133536f19bdec44b3da117ef0d12e43f6d4b56ac4c765f37eca501c7bda"}, + {file = "tokenizers-0.19.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce05fde79d2bc2e46ac08aacbc142bead21614d937aac950be88dc79f9db9022"}, + {file = "tokenizers-0.19.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:35583cd46d16f07c054efd18b5d46af4a2f070a2dd0a47914e66f3ff5efb2b1e"}, + {file = "tokenizers-0.19.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:43350270bfc16b06ad3f6f07eab21f089adb835544417afda0f83256a8bf8b75"}, + {file = "tokenizers-0.19.1-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b4399b59d1af5645bcee2072a463318114c39b8547437a7c2d6a186a1b5a0e2d"}, + {file = "tokenizers-0.19.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6852c5b2a853b8b0ddc5993cd4f33bfffdca4fcc5d52f89dd4b8eada99379285"}, + {file = "tokenizers-0.19.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcd266ae85c3d39df2f7e7d0e07f6c41a55e9a3123bb11f854412952deacd828"}, + {file = "tokenizers-0.19.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecb2651956eea2aa0a2d099434134b1b68f1c31f9a5084d6d53f08ed43d45ff2"}, + {file = "tokenizers-0.19.1-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:b279ab506ec4445166ac476fb4d3cc383accde1ea152998509a94d82547c8e2a"}, + {file = "tokenizers-0.19.1-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:89183e55fb86e61d848ff83753f64cded119f5d6e1f553d14ffee3700d0a4a49"}, + {file = "tokenizers-0.19.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2edbc75744235eea94d595a8b70fe279dd42f3296f76d5a86dde1d46e35f574"}, + {file = "tokenizers-0.19.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:0e64bfde9a723274e9a71630c3e9494ed7b4c0f76a1faacf7fe294cd26f7ae7c"}, + {file = "tokenizers-0.19.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0b5ca92bfa717759c052e345770792d02d1f43b06f9e790ca0a1db62838816f3"}, + {file = "tokenizers-0.19.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f8a20266e695ec9d7a946a019c1d5ca4eddb6613d4f466888eee04f16eedb85"}, + {file = "tokenizers-0.19.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63c38f45d8f2a2ec0f3a20073cccb335b9f99f73b3c69483cd52ebc75369d8a1"}, + {file = "tokenizers-0.19.1-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:dd26e3afe8a7b61422df3176e06664503d3f5973b94f45d5c45987e1cb711876"}, + {file = "tokenizers-0.19.1-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:eddd5783a4a6309ce23432353cdb36220e25cbb779bfa9122320666508b44b88"}, + {file = "tokenizers-0.19.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:56ae39d4036b753994476a1b935584071093b55c7a72e3b8288e68c313ca26e7"}, + {file = "tokenizers-0.19.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f9939ca7e58c2758c01b40324a59c034ce0cebad18e0d4563a9b1beab3018243"}, + {file = "tokenizers-0.19.1-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6c330c0eb815d212893c67a032e9dc1b38a803eccb32f3e8172c19cc69fbb439"}, + {file = "tokenizers-0.19.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec11802450a2487cdf0e634b750a04cbdc1c4d066b97d94ce7dd2cb51ebb325b"}, + {file = "tokenizers-0.19.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2b718f316b596f36e1dae097a7d5b91fc5b85e90bf08b01ff139bd8953b25af"}, + {file = "tokenizers-0.19.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ed69af290c2b65169f0ba9034d1dc39a5db9459b32f1dd8b5f3f32a3fcf06eab"}, + {file = "tokenizers-0.19.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f8a9c828277133af13f3859d1b6bf1c3cb6e9e1637df0e45312e6b7c2e622b1f"}, + {file = "tokenizers-0.19.1.tar.gz", hash = "sha256:ee59e6680ed0fdbe6b724cf38bd70400a0c1dd623b07ac729087270caeac88e3"}, +] + +[package.dependencies] +huggingface-hub = ">=0.16.4,<1.0" + +[package.extras] +dev = ["tokenizers[testing]"] +docs = ["setuptools-rust", "sphinx", "sphinx-rtd-theme"] +testing = ["black (==22.3)", "datasets", "numpy", "pytest", "requests", "ruff"] + +[[package]] +name = "tomlkit" +version = "0.13.0" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tomlkit-0.13.0-py3-none-any.whl", hash = "sha256:7075d3042d03b80f603482d69bf0c8f345c2b30e41699fd8883227f89972b264"}, + {file = "tomlkit-0.13.0.tar.gz", hash = "sha256:08ad192699734149f5b97b45f1f18dad7eb1b6d16bc72ad0c2335772650d7b72"}, +] + +[[package]] +name = "torch" +version = "2.3.1+cpu" +description = "Tensors and Dynamic neural networks in Python with strong GPU acceleration" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "torch-2.3.1+cpu-cp310-cp310-linux_x86_64.whl", hash = "sha256:d679e21d871982b9234444331a26350902cfd2d5ca44ce6f49896af8b3a3087d"}, + {file = "torch-2.3.1+cpu-cp310-cp310-win_amd64.whl", hash = "sha256:500bf790afc2fd374a15d06213242e517afccc50a46ea5955d321a9a68003335"}, + {file = "torch-2.3.1+cpu-cp311-cp311-linux_x86_64.whl", hash = "sha256:a272defe305dbd944aa28a91cc3db0f0149495b3ebec2e39723a7224fa05dc57"}, + {file = "torch-2.3.1+cpu-cp311-cp311-win_amd64.whl", hash = "sha256:d2965eb54d3c8818e2280a54bd53e8246a6bb34e4b10bd19c59f35b611dd9f05"}, + {file = "torch-2.3.1+cpu-cp312-cp312-linux_x86_64.whl", hash = "sha256:2141a6cb7021adf2f92a0fd372cfeac524ba460bd39ce3a641d30a561e41f69a"}, + {file = "torch-2.3.1+cpu-cp312-cp312-win_amd64.whl", hash = "sha256:6acdca2530462611095c44fd95af75ecd5b9646eac813452fe0adf31a9bc310a"}, + {file = "torch-2.3.1+cpu-cp38-cp38-linux_x86_64.whl", hash = "sha256:cab92d5101e6db686c5525e04d87cedbcf3a556073d71d07fbe7d1ce09630ffb"}, + {file = "torch-2.3.1+cpu-cp38-cp38-win_amd64.whl", hash = "sha256:dbc784569a367fd425158cf4ae82057dd3011185ba5fc68440432ba0562cb5b2"}, + {file = "torch-2.3.1+cpu-cp39-cp39-linux_x86_64.whl", hash = "sha256:a3cb8e61ba311cee1bb7463cbdcf3ebdfd071e2091e74c5785e3687eb02819f9"}, + {file = "torch-2.3.1+cpu-cp39-cp39-win_amd64.whl", hash = "sha256:df68668056e62c0332e03f43d9da5d4278b39df1ba58d30ec20d34242070955d"}, +] + +[package.dependencies] +filelock = "*" +fsspec = "*" +jinja2 = "*" +mkl = {version = ">=2021.1.1,<=2021.4.0", markers = "platform_system == \"Windows\""} +networkx = "*" +sympy = "*" +typing-extensions = ">=4.8.0" + +[package.extras] +opt-einsum = ["opt-einsum (>=3.3)"] +optree = ["optree (>=0.9.1)"] + +[package.source] +type = "legacy" +url = "https://download.pytorch.org/whl/cpu" +reference = "pytorch-cpu" + +[[package]] +name = "tqdm" +version = "4.66.4" +description = "Fast, Extensible Progress Meter" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tqdm-4.66.4-py3-none-any.whl", hash = "sha256:b75ca56b413b030bc3f00af51fd2c1a1a5eac6a0c1cca83cbb37a5c52abce644"}, + {file = "tqdm-4.66.4.tar.gz", hash = "sha256:e4d936c9de8727928f3be6079590e97d9abfe8d39a590be678eb5919ffc186bb"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] +notebook = ["ipywidgets (>=6)"] +slack = ["slack-sdk"] +telegram = ["requests"] + +[[package]] +name = "transformers" +version = "4.43.3" +description = "State-of-the-art Machine Learning for JAX, PyTorch and TensorFlow" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "transformers-4.43.3-py3-none-any.whl", hash = "sha256:6552beada5d826c25ff9b79139d237ab9050c6ea96b73d7fd2f8a8ba23ee76a4"}, + {file = "transformers-4.43.3.tar.gz", hash = "sha256:820c5b192bb1bf47250802901a8f0bf581e06b8fded89179d4ef08a1e903ee1c"}, +] + +[package.dependencies] +filelock = "*" +huggingface-hub = ">=0.23.2,<1.0" +numpy = ">=1.17" +packaging = ">=20.0" +pyyaml = ">=5.1" +regex = "!=2019.12.17" +requests = "*" +safetensors = ">=0.4.1" +tokenizers = ">=0.19,<0.20" +tqdm = ">=4.27" + +[package.extras] +accelerate = ["accelerate (>=0.21.0)"] +agents = ["Pillow (>=10.0.1,<=15.0)", "accelerate (>=0.21.0)", "datasets (!=2.5.0)", "diffusers", "opencv-python", "sentencepiece (>=0.1.91,!=0.1.92)", "torch"] +all = ["Pillow (>=10.0.1,<=15.0)", "accelerate (>=0.21.0)", "av (==9.2.0)", "codecarbon (==1.2.0)", "decord (==0.6.0)", "flax (>=0.4.1,<=0.7.0)", "jax (>=0.4.1,<=0.4.13)", "jaxlib (>=0.4.1,<=0.4.13)", "kenlm", "keras-nlp (>=0.3.1,<0.14.0)", "librosa", "onnxconverter-common", "optax (>=0.0.8,<=0.1.4)", "optuna", "phonemizer", "protobuf", "pyctcdecode (>=0.4.0)", "ray[tune] (>=2.7.0)", "scipy (<1.13.0)", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "tensorflow (>2.9,<2.16)", "tensorflow-text (<2.16)", "tf2onnx", "timm (<=0.9.16)", "tokenizers (>=0.19,<0.20)", "torch", "torchaudio", "torchvision"] +audio = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)"] +benchmark = ["optimum-benchmark (>=0.2.0)"] +codecarbon = ["codecarbon (==1.2.0)"] +deepspeed = ["accelerate (>=0.21.0)", "deepspeed (>=0.9.3)"] +deepspeed-testing = ["GitPython (<3.1.19)", "accelerate (>=0.21.0)", "beautifulsoup4", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "deepspeed (>=0.9.3)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "nltk", "optuna", "parameterized", "protobuf", "psutil", "pydantic", "pytest (>=7.2.0,<8.0.0)", "pytest-rich", "pytest-timeout", "pytest-xdist", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.4.4)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "sentencepiece (>=0.1.91,!=0.1.92)", "tensorboard", "timeout-decorator"] +dev = ["GitPython (<3.1.19)", "Pillow (>=10.0.1,<=15.0)", "accelerate (>=0.21.0)", "av (==9.2.0)", "beautifulsoup4", "codecarbon (==1.2.0)", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "decord (==0.6.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "flax (>=0.4.1,<=0.7.0)", "fugashi (>=1.0)", "ipadic (>=1.0.0,<2.0)", "isort (>=5.5.4)", "jax (>=0.4.1,<=0.4.13)", "jaxlib (>=0.4.1,<=0.4.13)", "kenlm", "keras-nlp (>=0.3.1,<0.14.0)", "librosa", "nltk", "onnxconverter-common", "optax (>=0.0.8,<=0.1.4)", "optuna", "parameterized", "phonemizer", "protobuf", "psutil", "pyctcdecode (>=0.4.0)", "pydantic", "pytest (>=7.2.0,<8.0.0)", "pytest-rich", "pytest-timeout", "pytest-xdist", "ray[tune] (>=2.7.0)", "rhoknp (>=1.1.0,<1.3.1)", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.4.4)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "scikit-learn", "scipy (<1.13.0)", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "sudachidict-core (>=20220729)", "sudachipy (>=0.6.6)", "tensorboard", "tensorflow (>2.9,<2.16)", "tensorflow-text (<2.16)", "tf2onnx", "timeout-decorator", "timm (<=0.9.16)", "tokenizers (>=0.19,<0.20)", "torch", "torchaudio", "torchvision", "unidic (>=1.0.2)", "unidic-lite (>=1.0.7)", "urllib3 (<2.0.0)"] +dev-tensorflow = ["GitPython (<3.1.19)", "Pillow (>=10.0.1,<=15.0)", "beautifulsoup4", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "isort (>=5.5.4)", "kenlm", "keras-nlp (>=0.3.1,<0.14.0)", "librosa", "nltk", "onnxconverter-common", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "parameterized", "phonemizer", "protobuf", "psutil", "pyctcdecode (>=0.4.0)", "pydantic", "pytest (>=7.2.0,<8.0.0)", "pytest-rich", "pytest-timeout", "pytest-xdist", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.4.4)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "scikit-learn", "sentencepiece (>=0.1.91,!=0.1.92)", "tensorboard", "tensorflow (>2.9,<2.16)", "tensorflow-text (<2.16)", "tf2onnx", "timeout-decorator", "tokenizers (>=0.19,<0.20)", "urllib3 (<2.0.0)"] +dev-torch = ["GitPython (<3.1.19)", "Pillow (>=10.0.1,<=15.0)", "accelerate (>=0.21.0)", "beautifulsoup4", "codecarbon (==1.2.0)", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "fugashi (>=1.0)", "ipadic (>=1.0.0,<2.0)", "isort (>=5.5.4)", "kenlm", "librosa", "nltk", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "optuna", "parameterized", "phonemizer", "protobuf", "psutil", "pyctcdecode (>=0.4.0)", "pydantic", "pytest (>=7.2.0,<8.0.0)", "pytest-rich", "pytest-timeout", "pytest-xdist", "ray[tune] (>=2.7.0)", "rhoknp (>=1.1.0,<1.3.1)", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.4.4)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "scikit-learn", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "sudachidict-core (>=20220729)", "sudachipy (>=0.6.6)", "tensorboard", "timeout-decorator", "timm (<=0.9.16)", "tokenizers (>=0.19,<0.20)", "torch", "torchaudio", "torchvision", "unidic (>=1.0.2)", "unidic-lite (>=1.0.7)", "urllib3 (<2.0.0)"] +flax = ["flax (>=0.4.1,<=0.7.0)", "jax (>=0.4.1,<=0.4.13)", "jaxlib (>=0.4.1,<=0.4.13)", "optax (>=0.0.8,<=0.1.4)", "scipy (<1.13.0)"] +flax-speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)"] +ftfy = ["ftfy"] +integrations = ["optuna", "ray[tune] (>=2.7.0)", "sigopt"] +ja = ["fugashi (>=1.0)", "ipadic (>=1.0.0,<2.0)", "rhoknp (>=1.1.0,<1.3.1)", "sudachidict-core (>=20220729)", "sudachipy (>=0.6.6)", "unidic (>=1.0.2)", "unidic-lite (>=1.0.7)"] +modelcreation = ["cookiecutter (==1.7.3)"] +natten = ["natten (>=0.14.6,<0.15.0)"] +onnx = ["onnxconverter-common", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "tf2onnx"] +onnxruntime = ["onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)"] +optuna = ["optuna"] +quality = ["GitPython (<3.1.19)", "datasets (!=2.5.0)", "isort (>=5.5.4)", "ruff (==0.4.4)", "urllib3 (<2.0.0)"] +ray = ["ray[tune] (>=2.7.0)"] +retrieval = ["datasets (!=2.5.0)", "faiss-cpu"] +ruff = ["ruff (==0.4.4)"] +sagemaker = ["sagemaker (>=2.31.0)"] +sentencepiece = ["protobuf", "sentencepiece (>=0.1.91,!=0.1.92)"] +serving = ["fastapi", "pydantic", "starlette", "uvicorn"] +sigopt = ["sigopt"] +sklearn = ["scikit-learn"] +speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)", "torchaudio"] +testing = ["GitPython (<3.1.19)", "beautifulsoup4", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "nltk", "parameterized", "psutil", "pydantic", "pytest (>=7.2.0,<8.0.0)", "pytest-rich", "pytest-timeout", "pytest-xdist", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.4.4)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "sentencepiece (>=0.1.91,!=0.1.92)", "tensorboard", "timeout-decorator"] +tf = ["keras-nlp (>=0.3.1,<0.14.0)", "onnxconverter-common", "tensorflow (>2.9,<2.16)", "tensorflow-text (<2.16)", "tf2onnx"] +tf-cpu = ["keras (>2.9,<2.16)", "keras-nlp (>=0.3.1,<0.14.0)", "onnxconverter-common", "tensorflow-cpu (>2.9,<2.16)", "tensorflow-probability (<0.24)", "tensorflow-text (<2.16)", "tf2onnx"] +tf-speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)"] +timm = ["timm (<=0.9.16)"] +tokenizers = ["tokenizers (>=0.19,<0.20)"] +torch = ["accelerate (>=0.21.0)", "torch"] +torch-speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)", "torchaudio"] +torch-vision = ["Pillow (>=10.0.1,<=15.0)", "torchvision"] +torchhub = ["filelock", "huggingface-hub (>=0.23.2,<1.0)", "importlib-metadata", "numpy (>=1.17)", "packaging (>=20.0)", "protobuf", "regex (!=2019.12.17)", "requests", "sentencepiece (>=0.1.91,!=0.1.92)", "tokenizers (>=0.19,<0.20)", "torch", "tqdm (>=4.27)"] +video = ["av (==9.2.0)", "decord (==0.6.0)"] +vision = ["Pillow (>=10.0.1,<=15.0)"] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "unidecode" +version = "1.3.8" +description = "ASCII transliterations of Unicode text" +optional = false +python-versions = ">=3.5" +files = [ + {file = "Unidecode-1.3.8-py3-none-any.whl", hash = "sha256:d130a61ce6696f8148a3bd8fe779c99adeb4b870584eeb9526584e9aa091fd39"}, + {file = "Unidecode-1.3.8.tar.gz", hash = "sha256:cfdb349d46ed3873ece4586b96aa75258726e2fa8ec21d6f00a591d98806c2f4"}, +] + +[[package]] +name = "urllib3" +version = "2.2.2" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "verspec" +version = "0.1.0" +description = "Flexible version handling" +optional = false +python-versions = "*" +files = [ + {file = "verspec-0.1.0-py3-none-any.whl", hash = "sha256:741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31"}, + {file = "verspec-0.1.0.tar.gz", hash = "sha256:c4504ca697b2056cdb4bfa7121461f5a0e81809255b41c03dda4ba823637c01e"}, +] + +[package.extras] +test = ["coverage", "flake8 (>=3.7)", "mypy", "pretend", "pytest"] + +[[package]] +name = "virtualenv" +version = "20.26.3" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, + {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[[package]] +name = "watchdog" +version = "4.0.1" +description = "Filesystem events monitoring" +optional = false +python-versions = ">=3.8" +files = [ + {file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:da2dfdaa8006eb6a71051795856bedd97e5b03e57da96f98e375682c48850645"}, + {file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e93f451f2dfa433d97765ca2634628b789b49ba8b504fdde5837cdcf25fdb53b"}, + {file = "watchdog-4.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ef0107bbb6a55f5be727cfc2ef945d5676b97bffb8425650dadbb184be9f9a2b"}, + {file = "watchdog-4.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:17e32f147d8bf9657e0922c0940bcde863b894cd871dbb694beb6704cfbd2fb5"}, + {file = "watchdog-4.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03e70d2df2258fb6cb0e95bbdbe06c16e608af94a3ffbd2b90c3f1e83eb10767"}, + {file = "watchdog-4.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:123587af84260c991dc5f62a6e7ef3d1c57dfddc99faacee508c71d287248459"}, + {file = "watchdog-4.0.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:093b23e6906a8b97051191a4a0c73a77ecc958121d42346274c6af6520dec175"}, + {file = "watchdog-4.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:611be3904f9843f0529c35a3ff3fd617449463cb4b73b1633950b3d97fa4bfb7"}, + {file = "watchdog-4.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:62c613ad689ddcb11707f030e722fa929f322ef7e4f18f5335d2b73c61a85c28"}, + {file = "watchdog-4.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d4925e4bf7b9bddd1c3de13c9b8a2cdb89a468f640e66fbfabaf735bd85b3e35"}, + {file = "watchdog-4.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cad0bbd66cd59fc474b4a4376bc5ac3fc698723510cbb64091c2a793b18654db"}, + {file = "watchdog-4.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a3c2c317a8fb53e5b3d25790553796105501a235343f5d2bf23bb8649c2c8709"}, + {file = "watchdog-4.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c9904904b6564d4ee8a1ed820db76185a3c96e05560c776c79a6ce5ab71888ba"}, + {file = "watchdog-4.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:667f3c579e813fcbad1b784db7a1aaa96524bed53437e119f6a2f5de4db04235"}, + {file = "watchdog-4.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d10a681c9a1d5a77e75c48a3b8e1a9f2ae2928eda463e8d33660437705659682"}, + {file = "watchdog-4.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0144c0ea9997b92615af1d94afc0c217e07ce2c14912c7b1a5731776329fcfc7"}, + {file = "watchdog-4.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:998d2be6976a0ee3a81fb8e2777900c28641fb5bfbd0c84717d89bca0addcdc5"}, + {file = "watchdog-4.0.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e7921319fe4430b11278d924ef66d4daa469fafb1da679a2e48c935fa27af193"}, + {file = "watchdog-4.0.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:f0de0f284248ab40188f23380b03b59126d1479cd59940f2a34f8852db710625"}, + {file = "watchdog-4.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bca36be5707e81b9e6ce3208d92d95540d4ca244c006b61511753583c81c70dd"}, + {file = "watchdog-4.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ab998f567ebdf6b1da7dc1e5accfaa7c6992244629c0fdaef062f43249bd8dee"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:dddba7ca1c807045323b6af4ff80f5ddc4d654c8bce8317dde1bd96b128ed253"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_armv7l.whl", hash = "sha256:4513ec234c68b14d4161440e07f995f231be21a09329051e67a2118a7a612d2d"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_i686.whl", hash = "sha256:4107ac5ab936a63952dea2a46a734a23230aa2f6f9db1291bf171dac3ebd53c6"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_ppc64.whl", hash = "sha256:6e8c70d2cd745daec2a08734d9f63092b793ad97612470a0ee4cbb8f5f705c57"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:f27279d060e2ab24c0aa98363ff906d2386aa6c4dc2f1a374655d4e02a6c5e5e"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:f8affdf3c0f0466e69f5b3917cdd042f89c8c63aebdb9f7c078996f607cdb0f5"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ac7041b385f04c047fcc2951dc001671dee1b7e0615cde772e84b01fbf68ee84"}, + {file = "watchdog-4.0.1-py3-none-win32.whl", hash = "sha256:206afc3d964f9a233e6ad34618ec60b9837d0582b500b63687e34011e15bb429"}, + {file = "watchdog-4.0.1-py3-none-win_amd64.whl", hash = "sha256:7577b3c43e5909623149f76b099ac49a1a01ca4e167d1785c76eb52fa585745a"}, + {file = "watchdog-4.0.1-py3-none-win_ia64.whl", hash = "sha256:d7b9f5f3299e8dd230880b6c55504a1f69cf1e4316275d1b215ebdd8187ec88d"}, + {file = "watchdog-4.0.1.tar.gz", hash = "sha256:eebaacf674fa25511e8867028d281e602ee6500045b57f43b08778082f7f8b44"}, +] + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + +[[package]] +name = "wheel" +version = "0.43.0" +description = "A built-package format for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "wheel-0.43.0-py3-none-any.whl", hash = "sha256:55c570405f142630c6b9f72fe09d9b67cf1477fcf543ae5b8dcb1f5b7377da81"}, + {file = "wheel-0.43.0.tar.gz", hash = "sha256:465ef92c69fa5c5da2d1cf8ac40559a8c940886afcef87dcf14b9470862f1d85"}, +] + +[package.extras] +test = ["pytest (>=6.0.0)", "setuptools (>=65)"] + +[[package]] +name = "yarl" +version = "1.9.4" +description = "Yet another URL library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"}, + {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"}, + {file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"}, + {file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"}, + {file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"}, + {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"}, + {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"}, + {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"}, + {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"}, + {file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"}, + {file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"}, + {file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"}, + {file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"}, + {file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"}, + {file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"}, + {file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"}, + {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"}, + {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" + +[[package]] +name = "zipp" +version = "3.19.2" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"}, + {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"}, +] + +[package.extras] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[metadata] +lock-version = "2.0" +python-versions = "~3.12" +content-hash = "9806c9e24ea140d62a53762b540a7f9e79ef4cf087e99fa97c78616771411acf" diff --git a/courageous-comets/pyproject.toml b/courageous-comets/pyproject.toml new file mode 100644 index 0000000..58b81b7 --- /dev/null +++ b/courageous-comets/pyproject.toml @@ -0,0 +1,113 @@ +[tool.poetry] +name = "courageous-comets" +version = "1.0.0" +authors = ["Courageous Comets"] +description = "" +readme = "docs/README.md" +license = "MIT" + +[tool.poetry.dependencies] +python = "~3.12" +asyncache = "0.3.1" +cachetools = "5.4.0" +coloredlogs = "15.0.1" +contractions = "0.1.73" +discord-py = "2.4.0" +jishaku = "2.5.2" +matplotlib = "3.9.1" +nltk = "3.8.1" +pydantic = "2.8.2" +pynacl = "1.5.0" +python-dotenv = "1.0.1" +pyyaml = "6.0.1" +redis = { extras = ["hiredis"], version = "5.0.7" } +redisvl = { extras = ["hiredis"], version = "0.2.3" } +torch = { version = "~2.3.1+cpu", source = "pytorch-cpu" } +transformers = "4.43.3" +unidecode = "1.3.8" + +[tool.poetry.dev-dependencies] +commitizen = "3.27.0" +mike = "2.1.2" +mkdocs = "1.6.0" +mkdocs-material = "9.5.29" +pre-commit = "3.7.1" +pymdown-extensions = "10.8.1" +pyright = "^1.1.373" +pytest = "8.2.2" +pytest-asyncio = "0.23.7" +pytest-mock = "3.14.0" +pytest-sugar = "1.0.0" +ruff = "0.5.2" + +[[tool.poetry.source]] +name = "pytorch-cpu" +url = "https://download.pytorch.org/whl/cpu" +priority = "explicit" + + +[tool.poetry.group.dev.dependencies] +faker = "^26.0.0" + +[tool.pyright] +typeCheckingMode = "basic" +pythonVersion = "3.12" +reportUnnecessaryTypeIgnoreComment = "error" + +[tool.pytest.ini_options] +asyncio_mode = "auto" +# Ignore warnings from dependencies +filterwarnings = [ + "ignore:'audioop' is deprecated and slated for removal in Python 3.13", + "ignore:invalid escape sequence *", + "ignore:`resume_download` is deprecated and will be removed *", + "ignore:datetime.datetime.utcnow", +] +markers = ["num_messages(num=10): create num messages."] + +[tool.ruff] +line-length = 100 +target-version = "py312" + +[tool.ruff.lint] +ignore = [ + # Self and cls do not require annotations. + "ANN101", + "ANN102", + # Module level docstrings don't always make sense + "D104", + # Pyright error codes are obnoxiously long. Ignore lint telling you to use them. + "PGH003", + # Documenting every file is a bit redundant for us. + "D100", + # No point in documenting magic methods. + "D105", + # Makes more sense to use `Parameters` in the main class instead of in the `__init__` . + "D107", + # We do not need cryptographically secure random functions. + "S311", + # Just let us use TODOs minimally!! + "TD003", + "FIX002", +] +select = ["ALL"] + +[tool.ruff.lint.per-file-ignores] +"**/test__*.py" = ["S101", "PLR2004"] +"examples/**/*.py" = ["INP001"] + +[tool.ruff.lint.pydocstyle] +convention = "numpy" + +[tool.commitizen] +changelog_file = "docs/CHANGELOG.md" +name = "cz_conventional_commits" +tag_format = "v$version" +version_scheme = "semver2" +version_provider = "poetry" +update_changelog_on_bump = true +major_version_zero = true + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/courageous-comets/secrets/.gitignore b/courageous-comets/secrets/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/courageous-comets/secrets/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/courageous-comets/tests/__init__.py b/courageous-comets/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/courageous-comets/tests/conftest.py b/courageous-comets/tests/conftest.py new file mode 100644 index 0000000..d2a50d5 --- /dev/null +++ b/courageous-comets/tests/conftest.py @@ -0,0 +1,56 @@ +from pathlib import Path + +import pytest +import yaml + +from courageous_comets import settings +from courageous_comets.nltk import init_nltk +from courageous_comets.redis.schema import MESSAGE_SCHEMA +from courageous_comets.transformers import init_transformers +from courageous_comets.vectorizer import Vectorizer + + +@pytest.fixture(scope="session") +def application_config() -> dict: + """Load the application configuration for testing.""" + with Path("application.yaml").open("r") as file: + return yaml.safe_load(file) + + +@pytest.fixture(scope="session", autouse=True) +async def _load_nltk_data(application_config: dict) -> None: + """Load the NLTK data for testing.""" + resources = application_config.get("nltk", []) + await init_nltk(resources) + + +@pytest.fixture(scope="session", autouse=True) +async def _load_transformers(application_config: dict) -> None: + """Load the transformers for testing.""" + transformers = application_config.get("transformers", []) + await init_transformers(transformers) + + +@pytest.fixture(autouse=True) +def _patch_redis_keys_prefix(monkeypatch: pytest.MonkeyPatch) -> None: + """Set the REDIS_KEYS_PREFIX for testing. + + The patch applies the following steps: + + - Append _test to the courageous_comets.settings.REDIS_KEYS_PREFIX + - Update the names of Redis indexes using courageous_comets.settings.REDIS_KEYS_PREFIX + + """ + settings.REDIS_KEYS_PREFIX = settings.REDIS_KEYS_PREFIX + "_test" + # MonkeyPatch the name of the Redis index for testing + monkeypatch.setitem( + MESSAGE_SCHEMA["index"], + "prefix", + f"{settings.REDIS_KEYS_PREFIX}:messages", + ) + + +@pytest.fixture(scope="session") +def vectorizer() -> Vectorizer: + """Set up the vectorizer for encoding messages.""" + return Vectorizer() diff --git a/courageous-comets/tests/courageous_comets/__init__.py b/courageous-comets/tests/courageous_comets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/courageous-comets/tests/courageous_comets/test__client.py b/courageous-comets/tests/courageous_comets/test__client.py new file mode 100644 index 0000000..b223eb5 --- /dev/null +++ b/courageous-comets/tests/courageous_comets/test__client.py @@ -0,0 +1,224 @@ +from typing import Self + +import discord +import pytest +from discord.ext import commands +from pytest_mock import MockerFixture, MockType + +from courageous_comets.client import CourageousCometsBot, intents, logger, sync + + +class MockAsyncContextManager: + """Mock an asynchronous context manager.""" + + async def __aenter__(self, *args, **kwargs) -> Self: # noqa: ANN002, ANN003 + return self + + async def __aexit__(self, *args, **kwargs) -> None: # noqa: ANN002, ANN003 + pass + + +@pytest.fixture() +def mock_context(mocker: MockerFixture) -> MockType: + """Create a mock context for testing.""" + ctx = mocker.MagicMock(spec=commands.Context) + ctx.bot = mocker.MagicMock(spec=commands.Bot) + ctx.bot.tree = mocker.MagicMock(spec=discord.app_commands.CommandTree) + ctx.bot.tree.sync = mocker.AsyncMock(return_value=[mocker.Mock()]) + ctx.typing = mocker.MagicMock(MockAsyncContextManager()) + return ctx + + +@pytest.fixture() +def bot() -> CourageousCometsBot: + """Create a CourageousCometsBot instance for testing.""" + return CourageousCometsBot() + + +def test__bot_has_required_intents(bot: CourageousCometsBot) -> None: + """ + Test whether the bot has the required intents. + + Asserts + ------- + - The bot has the required intents. + """ + assert bot.intents == intents + + +async def test__on_ready_logs_message(bot: CourageousCometsBot, mocker: MockerFixture) -> None: + """ + Test whether the on_ready function logs the expected message. + + Asserts + ------- + - The logger.info function is called with the expected message. + """ + logger_info = mocker.spy(logger, "info") + await bot.on_ready() + logger_info.assert_called_with("Logged in as %s", mocker.ANY) + + +async def test__load_cogs_loads_all_cogs(bot: CourageousCometsBot, mocker: MockerFixture) -> None: + """ + Test whether the load_cogs function loads all cogs from the config file. + + Asserts + ------- + - The bot.load_extension function is called for each cog in the config file. + """ + cogs = ["cog1", "cog2", "cog3"] + + load_extension = mocker.patch("discord.ext.commands.Bot.load_extension") + + await bot.load_cogs(cogs) + + for cog in cogs: + load_extension.assert_any_call(cog) + + +async def test__load_cogs_logs_loaded_cogs( + bot: CourageousCometsBot, + mocker: MockerFixture, +) -> None: + """ + Test whether the load_cogs function logs the correct message for each cog. + + Asserts + ------- + - The bot.load_extension function is awaited for each cog in the config file. + - The logger.debug function is called with the correct message for each cog. + """ + cogs = ["cog1", "cog2", "cog3"] + + load_extension = mocker.patch("discord.ext.commands.Bot.load_extension", return_value=None) + logger_debug = mocker.spy(logger, "debug") + + await bot.load_cogs(cogs) + + for cog in cogs: + load_extension.assert_any_await(cog) + logger_debug.assert_any_call("Loaded cog %s", cog) + + +async def test__load_cogs_logs_exception_on_extension_error( + bot: CourageousCometsBot, + mocker: MockerFixture, +) -> None: + """ + Test whether the load_cogs function logs an exception when an ExtensionError is raised. + + Asserts + ------- + - The bot.load_extension function is awaited for each cog in the config file. + - The logger.exception function is called when an ExtensionError is raised. + """ + cogs = ["cog1"] + + expected = commands.ExtensionError(name="cog1") + load_extension = mocker.patch("discord.ext.commands.Bot.load_extension", side_effect=expected) + logger_exception = mocker.spy(logger, "exception") + + await bot.load_cogs(cogs) + + load_extension.assert_awaited_with("cog1") + logger_exception.assert_called_with("Failed to load cog %s", "cog1", exc_info=expected) + + +async def test__sync_syncs_to_current_guild(mock_context: MockType) -> None: + """ + Test whether the sync command syncs to the current guild. + + Asserts + ------- + - The bot.sync function is awaited with the expected parameters. + - The ctx.send function is awaited with the expected message. + """ + await sync(mock_context, [], "~") # type: ignore + + mock_context.bot.tree.sync.assert_awaited_with(guild=mock_context.guild) + mock_context.send.assert_awaited_with( + f"Synced {len(mock_context.bot.tree.sync.return_value)} command(s) to the current guild.", + ) + + +async def test__sync_syncs_to_global_scope(mock_context: MockType) -> None: + """ + Test whether the sync command syncs to the global scope. + + Asserts + ------- + - The bot.sync function is awaited with the expected parameters. + - The ctx.send function is awaited with the expected message + """ + await sync(mock_context, [], "*") # type: ignore + + mock_context.bot.tree.sync.assert_awaited_with() + mock_context.send.assert_awaited_with( + f"Synced {len(mock_context.bot.tree.sync.return_value)} command(s) globally.", + ) + + +async def test__sync_removes_non_global_commands(mock_context: MockType) -> None: + """ + Test whether the sync command removes non-global commands. + + Asserts + ------- + - The bot.tree.clear_commands function is called with the expected parameters. + - The bot.sync function is awaited with the expected parameters + - The ctx.send function is awaited with the expected message + """ + await sync(mock_context, [], "^") # type: ignore + + mock_context.bot.tree.clear_commands.assert_called_with(guild=mock_context.guild) + mock_context.bot.tree.sync.assert_awaited_with(guild=mock_context.guild) + mock_context.send.assert_awaited_with("Synced 0 command(s) to the current guild.") + + +async def test__sync_syncs_to_given_guilds( + mocker: MockerFixture, + mock_context: MockType, +) -> None: + """ + Test whether the sync command syncs to the given guilds. + + Asserts + ------- + - The bot.sync function is awaited with the expected parameters. + - The ctx.send function is awaited with the expected message. + """ + guilds = [mocker.Mock(), mocker.Mock()] + + await sync(mock_context, guilds) # type: ignore + + for guild in guilds: + mock_context.bot.tree.sync.assert_any_await(guild=guild) + + mock_context.send.assert_awaited_with(f"Synced the tree to {len(guilds)}/{len(guilds)}.") + + +async def test__sync_logs_exception_on_http_exception( + mocker: MockerFixture, + mock_context: MockType, +) -> None: + """ + Test whether the sync command handles an HTTPException correctly. + + Asserts + ------- + - The logger.exception function is called when an HTTPException is raised. + - The ctx.send function is awaited with the expected message. + """ + guilds = [mocker.Mock(), mocker.Mock()] + expected = discord.HTTPException(response=mocker.Mock(), message="Failed to sync") + mock_context.bot.tree.sync.side_effect = expected + + logger_exception = mocker.spy(logger, "exception") + + await sync(mock_context, guilds) # type: ignore + + for guild in guilds: + logger_exception.assert_any_call("Failed to sync to guild %s", guild, exc_info=expected) + + mock_context.send.assert_awaited_with(f"Synced the tree to 0/{len(guilds)}.") diff --git a/courageous-comets/tests/courageous_comets/test__preprocessing.py b/courageous-comets/tests/courageous_comets/test__preprocessing.py new file mode 100644 index 0000000..4c71ab1 --- /dev/null +++ b/courageous-comets/tests/courageous_comets/test__preprocessing.py @@ -0,0 +1,129 @@ +import pytest + +from courageous_comets.preprocessing import ( + drop_extra_whitespace, + drop_links, + drop_punctuation, + drop_very_long_words, +) + + +@pytest.mark.parametrize( + ("text", "expected"), + [ + ("Hello, world!", "Hello, world!"), + ("Hello, world!", "Hello, world!"), + ("Hello, world!", "Hello, world!"), + ("Hello, world! ", "Hello, world!"), + (" Hello, world! ", "Hello, world!"), + (" Hello, world! ", "Hello, world!"), + (" Hello, world! ", "Hello, world!"), + ], +) +def test__drop_extra_whitespace(text: str, expected: str) -> None: + """ + Test whether `drop_extra_whitespace` removes extra whitespace from the given text. + + Asserts + ------- + - Extra whitespace is removed from the given text. + - Text without extra whitespace is not modified. + """ + assert drop_extra_whitespace(text) == expected + + +@pytest.mark.parametrize( + ("text", "expected"), + [ + ("Hello, world!", "Hello, world!"), + ("Hello, http://world.com!", "Hello, "), + ("Hello, https://world.com! How are you?", "Hello, How are you?"), + ], +) +def test__drop_links(text: str, expected: str) -> None: + """ + Test whether `drop_links` removes links from the given text. + + Asserts + ------- + - Links are removed from the given text. + - Text without links is not modified. + """ + assert drop_links(text) == expected + + +@pytest.mark.parametrize( + ("text", "expected"), + [ + ( + "Hello, world!", + "Hello world", + ), + ( + "Testing... 1, 2, 3.", + "Testing 1 2 3", + ), + ( + "No punctuation here", + "No punctuation here", + ), + ( + "Special characters: @#&*()", + "Special characters ", + ), + ( + "Mixed punctuation! How's it going?", + "Mixed punctuation Hows it going", + ), + ( + "End with punctuation.", + "End with punctuation", + ), + ( + "Multiple spaces and punctuation!!!", + "Multiple spaces and punctuation", + ), + ( + "Punctuation-in-the-middle.", + "Punctuationinthemiddle", + ), + ( + "12345!@#$%", + "12345", + ), + ( + "Quotes 'single' and \"double\"", + "Quotes single and double", + ), + ], +) +def test__drop_punctuation(text: str, expected: str) -> None: + """ + Test whether `drop_punctuation` removes punctuation from the given text. + + Asserts + ------- + - Punctuation is removed from the given text. + - Text without punctuation is not modified. + """ + assert drop_punctuation(text) == expected + + +@pytest.mark.parametrize( + ("text", "expected"), + [ + ("Hello, world!", "Hello, world!"), + ("Hello, verylongword!", "Hello, "), + ("Hello, verylongword! How are you?", "Hello, How are you?"), + ], +) +def test__drop_very_long_word(text: str, expected: str) -> None: + """ + Test whether `drop_very_long_words` removes very long words from the given text. + + Asserts + ------- + - Very long words are removed from the given text. + - Text without very long words is not modified. + """ + assert drop_very_long_words(text, max_length=10) == expected diff --git a/courageous-comets/tests/courageous_comets/test__scope.py b/courageous-comets/tests/courageous_comets/test__scope.py new file mode 100644 index 0000000..bf43417 --- /dev/null +++ b/courageous-comets/tests/courageous_comets/test__scope.py @@ -0,0 +1,23 @@ +import pytest + +from courageous_comets.enums import StatisticScope +from courageous_comets.redis.messages import build_search_scope + + +@pytest.mark.parametrize( + ("guild_id", "ids", "scope", "expected"), + [ + ("1", ["2", "3"], StatisticScope.GUILD, "@guild_id:{1}"), + ("1", None, StatisticScope.GUILD, "@guild_id:{1}"), + ("1", ["2", "3"], StatisticScope.CHANNEL, "(@guild_id:{1} @channel_id:{2|3})"), + ("1", ["2", "3"], StatisticScope.USER, "(@guild_id:{1} @user_id:{2|3})"), + ], +) +def test__build_search_scope( + guild_id: str, + ids: list[str] | None, + scope: StatisticScope, + expected: str, +) -> None: + """Tests whether the correct search scope is generated.""" + assert str(build_search_scope(guild_id, ids, scope)) == expected diff --git a/courageous-comets/tests/courageous_comets/test__sentiment.py b/courageous-comets/tests/courageous_comets/test__sentiment.py new file mode 100644 index 0000000..cf3e82e --- /dev/null +++ b/courageous-comets/tests/courageous_comets/test__sentiment.py @@ -0,0 +1,37 @@ +import pytest +from pytest_mock import MockerFixture +from redis.asyncio import Redis + +from courageous_comets.models import SentimentResult +from courageous_comets.sentiment import ( + calculate_sentiment, +) + + +@pytest.fixture() +def redis(mocker: MockerFixture) -> MockerFixture: + """Create a mock Redis instance for testing.""" + mock = mocker.AsyncMock(spec=Redis) + mock.hset = mocker.AsyncMock() + mock.hmget = mocker.AsyncMock() + return mock + + +def test__calculate_sentiment_analyzes_sentiment_of_given_text( + mocker: MockerFixture, +) -> None: + """ + Test whether the sentiment calculation analyzes the sentiment of the given text. + + Asserts + ------- + - The function produces sentiment scores for the given text. + """ + expected = SentimentResult.model_construct( + neg=mocker.ANY, + neu=mocker.ANY, + pos=mocker.ANY, + compound=mocker.ANY, + ) + result = calculate_sentiment("I love this product!") + assert result == expected diff --git a/courageous-comets/tests/courageous_comets/test__words.py b/courageous-comets/tests/courageous_comets/test__words.py new file mode 100644 index 0000000..a36267e --- /dev/null +++ b/courageous-comets/tests/courageous_comets/test__words.py @@ -0,0 +1,93 @@ +import pytest + +from courageous_comets.words import tokenize_sentence, word_frequency + + +@pytest.mark.parametrize( + ("sentence", "expected"), + [ + ( + "The quick brown fox jumps over the lazy dog", + ["quick", "brown", "fox", "jump", "lazi", "dog"], + ), + ( + "Hello, world!", + ["hello", "world"], + ), + ( + "I'm sorry, Dave. I'm afraid I can't do that.", + ["sorri", "dave", "afraid"], + ), + ( + "I've been waiting for you, Obi-Wan. We meet again at last.", + ["wait", "obi-wan", "meet", "last"], + ), + ( + "I don't like sand. It's coarse and rough and irritating and it gets everywhere.", + ["like", "sand", "coars", "rough", "irrit", "get", "everywher"], + ), + ( + "You were the chosen one! It was said that you would destroy the Sith, not join them.", + ["chosen", "one", "said", "would", "destroy", "sith", "join"], + ), + ( + "Bring balance to the Force, not leave it in darkness.", + ["bring", "balanc", "forc", "leav", "dark"], + ), + ( + "I'm gonna wreck it!", + ["go", "wreck"], + ), + ( + "I'm gonna wreck it! I'm gonna wreck it!", + ["go", "wreck", "go", "wreck"], + ), + ( + "", # Empty string + [], + ), + ], +) +def test__tokenize_sentence(sentence: str, expected: list[str]) -> None: + """ + Test whether a sentence is tokenized correctly. + + Asserts + ------- + - Contractions are expanded. + - The sentence is split into words. + - Punctuation is removed. + - The words are stemmed. + - Stopwords and words with a length of 1 are removed. + """ + result = tokenize_sentence(sentence) + assert result == expected + + +@pytest.mark.parametrize( + ("words", "expected"), + [ + ( + ["go", "wreck"], + {"go": 1, "wreck": 1}, + ), + ( + ["go", "wreck", "go", "wreck"], + {"go": 2, "wreck": 2}, + ), + ( + [], + {}, + ), + ], +) +def test__word_frequency(words: list[str], expected: dict[str, int]) -> None: + """ + Test whether the frequency of words is calculated correctly. + + Asserts + ------- + - The frequency of each word is counted. + """ + result = word_frequency(words) + assert result == expected diff --git a/courageous-comets/tests/integrations/__init__.py b/courageous-comets/tests/integrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/courageous-comets/tests/integrations/conftest.py b/courageous-comets/tests/integrations/conftest.py new file mode 100644 index 0000000..4edf44a --- /dev/null +++ b/courageous-comets/tests/integrations/conftest.py @@ -0,0 +1,94 @@ +from collections.abc import AsyncGenerator +from typing import cast + +import pytest +import pytest_asyncio +from faker import Faker +from redis.asyncio import Redis + +from courageous_comets import models, settings +from courageous_comets.client import CourageousCometsBot +from courageous_comets.sentiment import calculate_sentiment +from courageous_comets.vectorizer import Vectorizer +from courageous_comets.words import tokenize_sentence, word_frequency + + +@pytest.fixture() +async def bot() -> AsyncGenerator[CourageousCometsBot, None]: + """Fixture that sets up and tears down the CourageousCometsBot instance.""" + instance = CourageousCometsBot() + await instance.setup_hook() + yield instance + await instance.close() + + +@pytest.fixture() +async def redis(bot: CourageousCometsBot) -> AsyncGenerator[Redis, None]: + """Acquire a connection to the Redis database with teardown.""" + instance = cast(Redis, bot.redis) + + yield instance + + # Delete any keys that were created during the test + keys_to_delete = [key async for key in instance.scan_iter(f"{settings.REDIS_KEYS_PREFIX}:*")] + + if keys_to_delete: + await instance.delete(*keys_to_delete) + + await instance.aclose() + + +@pytest_asyncio.fixture +async def messages( + vectorizer: Vectorizer, + faker: Faker, + request: pytest.FixtureRequest, +) -> list[models.MessageAnalysis]: + """ + Generate random messages for testing. + + The number of messages to generate can be specified using the `num_messages` mark. + + Example + ------- + ```python + @pytest.mark.num_messages(100) + async def test__something(messages: list[models.MessageAnalysis]): + assert len(messages) == 100 + ``` + """ + # Retrieve the number of messages to generate from the test function + # If the mark is not present, or the number of messages is not specified, generate 10 messages + mark: pytest.Mark | None = request.node.get_closest_marker("num_messages") + num_messages = int(mark.args[0]) if mark and len(mark.args) > 0 else 10 + + # Message ID range + min_id = 1 + max_id = 1_0000_000 + + messages: list[models.MessageAnalysis] = [] + + for _ in range(num_messages): + # Generate a random message of 10 words + sentence = faker.sentence(nb_words=10) + + # Process the message + embedding = await vectorizer.aencode(sentence) + sentiment = calculate_sentiment(sentence) + tokens = tokenize_sentence(sentence) + + # Create a message object and append it to the list + messages.append( + models.MessageAnalysis( + guild_id="1", + channel_id=str(faker.random_int(min=min_id, max=5)), # 5 channels + message_id=str(faker.random_int(min=min_id, max=max_id)), + user_id=str(faker.random_int(min=min_id, max=5)), # 5 users + timestamp=faker.date_time(), + embedding=embedding, + sentiment=sentiment, + tokens=word_frequency(tokens), + ), + ) + + return messages diff --git a/courageous-comets/tests/integrations/test__messages.py b/courageous-comets/tests/integrations/test__messages.py new file mode 100644 index 0000000..31b6857 --- /dev/null +++ b/courageous-comets/tests/integrations/test__messages.py @@ -0,0 +1,43 @@ +import discord +import pytest +from pytest_mock import MockerFixture +from redis.asyncio import Redis + +from courageous_comets.client import CourageousCometsBot +from courageous_comets.cogs.messages import Messages +from courageous_comets.redis.keys import key_schema + + +@pytest.fixture() +def cog(bot: CourageousCometsBot) -> Messages: + """Return an instance of the Messages cog.""" + return Messages(bot) + + +async def test__messages_on_message__message_saved_to_redis( + cog: Messages, + redis: Redis, + mocker: MockerFixture, +) -> None: + """ + Test whether messages received by the cog are saved to Redis. + + Asserts + ------- + - The message key is present in Redis. + """ + message = mocker.MagicMock(spec=discord.Message) + + message.clean_content = "The quick brown fox jumps over the lazy dog." + message.id = 1 + message.author.id = 1 + message.author.bot = False + message.channel.id = 1 + message.guild.id = 1 + + await cog.on_message(message) + + key = key_schema.guild_messages(guild_id=1, message_id=1) + key_exists = await redis.exists(key) + + assert key_exists diff --git a/courageous-comets/tests/integrations/test__redis.py b/courageous-comets/tests/integrations/test__redis.py new file mode 100644 index 0000000..c8fb37e --- /dev/null +++ b/courageous-comets/tests/integrations/test__redis.py @@ -0,0 +1,199 @@ +import datetime + +import pytest +import pytest_asyncio +from redis.asyncio import Redis + +from courageous_comets import models +from courageous_comets.redis.keys import key_schema +from courageous_comets.redis.messages import ( + get_messages_by_semantics_similarity, + get_messages_by_sentiment_similarity, + get_recent_messages, + save_message, +) +from courageous_comets.sentiment import calculate_sentiment +from courageous_comets.vectorizer import Vectorizer +from courageous_comets.words import tokenize_sentence, word_frequency + + +@pytest.fixture(scope="session") +def content() -> str: + """Fixture that sets up the content of message. + + Returns + ------- + str + The message content. + """ + return "The quick brown fox jumps over the lazy dog." + + +@pytest.fixture(scope="session") +def sentiment(content: str) -> models.SentimentResult: + """Fixture that calculates the sentiment analysis of content. + + Parameters + ---------- + content: str + The text to run the sentiment analysis on. + + Returns + ------- + models.SentimentResult + The result of the sentiment analysis. + """ + return calculate_sentiment(content) + + +@pytest_asyncio.fixture(scope="session") +async def message( + content: str, + vectorizer: Vectorizer, + sentiment: models.SentimentResult, +) -> models.MessageAnalysis: + """Fixture that creates a message to be stored on Redis. + + Parameters + ---------- + content: str + The contents of the message. + vectorizer: courageous_comets.vectorizer.Vectorizer + The model that creates the embedding vector of content. + sentiment: models.SentimentResult + The result of the sentiment analysis of content. + + Returns + ------- + courageous_comets.models.MessageAnalysis + An analysis of the message contents. + """ + embedding = await vectorizer.aencode(content) + tokens = tokenize_sentence(content) + return models.MessageAnalysis( + message_id="1", + channel_id="1", + guild_id="1", + timestamp=datetime.datetime.fromtimestamp(0, datetime.UTC), + user_id="1", + embedding=embedding, + sentiment=sentiment, + tokens=word_frequency(tokens), + ) + + +async def test__save_message( + redis: Redis, + message: models.MessageAnalysis, +) -> None: + """ + Tests whether the save_mesage function stores the message on Redis. + + Parameters + ---------- + redis: redis.Redis + The Redis connection instance. + message: courageous_comets.models.MessageAnalysis + The Discord message with an embedding vector of its content. + + Asserts + ------- + - The returned key is the same as the one constructed by the key_schema + """ + key = await save_message(redis, message) + assert key == key_schema.guild_messages( + guild_id=int(message.guild_id), + message_id=int(message.message_id), + ) + + +async def test__get_messages_by_semantics_similarity( + redis: Redis, + message: models.MessageAnalysis, +) -> None: + """ + Tests that the same message is returned from a semantics similarity search. + + Parameters + ---------- + redis: redis.Redis + The Redis connection instance. + message: courageous_comets.models.MessageAnalysis + The message to save. + + Asserts + ------- + - The same message is returned using a semantics similarity search. + """ + await save_message(redis, message) + messages = await get_messages_by_semantics_similarity( + redis, + guild_id=message.guild_id, + embedding=message.embedding, + ) + assert len(messages) == 1 + assert messages[0].message_id == message.message_id + + +async def test__get_messages_by_sentiment_similarity( + redis: Redis, + message: models.MessageAnalysis, + sentiment: models.SentimentResult, +) -> None: + """ + Tests that the same message is returned from a sentiment similarity search. + + Parameters + ---------- + redis: redis.Redis + The Redis connection instance. + message: courageous_comets.models.MessageAnalysis + The message to save. + sentiment : courageous_comets.models.SentimentResult + The sentiment analayis result of message. + + Asserts + ------- + - The same message is returned using a sentiment similarity search. + """ + await save_message(redis, message) + messages = await get_messages_by_sentiment_similarity( + redis, + guild_id=message.guild_id, + sentiment=sentiment.compound, + radius=0.1, + ) + assert len(messages) == 1 + assert messages[0].message_id == message.message_id + + +@pytest.mark.parametrize(("limit", "expect"), [(10, 10), (150, 100)]) +@pytest.mark.num_messages(100) +async def test__get_recent_messages( + redis: Redis, + messages: list[models.MessageAnalysis], + limit: int, + expect: int, +) -> None: + """Tests that the expected number of messages are returned from Redis. + + Parameters + ---------- + redis: redis.Redis + The Redis connection instance. + messages list[courageous_comets.models.MessageAnalysis] + The messages to save + + Asserts + ------- + - The number of mesages returned does not exceed specified limit. + """ + # Save the messages to the database. + for message in messages: + await save_message(redis, message) + # All messages have the same guild_id + guild_id = messages[0].guild_id + + # Update its timestamp with the provided message_timestamp + db_messages = await get_recent_messages(redis, guild_id=guild_id, limit=limit) + assert len(db_messages) == expect