diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index b657c0fb..f028676a 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,8 +1,8 @@ # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.209.6/containers/python-3/.devcontainer/base.Dockerfile # [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster -ARG VARIANT="3.9" -FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} +ARG VARIANT="3.11" +FROM mcr.microsoft.com/devcontainers/python:1-${VARIANT} # [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 ARG NODE_VERSION="none" @@ -11,7 +11,7 @@ RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/ # Poetry ARG POETRY_VERSION="none" ENV POETRY_VIRTUALENVS_IN_PROJECT=true -RUN if [ "${POETRY_VERSION}" != "none" ]; then su vscode -c "umask 0002 && pip3 install poetry==${POETRY_VERSION}"; fi +RUN if [ "${POETRY_VERSION}" != "none" ]; then su vscode -c "umask 0002 && pip3 install poetry==${POETRY_VERSION} tox"; fi # [Optional] If your pip requirements rarely change, uncomment this section to add them to the image. # COPY requirements.txt /tmp/pip-tmp/ @@ -20,8 +20,8 @@ RUN if [ "${POETRY_VERSION}" != "none" ]; then su vscode -c "umask 0002 && pip3 # && rm -rf /tmp/pip-tmp # [Optional] Uncomment this section to install additional OS packages. -# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ -# && apt-get -y install --no-install-recommends +RUN apt-get update && apt-get upgrade -y \ + && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y gcc python3-dev # [Optional] Uncomment this line to install global node packages. # RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 7ef05733..983a8aad 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -9,10 +9,10 @@ // Update 'VARIANT' to pick a Python version: 3, 3.10, 3.9, 3.8, 3.7, 3.6 // Append -bullseye or -buster to pin to an OS version. // Use -bullseye variants on local on arm64/Apple Silicon. - "VARIANT": "3.10", + "VARIANT": "3.11", // Options // "NODE_VERSION": "lts/*", - "POETRY_VERSION": "1.5.1" + "POETRY_VERSION": "1.7.1" } }, // Set *default* container specific settings.json values on container create. @@ -22,7 +22,7 @@ "python.defaultInterpreterPath": "./.venv/bin/python", "python.terminal.activateEnvironment": true, "python.terminal.activateEnvInCurrentTerminal": true, - "editor.rulers": [79], + "editor.rulers": [130], "python.testing.pytestArgs": [ "tests" ], @@ -32,6 +32,7 @@ // Add the IDs of extensions you want installed when the container is created. "extensions": [ "ms-python.python", + "ms-toolsai.jupyter", "GitHub.vscode-pull-request-github" ] } @@ -41,7 +42,7 @@ 8000 ], // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "poetry install && poetry run pre-commit install --install-hooks", + "postCreateCommand": "make clean && make install", // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "vscode", "features": { diff --git a/.github/actions/setup-poetry-action/action.yml b/.github/actions/setup-poetry-action/action.yml new file mode 100644 index 00000000..0fcd6c16 --- /dev/null +++ b/.github/actions/setup-poetry-action/action.yml @@ -0,0 +1,55 @@ +name: Poetry Setup Action + +inputs: + poetry-version: + required: false + type: string + default: latest + python-version: + required: false + type: string + default: 3.11 + install-dependencies: + required: false + type: boolean + default: true + without: + required: false + type: string + +runs: + using: composite + steps: + - uses: actions/setup-python@v4 + with: + python-version: ${{ inputs.python-version }} + - if: ${{ inputs.poetry-version != 'latest' }} + shell: bash + run: pip install --upgrade pip setuptools wheel poetry==${{ inputs.poetry-version }} + - if: ${{ inputs.poetry-version == 'latest' }} + shell: bash + run: pip install --upgrade pip setuptools wheel poetry + - name: Configure Poetry + shell: bash + run: | + poetry config virtualenvs.create true + poetry config virtualenvs.in-project true + poetry config installer.max-workers 1 + - uses: actions/cache@v3 + if: inputs.without != '' && inputs.install-dependencies + with: + path: ./.venv + key: venv-without-${{ inputs.without }}-poetry-${{ inputs.poetry-version }}-python-${{ inputs.python-version }}-${{ hashFiles('poetry.lock') }}-${{ runner.os }} + - uses: actions/cache@v3 + if: inputs.without == '' && inputs.install-dependencies + with: + path: ./.venv + key: venv-poetry-${{ inputs.poetry-version }}-python-${{ inputs.python-version }}-${{ hashFiles('poetry.lock') }}-${{ runner.os }} + - name: Install dependencies + if: inputs.without != '' && inputs.install-dependencies + shell: bash + run: poetry install --no-interaction --without ${{ inputs.without }} + - name: Install dependencies + if: inputs.without == '' && inputs.install-dependencies + shell: bash + run: poetry install --no-interaction diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7fcd1fc0..692ec952 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,8 +9,8 @@ on: pull_request: env: - PYTHON_VERSION: '3.10' - POETRY_VERSION: 1.5.1 + PYTHON_VERSION: '3.11' + POETRY_VERSION: 1.7.1 jobs: docker-images: @@ -33,16 +33,6 @@ jobs: context: ./ steps: - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: ${{ env.PYTHON_VERSION }} - - name: Setup Poetry - run: | - pip install --upgrade pip setuptools wheel poetry==${{ env.POETRY_VERSION }} - poetry config virtualenvs.create true --local - poetry config virtualenvs.in-project true --local - - name: Export requirements.txt - run: poetry export -f requirements.txt --output requirements.txt - uses: docker/metadata-action@v4 id: meta with: @@ -93,20 +83,11 @@ jobs: tag: macos steps: - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: ./.github/actions/setup-poetry-action with: + poetry-version: ${{ env.POETRY_VERSION }} python-version: ${{ env.PYTHON_VERSION }} - - name: Setup Poetry - run: | - pip install --upgrade pip setuptools wheel poetry==${{ env.POETRY_VERSION }} - poetry config virtualenvs.create true --local - poetry config virtualenvs.in-project true --local - - uses: actions/cache@v3 - with: - path: ./.venv - key: venv-build-poetry-${{ env.POETRY_VERSION }}-python-${{ env.PYTHON_VERSION }}-${{ hashFiles('poetry.lock') }}-${{ runner.os }} - - name: Install dependencies - run: poetry install --without test + without: test - name: Run PyInstaller run: | poetry run pyinstaller scanner.spec @@ -127,7 +108,7 @@ jobs: - name: Upload archive uses: actions/upload-artifact@v3 with: - name: releases + name: releases-${{github.sha}} path: ./${{ steps.filename.outputs.FILENAME }} - name: Add archive to release uses: softprops/action-gh-release@v1 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9172edd5..5ea470a0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,7 +4,7 @@ on: pull_request: env: - POETRY_VERSION: 1.5.1 + POETRY_VERSION: 1.7.1 jobs: tests: @@ -14,30 +14,20 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python: ['3.9', '3.10', '3.11'] + python: ['3.9', '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: ./.github/actions/setup-poetry-action with: + poetry-version: ${{ env.POETRY_VERSION }} python-version: ${{ matrix.python }} - - name: Setup Poetry - run: | - pip install --upgrade pip setuptools wheel poetry==${{ env.POETRY_VERSION }} - poetry config virtualenvs.create true --local - poetry config virtualenvs.in-project true --local - poetry config installer.max-workers 1 --local - - uses: actions/cache@v3 - with: - path: ./.venv - key: venv-test-poetry-${{ env.POETRY_VERSION }}-python-${{ matrix.python }}-${{ hashFiles('poetry.lock') }}-${{ runner.os }} + without: build - uses: actions/cache@v3 with: path: ~/.cache/pre-commit key: pre-commit-python-${{ matrix.python }}-${{ hashFiles('poetry.lock', '.pre-commit-config.yaml') }}-${{ runner.os }} - - name: Install dependencies - run: poetry install --no-interaction --without build - name: Run linting run: poetry run pre-commit run -a - name: Run tests - run: poetry run pytest -v -m "not tgtg_api" --cov --cov-report=xml + run: poetry run pytest -v -m "not tgtg_api" --cov=tgtg_scanner --cov-report=xml - uses: codecov/codecov-action@v3 diff --git a/.github/workflows/tgtg.yml b/.github/workflows/tgtg.yml index ca9d805d..d5fabc82 100644 --- a/.github/workflows/tgtg.yml +++ b/.github/workflows/tgtg.yml @@ -5,8 +5,8 @@ on: - cron: 0 1 * * * env: - PYTHON_VERSION: '3.10' - POETRY_VERSION: 1.5.1 + PYTHON_VERSION: '3.11' + POETRY_VERSION: 1.7.1 jobs: test: @@ -14,20 +14,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: ./.github/actions/setup-poetry-action with: + poetry-version: ${{ env.POETRY_VERSION }} python-version: ${{ env.PYTHON_VERSION }} - - name: Setup Poetry - run: | - pip install --upgrade pip setuptools wheel poetry==${{ env.POETRY_VERSION }} - poetry config virtualenvs.create true --local - poetry config virtualenvs.in-project true --local - - uses: actions/cache@v3 - with: - path: ./.venv - key: venv-test-poetry-${{ env.POETRY_VERSION }}-python-${{ env.PYTHON_VERSION }}-${{ hashFiles('poetry.lock') }}-${{ runner.os }} - - name: Install dependencies - run: poetry install --without build + without: build - name: Run tests uses: nick-fields/retry@v2 with: diff --git a/.gitignore b/.gitignore index cd47c740..0520dc51 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,8 @@ venv .coverage coverage.xml .pytest_cache -requirements.txt +.mypy_cache +.tox .env config.ini diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fa1ee02d..c46bb753 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ exclude: (build|dist) repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: trailing-whitespace - id: check-added-large-files @@ -12,43 +12,52 @@ repos: - id: check-toml - id: check-added-large-files -- repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v2.0.2 +- repo: https://github.com/ambv/black + rev: 23.12.1 hooks: - - id: autopep8 - args: - - --in-place - - --max-line-length=79 + - id: black + args: [--line-length, '130', --target-version, py38] - repo: https://github.com/PyCQA/isort - rev: 5.12.0 + rev: 5.13.2 hooks: - id: isort + args: [--profile, black] - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 + rev: 6.1.0 hooks: - id: flake8 - args: - - --max-line-length=79 + args: [--max-line-length, '130'] - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks - rev: v2.9.0 + rev: v2.12.0 hooks: - id: pretty-format-toml args: [--autofix] exclude: poetry.lock - id: pretty-format-yaml args: [--autofix, --indent, '2'] + - id: pretty-format-ini + args: [--autofix] - repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.35.0 + rev: v0.38.0 hooks: - id: markdownlint args: [--fix] - repo: https://github.com/python-poetry/poetry - rev: 1.5.1 # add version here + rev: 1.7.0 hooks: - id: poetry-check - id: poetry-lock + args: [--no-update] + - id: poetry-export + args: [-f, requirements.txt, -o, requirements.txt] + +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.8.0 + hooks: + - id: mypy + additional_dependencies: [types-requests] diff --git a/Makefile b/Makefile index 18370624..de439660 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,27 @@ -images: - poetry export -f requirements.txt --output requirements.txt - docker build -f ./docker/Dockerfile -t tgtg-scanner:latest . - docker build -f ./docker/Dockerfile.alpine -t tgtg-scanner:latest-alpine . +clean: + rm -rf .venv + rm -rf .pytest_cache + rm -rf .tox + rm -rf .mypy_cache install: poetry install + poetry run pre-commit install --install-hooks + +server: + poetry run tgtg_server start: - poetry run scanner -d + poetry run scanner -d --base_url http://localhost:8080 + +test: + poetry run pytest -v -m "not tgtg_api" --cov=tgtg_scanner + +lint: + poetry run pre-commit run -a + +tox: + tox executable: rm -r ./build ||: @@ -16,8 +30,6 @@ executable: cp ./config.sample.ini ./dist/config.ini zip -j ./dist/scanner.zip ./dist/* -test: - poetry run pytest -v -m "not tgtg_api" --cov - -lint: - poetry run pre-commit run -a +images: + docker build -f ./docker/Dockerfile -t tgtg-scanner:latest . + docker build -f ./docker/Dockerfile.alpine -t tgtg-scanner:latest-alpine . diff --git a/README.md b/README.md index 570aa9ca..d82bfcda 100644 --- a/README.md +++ b/README.md @@ -77,10 +77,10 @@ My preferred method for servers, NAS, and RapsberryPis is using the pre-build mu [Docker Hub](https://hub.docker.com/r/derhenning/tgtg). The images are built for Linux on `amd64`, `arm64`, `armv7`, `armv6`, and `i386`. -1. Install Docker and docker-compose +1. Install Docker 2. Copy and edit `docker-compose.yml` as described in the [Wiki](https://github.com/Der-Henning/tgtg/wiki/Configuration) -3. Run `docker-compose up -d` +3. Run `docker compose up -d` The container automatically creates a volume mounting `\tokens` where the app saves the TGTG credentials after login. @@ -158,7 +158,7 @@ or `python -m tgtg_scanner --help` displays the available commands. ```txt -usage: scanner [-h] [-v] [-d] [-c config_file] [-l log_file] [-t | -f | -F | -a item_id [item_id ...] | -r item_id [item_id ...] | -R] [-j | -J] +usage: scanner [-h] [-v] [-d] [-c config_file] [-l log_file] [-t | -f | -F | -a item_id [item_id ...] | -r item_id [item_id ...] | -R] [-j | -J] [--base_url BASE_URL] Notifications for Too Good To Go @@ -180,6 +180,7 @@ options: -R, --remove_all remove all favorites and exit -j, --json output as plain json -J, --json_pretty output as pretty json + --base_url BASE_URL Overwrite TGTG API URL for testing ``` @@ -199,13 +200,13 @@ You can scrape the data with Prometheus to create and visualize historic data or Scrape config: ```xml - - job_name: 'TGTG' - scrape_interval: 1m - scheme: http - metrics_path: / - static_configs: - - targets: - - 'localhost:8000' +- job_name: 'TGTG' + scrape_interval: 1m + scheme: http + metrics_path: / + static_configs: + - targets: + - 'localhost:8000' ``` ## Development @@ -218,17 +219,27 @@ development container including all required dependencies. Alternatively, install all required development environment dependencies, including linting, testing, and building by executing ```bash -poetry install +make install +``` + +For developement and testing it is sometimes usefull to trigger TGTG Magic Bag events. +For this purpose you can run the TGTG dev API proxy server. +The proxy redirects all requests to the official TGTG API server. +The responses from the item endpoint are modified by randomizing the amount of available magic bags. + +```bash +make server ``` ### Makefile commands -- `make images` builds docker images with tag `tgtg-scanner:latest` and `tgtg-scanner:latest-alpine` -- `make install` installs development dependencies -- `make start` is short for `poetry run scanner -d` -- `make executable` creates a bundled executable in `/dist` +- `make install` installs development dependencies and pre-commit hooks +- `make server` starts TGTG dev API proxy server +- `make start` runs the scanner with debugging and using the API proxy - `make test` runs unit tests -- `make lint` run pre-commit hooks +- `make lint` run pre-commit hooks including linting and code checks +- `make executable` creates a bundled executable in `/dist` +- `make images` builds docker images with tag `tgtg-scanner:latest` and `tgtg-scanner:latest-alpine` ### Creating new notifiers diff --git a/config.sample.ini b/config.sample.ini index 235b56d9..fc01ac33 100644 --- a/config.sample.ini +++ b/config.sample.ini @@ -1,3 +1,9 @@ +## TGTG Scanner Configuration +## -------------------------- +## This is the configuration file for the TGTG Scanner. +## You can find more information about the configuration on the project page: +## https://github.com/Der-Henning/tgtg/wiki/Configuration + [MAIN] ## true for debug log messages ; Debug = true @@ -26,10 +32,10 @@ MetricsPort = 8000 ; DisableTests = true ## Disable all console outputs. only displays errors or Console notifier messages -; quiet = true +; Quiet = true ## Set localization for ${{pickupdate}} -locale = en_US +Locale = en_US ## Disable to not show activity spinner in console Activity = True @@ -51,18 +57,18 @@ Username = ## Address can be any address that can be found by Google Maps e.g. Strobelallee 50, 44139 Dortmund, Germany ## Calculations are made only with cached values once the cache is filled. ## Distance and time to the item location is cached when the avaiable item amount rises from 0 to something else. -## API requests are only made when the feature is enabled and the cache is empty. +## API requests are only made when the feature is Enabled and the cache is empty. ## Requests are only made for the modes of transportation used by the notifiers. ## Distance and time do not consider traffic and use the 'best' route according to Google Maps. ## Attributes to use in notifiers: walking_dt, driving_dt, biking_dt, transit_dt [LOCATION] -enabled = False -Google_Maps_API_Key = -Address = +Enabled = False +GoogleMapsAPIKey = +OriginAddress = #### Notifiers ## To enable notifier fill in the needed settings -## and set enabled to true +## and set Enabled to true ## Apprise allows you to send a notification to almost all of the most popular notification services ## Apprise URL example: twilio://AccountSid:AuthToken@FromPhoneNo/ToPhoneNo @@ -72,9 +78,9 @@ Address = ## Default Body: ## '${{display_name}} - new amount: ${{items_available}} - https://share.toogoodtogo.com/item/${{item_id}}' [Apprise] -enabled = false +Enabled = false URL = -; cron = +; Cron = ; Title = ; Body = @@ -82,16 +88,16 @@ URL = ## Simple notifier for the console output ## Message can be modified with the body property ## The body can use variables as described below -enabled = false -; body = -; cron = +Enabled = false +; Body = +; Cron = [SMTP] ## SMTP Settings / Example for gmail ## Subject and Body options are optional. ## Subject and Body options can use variables as described below ## The Body option is interpreted as HTML -enabled = false +Enabled = false Host = smtp.gmail.com Port = 587 Username = max.mustermann@gmail.com @@ -99,22 +105,22 @@ Password = TLS = true SSL = false Sender = max.mustermann@gmail.com -Recipient = max.mustermann@gmail.com -; cron = +Recipients = max.mustermann@gmail.com +; Cron = ; Subject = ; Body = [PUSHSAFER] -enabled = false +Enabled = false Key = DeviceID = -; cron = +; Cron = ## The IFTTT Body contains a json string which can use the variables described below for the webhook ## Default Body: ## {"value1": "${{display_name}}", "value2": ${{items_available}}, "value3": "https://share.toogoodtogo.com/item/${{item_id}}"} [IFTTT] -enabled = false +Enabled = false Event = Key = ; Body = @@ -125,18 +131,18 @@ Key = ## ${{display_name}} - New Amount: ${{items_available}} - https://share.toogoodtogo.com/item/${{item_id}} ## You can use variables on body, title, tags and click [NTFY] -enabled = false -server = https://ntfy.sh -topic = -; title = tgtg -; body = -; priority = default -; tags = tgtg -; click = https://share.toogoodtogo.com/item/${{item_id}} -; username = -; password = -; timeout = 60 -; cron = +Enabled = false +Server = https://ntfy.sh +Topic = +; Title = tgtg +; Body = +; Priority = default +; Tags = tgtg +; Click = https://share.toogoodtogo.com/item/${{item_id}} +; Username = +; Password = +; Timeout = 60 +; Cron = ## To use Telegram notifications you have to create a bot using the @botfather ## If you only provide the token of the bot will use the last chat it reseived a message on @@ -146,33 +152,33 @@ topic = ## Example: ## *${{display_name}}*\n*Available*: ${{items_available}}\n*Rating*: ${{rating}}\n*Price*: ${{price}} ${{currency}}\n*Pickup*: ${{pickupdate}}\n*Link*: https://share.toogoodtogo.com/item/${{item_id}} [TELEGRAM] -enabled = false -token = -chat_ids = -; timeout = 60 -; cron = -; body = +Enabled = false +Token = +ChatIDs = +; Timeout = 60 +; Cron = +; Body = ## WebHook URL and body can contain variables in the form of ${{variable}} ## Available variables: item_id, items_available, display_name, price, currency, pickupdate, description, favorite, rating, scanned_on ## json body example: {"value1": "${{display_name}}", "value2": ${{items_available}}} ## You have to provide "" for strings if needed [WEBHOOK] -enabled = false +Enabled = false URL = ; Method = POST -; body = -; type = text/plain -; headers = -; username = -; password = -; timeout = 60 -; cron = +; Body = +; Type = text/plain +; Headers = +; Username = +; Password = +; Timeout = 60 +; Cron = [SCRIPT] ## To run a script file ## Please make sure script file has execute rights ## Command example: /home/user/tgtg/script.sh -n ${{display_name}} -a ${{items_available}} -enabled = false -command = -; cron = +Enabled = false +Command = +; Cron = diff --git a/docker-compose.yml b/docker-compose.yml index a40269e4..56726f7a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,8 +2,11 @@ version: '3.3' services: scanner: - image: derhenning/tgtg:latest-alpine ## pre build image from docker hub - #image: tgtg-scanner:latest ## locally build image + image: derhenning/tgtg:latest-alpine ## pre build image from docker hub + # image: tgtg-scanner:latest ## locally build image + # build: ## build image with compose + # context: . + # dockerfile: docker/Dockerfile.alpine environment: # Configuration via environment variables. # Basic example using Telegram notifications diff --git a/docker/Dockerfile b/docker/Dockerfile index e324d57f..43a7c1cb 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,27 +1,28 @@ -FROM python:3.10-slim +FROM python:3.11-slim ENV PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 \ PIP_NO_CACHE_DIR=off \ PIP_DISABLE_PIP_VERSION_CHECK=on \ - PIP_NO_WARN_SCRIPT_LOCATION=0 + PIP_NO_WARN_SCRIPT_LOCATION=0 \ + PIP_ROOT_USER_ACTION=ignore ENV TGTG_TOKEN_PATH=/tokens ENV LOGS_PATH=/logs ENV DOCKER=true ENV UID=1000 ENV GID=1000 -RUN addgroup --gid $GID tgtg && \ +RUN addgroup --gid ${GID} tgtg && \ adduser --shell /bin/false \ --disabled-password \ - --uid $UID \ - --gid $GID \ + --uid ${UID} \ + --gid ${GID} \ tgtg -RUN mkdir -p /logs -RUN mkdir -p /tokens -RUN chown tgtg:tgtg /tokens -RUN chown tgtg:tgtg /logs -VOLUME /tokens +RUN mkdir -p ${LOGS_PATH} +RUN mkdir -p ${TGTG_TOKEN_PATH} +RUN chown tgtg:tgtg ${LOGS_PATH} +RUN chown tgtg:tgtg ${TGTG_TOKEN_PATH} +VOLUME ${TGTG_TOKEN_PATH} RUN --mount=type=bind,target=/context \ pip install -r /context/requirements.txt && \ diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index 28a5d55f..6ee66367 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -1,28 +1,29 @@ -FROM python:3.10-alpine as base +FROM python:3.11-alpine ENV PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 \ PIP_NO_CACHE_DIR=off \ PIP_DISABLE_PIP_VERSION_CHECK=on \ - PIP_NO_WARN_SCRIPT_LOCATION=0 + PIP_NO_WARN_SCRIPT_LOCATION=0 \ + PIP_ROOT_USER_ACTION=ignore ENV TGTG_TOKEN_PATH=/tokens ENV LOGS_PATH=/logs ENV DOCKER=true ENV UID=1000 ENV GID=1000 -RUN addgroup --gid $GID --system tgtg && \ +RUN addgroup --gid ${GID} --system tgtg && \ adduser --shell /bin/false \ --disabled-password \ - --uid $UID \ + --uid ${UID} \ --system \ --ingroup tgtg \ tgtg -RUN mkdir -p /logs -RUN mkdir -p /tokens -RUN chown tgtg:tgtg /tokens -RUN chown tgtg:tgtg /logs -VOLUME /tokens +RUN mkdir -p ${LOGS_PATH} +RUN mkdir -p ${TGTG_TOKEN_PATH} +RUN chown tgtg:tgtg ${LOGS_PATH} +RUN chown tgtg:tgtg ${TGTG_TOKEN_PATH} +VOLUME ${TGTG_TOKEN_PATH} RUN apk update && apk add --no-cache shadow runuser RUN --mount=type=bind,target=/context \ diff --git a/poetry.lock b/poetry.lock index a7da09fd..596a7301 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,25 +1,58 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "altgraph" -version = "0.17.3" +version = "0.17.4" description = "Python graph (network) package" optional = false python-versions = "*" files = [ - {file = "altgraph-0.17.3-py2.py3-none-any.whl", hash = "sha256:c8ac1ca6772207179ed8003ce7687757c04b0b71536f81e2ac5755c6226458fe"}, - {file = "altgraph-0.17.3.tar.gz", hash = "sha256:ad33358114df7c9416cdb8fa1eaa5852166c505118717021c6a8c7c7abbd03dd"}, + {file = "altgraph-0.17.4-py2.py3-none-any.whl", hash = "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff"}, + {file = "altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406"}, +] + +[[package]] +name = "anyio" +version = "4.2.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee"}, + {file = "anyio-4.2.0.tar.gz", hash = "sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] + +[[package]] +name = "appnope" +version = "0.1.3" +description = "Disable App Nap on macOS >= 10.9" +optional = false +python-versions = "*" +files = [ + {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"}, + {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"}, ] [[package]] name = "apprise" -version = "1.4.5" +version = "1.7.1" description = "Push Notifications that work with just about every platform!" optional = false python-versions = ">=3.6" files = [ - {file = "apprise-1.4.5-py3-none-any.whl", hash = "sha256:01c9949327d94c11c886bd1ae387ba7f61cdb9d6247b8096686920685e40fb47"}, - {file = "apprise-1.4.5.tar.gz", hash = "sha256:b7c66513c5456690a298ed887c9016ded42f15e365d16142e728b74f7cffee82"}, + {file = "apprise-1.7.1-py3-none-any.whl", hash = "sha256:eb2a7b546c6d4f426abb8b1006957e6a480c21215b5d780358445531611d1db7"}, + {file = "apprise-1.7.1.tar.gz", hash = "sha256:8d439d08550470524425dedee4bc8a72766c216c218f3772c37404eb2fd86e5a"}, ] [package.dependencies] @@ -31,161 +64,447 @@ requests = "*" requests-oauthlib = "*" [[package]] -name = "apscheduler" -version = "3.6.3" -description = "In-process task scheduler with Cron-like capabilities" +name = "argon2-cffi" +version = "23.1.0" +description = "Argon2 for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea"}, + {file = "argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08"}, +] + +[package.dependencies] +argon2-cffi-bindings = "*" + +[package.extras] +dev = ["argon2-cffi[tests,typing]", "tox (>4)"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-copybutton", "sphinx-notfound-page"] +tests = ["hypothesis", "pytest"] +typing = ["mypy"] + +[[package]] +name = "argon2-cffi-bindings" +version = "21.2.0" +description = "Low-level CFFI bindings for Argon2" +optional = false +python-versions = ">=3.6" +files = [ + {file = "argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f"}, + {file = "argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3e385d1c39c520c08b53d63300c3ecc28622f076f4c2b0e6d7e796e9f6502194"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3e3cc67fdb7d82c4718f19b4e7a87123caf8a93fde7e23cf66ac0337d3cb3f"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a22ad9800121b71099d0fb0a65323810a15f2e292f2ba450810a7316e128ee5"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9f8b450ed0547e3d473fdc8612083fd08dd2120d6ac8f73828df9b7d45bb351"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:93f9bf70084f97245ba10ee36575f0c3f1e7d7724d67d8e5b08e61787c320ed7"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a"}, +] + +[package.dependencies] +cffi = ">=1.0.1" + +[package.extras] +dev = ["cogapp", "pre-commit", "pytest", "wheel"] +tests = ["pytest"] + +[[package]] +name = "arrow" +version = "1.3.0" +description = "Better dates & times for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80"}, + {file = "arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85"}, +] + +[package.dependencies] +python-dateutil = ">=2.7.0" +types-python-dateutil = ">=2.8.10" + +[package.extras] +doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"] +test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2021.1)", "simplejson (==3.*)"] + +[[package]] +name = "asttokens" +version = "2.4.1" +description = "Annotate AST trees with source code positions" optional = false python-versions = "*" files = [ - {file = "APScheduler-3.6.3-py2.py3-none-any.whl", hash = "sha256:e8b1ecdb4c7cb2818913f766d5898183c7cb8936680710a4d3a966e02262e526"}, - {file = "APScheduler-3.6.3.tar.gz", hash = "sha256:3bb5229eed6fbbdafc13ce962712ae66e175aa214c69bed35a06bffcf0c5e244"}, + {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, + {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, ] [package.dependencies] -pytz = "*" -setuptools = ">=0.7" -six = ">=1.4.0" -tzlocal = ">=1.2" +six = ">=1.12.0" [package.extras] -asyncio = ["trollius"] -doc = ["sphinx", "sphinx-rtd-theme"] -gevent = ["gevent"] -mongodb = ["pymongo (>=2.8)"] -redis = ["redis (>=3.0)"] -rethinkdb = ["rethinkdb (>=2.4.0)"] -sqlalchemy = ["sqlalchemy (>=0.8)"] -testing = ["mock", "pytest", "pytest-asyncio", "pytest-asyncio (<0.6)", "pytest-cov", "pytest-tornado5"] -tornado = ["tornado (>=4.3)"] -twisted = ["twisted"] -zookeeper = ["kazoo"] +astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] +test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] + +[[package]] +name = "async-lru" +version = "2.0.4" +description = "Simple LRU cache for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "async-lru-2.0.4.tar.gz", hash = "sha256:b8a59a5df60805ff63220b2a0c5b5393da5521b113cd5465a44eb037d81a5627"}, + {file = "async_lru-2.0.4-py3-none-any.whl", hash = "sha256:ff02944ce3c288c5be660c42dbcca0742b32c3b279d6dceda655190240b99224"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} + +[[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.14.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Babel-2.14.0-py3-none-any.whl", hash = "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287"}, + {file = "Babel-2.14.0.tar.gz", hash = "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363"}, +] + +[package.extras] +dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] + +[[package]] +name = "beautifulsoup4" +version = "4.12.2" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.12.2-py3-none-any.whl", hash = "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a"}, + {file = "beautifulsoup4-4.12.2.tar.gz", hash = "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da"}, +] + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +name = "bleach" +version = "6.1.0" +description = "An easy safelist-based HTML-sanitizing tool." +optional = false +python-versions = ">=3.8" +files = [ + {file = "bleach-6.1.0-py3-none-any.whl", hash = "sha256:3225f354cfc436b9789c66c4ee030194bee0568fbf9cbdad3bc8b5c26c5f12b6"}, + {file = "bleach-6.1.0.tar.gz", hash = "sha256:0a31f1837963c41d46bbf1331b8778e1308ea0791db03cc4e7357b97cf42a8fe"}, +] + +[package.dependencies] +six = ">=1.9.0" +webencodings = "*" + +[package.extras] +css = ["tinycss2 (>=1.1.0,<1.3)"] + +[[package]] +name = "build" +version = "1.0.3" +description = "A simple, correct Python build frontend" +optional = false +python-versions = ">= 3.7" +files = [ + {file = "build-1.0.3-py3-none-any.whl", hash = "sha256:589bf99a67df7c9cf07ec0ac0e5e2ea5d4b37ac63301c4986d1acb126aa83f8f"}, + {file = "build-1.0.3.tar.gz", hash = "sha256:538aab1b64f9828977f84bc63ae570b060a8ed1be419e7870b8b4fc5e6ea553b"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "os_name == \"nt\""} +importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} +packaging = ">=19.0" +pyproject_hooks = "*" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} + +[package.extras] +docs = ["furo (>=2023.08.17)", "sphinx (>=7.0,<8.0)", "sphinx-argparse-cli (>=1.5)", "sphinx-autodoc-typehints (>=1.10)", "sphinx-issues (>=3.0.0)"] +test = ["filelock (>=3)", "pytest (>=6.2.4)", "pytest-cov (>=2.12)", "pytest-mock (>=2)", "pytest-rerunfailures (>=9.1)", "pytest-xdist (>=1.34)", "setuptools (>=42.0.0)", "setuptools (>=56.0.0)", "setuptools (>=56.0.0)", "setuptools (>=67.8.0)", "wheel (>=0.36.0)"] +typing = ["importlib-metadata (>=5.1)", "mypy (>=1.5.0,<1.6.0)", "tomli", "typing-extensions (>=3.7.4.3)"] +virtualenv = ["virtualenv (>=20.0.35)"] + +[[package]] +name = "cachecontrol" +version = "0.13.1" +description = "httplib2 caching for requests" +optional = false +python-versions = ">=3.7" +files = [ + {file = "cachecontrol-0.13.1-py3-none-any.whl", hash = "sha256:95dedbec849f46dda3137866dc28b9d133fc9af55f5b805ab1291833e4457aa4"}, + {file = "cachecontrol-0.13.1.tar.gz", hash = "sha256:f012366b79d2243a6118309ce73151bf52a38d4a5dac8ea57f09bd29087e506b"}, +] + +[package.dependencies] +filelock = {version = ">=3.8.0", optional = true, markers = "extra == \"filecache\""} +msgpack = ">=0.5.2" +requests = ">=2.16.0" + +[package.extras] +dev = ["CacheControl[filecache,redis]", "black", "build", "cherrypy", "mypy", "pytest", "pytest-cov", "sphinx", "tox", "types-redis", "types-requests"] +filecache = ["filelock (>=3.8.0)"] +redis = ["redis (>=2.10.5)"] [[package]] name = "cachetools" -version = "4.2.2" +version = "5.3.2" description = "Extensible memoizing collections and decorators" optional = false -python-versions = "~=3.5" +python-versions = ">=3.7" files = [ - {file = "cachetools-4.2.2-py3-none-any.whl", hash = "sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001"}, - {file = "cachetools-4.2.2.tar.gz", hash = "sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff"}, + {file = "cachetools-5.3.2-py3-none-any.whl", hash = "sha256:861f35a13a451f94e301ce2bec7cac63e881232ccce7ed67fab9b5df4d3beaa1"}, + {file = "cachetools-5.3.2.tar.gz", hash = "sha256:086ee420196f7b2ab9ca2db2520aca326318b68fe5ba8bc4d49cca91add450f2"}, ] [[package]] name = "certifi" -version = "2023.7.22" +version = "2023.11.17" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, - {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, + {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, + {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, ] +[[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.3.1" +version = "3.4.0" description = "Validate configuration and produce human readable error messages." optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.8" files = [ - {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, - {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, + {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.2.0" +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.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"}, - {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"}, + {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 = "cleo" +version = "2.1.0" +description = "Cleo allows you to create beautiful and testable command-line interfaces." +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "cleo-2.1.0-py3-none-any.whl", hash = "sha256:4a31bd4dd45695a64ee3c4758f583f134267c2bc518d8ae9a29cf237d009b07e"}, + {file = "cleo-2.1.0.tar.gz", hash = "sha256:0b2c880b5d13660a7ea651001fb4acb527696c01f15c9ee650f377aa543fd523"}, ] +[package.dependencies] +crashtest = ">=0.4.1,<0.5.0" +rapidfuzz = ">=3.0.0,<4.0.0" + [[package]] name = "click" -version = "8.1.6" +version = "8.1.7" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"}, - {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"}, + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, ] [package.dependencies] @@ -204,13 +523,13 @@ files = [ [[package]] name = "colorlog" -version = "6.7.0" +version = "6.8.0" description = "Add colours to the output of Python's logging module." optional = false python-versions = ">=3.6" files = [ - {file = "colorlog-6.7.0-py2.py3-none-any.whl", hash = "sha256:0d33ca236784a1ba3ff9c532d4964126d8a2c44f1f0cb1d2b0728196f512f662"}, - {file = "colorlog-6.7.0.tar.gz", hash = "sha256:bd94bd21c1e13fac7bd3153f4bc3a7dc0eb0974b8bc2fdf1a989e474f6e582e5"}, + {file = "colorlog-6.8.0-py3-none-any.whl", hash = "sha256:4ed23b05a1154294ac99f511fabe8c1d6d4364ec1f7fc989c7fb515ccc29d375"}, + {file = "colorlog-6.8.0.tar.gz", hash = "sha256:fbb6fdf9d5685f2517f388fb29bb27d54e8654dd31f58bc2a3b217e967a95ca6"}, ] [package.dependencies] @@ -219,73 +538,82 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} [package.extras] development = ["black", "flake8", "mypy", "pytest", "types-colorama"] +[[package]] +name = "comm" +version = "0.2.0" +description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc." +optional = false +python-versions = ">=3.8" +files = [ + {file = "comm-0.2.0-py3-none-any.whl", hash = "sha256:2da8d9ebb8dd7bfc247adaff99f24dce705638a8042b85cb995066793e391001"}, + {file = "comm-0.2.0.tar.gz", hash = "sha256:a517ea2ca28931c7007a7a99c562a0fa5883cfb48963140cf642c41c948498be"}, +] + +[package.dependencies] +traitlets = ">=4" + +[package.extras] +test = ["pytest"] + [[package]] name = "coverage" -version = "7.2.7" +version = "7.4.0" description = "Code coverage measurement for Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, - {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, - {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, - {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, - {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, - {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, - {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, - {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, - {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, - {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, - {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, - {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, - {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, - {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, - {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, - {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, - {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, - {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, - {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, - {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, - {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, - {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, - {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, - {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, + {file = "coverage-7.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36b0ea8ab20d6a7564e89cb6135920bc9188fb5f1f7152e94e8300b7b189441a"}, + {file = "coverage-7.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0676cd0ba581e514b7f726495ea75aba3eb20899d824636c6f59b0ed2f88c471"}, + {file = "coverage-7.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ca5c71a5a1765a0f8f88022c52b6b8be740e512980362f7fdbb03725a0d6b9"}, + {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7c97726520f784239f6c62506bc70e48d01ae71e9da128259d61ca5e9788516"}, + {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:815ac2d0f3398a14286dc2cea223a6f338109f9ecf39a71160cd1628786bc6f5"}, + {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:80b5ee39b7f0131ebec7968baa9b2309eddb35b8403d1869e08f024efd883566"}, + {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5b2ccb7548a0b65974860a78c9ffe1173cfb5877460e5a229238d985565574ae"}, + {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:995ea5c48c4ebfd898eacb098164b3cc826ba273b3049e4a889658548e321b43"}, + {file = "coverage-7.4.0-cp310-cp310-win32.whl", hash = "sha256:79287fd95585ed36e83182794a57a46aeae0b64ca53929d1176db56aacc83451"}, + {file = "coverage-7.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:5b14b4f8760006bfdb6e08667af7bc2d8d9bfdb648351915315ea17645347137"}, + {file = "coverage-7.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:04387a4a6ecb330c1878907ce0dc04078ea72a869263e53c72a1ba5bbdf380ca"}, + {file = "coverage-7.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea81d8f9691bb53f4fb4db603203029643caffc82bf998ab5b59ca05560f4c06"}, + {file = "coverage-7.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74775198b702868ec2d058cb92720a3c5a9177296f75bd97317c787daf711505"}, + {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76f03940f9973bfaee8cfba70ac991825611b9aac047e5c80d499a44079ec0bc"}, + {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:485e9f897cf4856a65a57c7f6ea3dc0d4e6c076c87311d4bc003f82cfe199d25"}, + {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6ae8c9d301207e6856865867d762a4b6fd379c714fcc0607a84b92ee63feff70"}, + {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bf477c355274a72435ceb140dc42de0dc1e1e0bf6e97195be30487d8eaaf1a09"}, + {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:83c2dda2666fe32332f8e87481eed056c8b4d163fe18ecc690b02802d36a4d26"}, + {file = "coverage-7.4.0-cp311-cp311-win32.whl", hash = "sha256:697d1317e5290a313ef0d369650cfee1a114abb6021fa239ca12b4849ebbd614"}, + {file = "coverage-7.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:26776ff6c711d9d835557ee453082025d871e30b3fd6c27fcef14733f67f0590"}, + {file = "coverage-7.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:13eaf476ec3e883fe3e5fe3707caeb88268a06284484a3daf8250259ef1ba143"}, + {file = "coverage-7.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846f52f46e212affb5bcf131c952fb4075b55aae6b61adc9856222df89cbe3e2"}, + {file = "coverage-7.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26f66da8695719ccf90e794ed567a1549bb2644a706b41e9f6eae6816b398c4a"}, + {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:164fdcc3246c69a6526a59b744b62e303039a81e42cfbbdc171c91a8cc2f9446"}, + {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:316543f71025a6565677d84bc4df2114e9b6a615aa39fb165d697dba06a54af9"}, + {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bb1de682da0b824411e00a0d4da5a784ec6496b6850fdf8c865c1d68c0e318dd"}, + {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0e8d06778e8fbffccfe96331a3946237f87b1e1d359d7fbe8b06b96c95a5407a"}, + {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a56de34db7b7ff77056a37aedded01b2b98b508227d2d0979d373a9b5d353daa"}, + {file = "coverage-7.4.0-cp312-cp312-win32.whl", hash = "sha256:51456e6fa099a8d9d91497202d9563a320513fcf59f33991b0661a4a6f2ad450"}, + {file = "coverage-7.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd3c1e4cb2ff0083758f09be0f77402e1bdf704adb7f89108007300a6da587d0"}, + {file = "coverage-7.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9d1bf53c4c8de58d22e0e956a79a5b37f754ed1ffdbf1a260d9dcfa2d8a325e"}, + {file = "coverage-7.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:109f5985182b6b81fe33323ab4707011875198c41964f014579cf82cebf2bb85"}, + {file = "coverage-7.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc9d4bc55de8003663ec94c2f215d12d42ceea128da8f0f4036235a119c88ac"}, + {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc6d65b21c219ec2072c1293c505cf36e4e913a3f936d80028993dd73c7906b1"}, + {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a10a4920def78bbfff4eff8a05c51be03e42f1c3735be42d851f199144897ba"}, + {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b8e99f06160602bc64da35158bb76c73522a4010f0649be44a4e167ff8555952"}, + {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7d360587e64d006402b7116623cebf9d48893329ef035278969fa3bbf75b697e"}, + {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:29f3abe810930311c0b5d1a7140f6395369c3db1be68345638c33eec07535105"}, + {file = "coverage-7.4.0-cp38-cp38-win32.whl", hash = "sha256:5040148f4ec43644702e7b16ca864c5314ccb8ee0751ef617d49aa0e2d6bf4f2"}, + {file = "coverage-7.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:9864463c1c2f9cb3b5db2cf1ff475eed2f0b4285c2aaf4d357b69959941aa555"}, + {file = "coverage-7.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:936d38794044b26c99d3dd004d8af0035ac535b92090f7f2bb5aa9c8e2f5cd42"}, + {file = "coverage-7.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:799c8f873794a08cdf216aa5d0531c6a3747793b70c53f70e98259720a6fe2d7"}, + {file = "coverage-7.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7defbb9737274023e2d7af02cac77043c86ce88a907c58f42b580a97d5bcca9"}, + {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1526d265743fb49363974b7aa8d5899ff64ee07df47dd8d3e37dcc0818f09ed"}, + {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf635a52fc1ea401baf88843ae8708591aa4adff875e5c23220de43b1ccf575c"}, + {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:756ded44f47f330666843b5781be126ab57bb57c22adbb07d83f6b519783b870"}, + {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0eb3c2f32dabe3a4aaf6441dde94f35687224dfd7eb2a7f47f3fd9428e421058"}, + {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bfd5db349d15c08311702611f3dccbef4b4e2ec148fcc636cf8739519b4a5c0f"}, + {file = "coverage-7.4.0-cp39-cp39-win32.whl", hash = "sha256:53d7d9158ee03956e0eadac38dfa1ec8068431ef8058fe6447043db1fb40d932"}, + {file = "coverage-7.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:cfd2a8b6b0d8e66e944d47cdec2f47c48fef2ba2f2dff5a9a75757f64172857e"}, + {file = "coverage-7.4.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:c530833afc4707fe48524a44844493f36d8727f04dcce91fb978c414a8556cc6"}, + {file = "coverage-7.4.0.tar.gz", hash = "sha256:707c0f58cb1712b8809ece32b68996ee1e609f71bd14615bd8f87a1293cb610e"}, ] [package.dependencies] @@ -294,6 +622,17 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1 [package.extras] toml = ["tomli"] +[[package]] +name = "crashtest" +version = "0.4.1" +description = "Manage Python errors with ease" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "crashtest-0.4.1-py3-none-any.whl", hash = "sha256:8d23eac5fa660409f57472e3851dab7ac18aba459a8d19cbbba86d3d5aecd2a5"}, + {file = "crashtest-0.4.1.tar.gz", hash = "sha256:80d7b1f316ebfbd429f648076d6275c877ba30ba48979de4191714a75266f0ce"}, +] + [[package]] name = "cron-descriptor" version = "1.4.0" @@ -307,45 +646,266 @@ files = [ [package.extras] dev = ["polib"] +[[package]] +name = "cryptography" +version = "41.0.7" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf"}, + {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1"}, + {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157"}, + {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406"}, + {file = "cryptography-41.0.7-cp37-abi3-win32.whl", hash = "sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d"}, + {file = "cryptography-41.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309"}, + {file = "cryptography-41.0.7.tar.gz", hash = "sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc"}, +] + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] +nox = ["nox"] +pep8test = ["black", "check-sdist", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "debugpy" +version = "1.8.0" +description = "An implementation of the Debug Adapter Protocol for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "debugpy-1.8.0-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:7fb95ca78f7ac43393cd0e0f2b6deda438ec7c5e47fa5d38553340897d2fbdfb"}, + {file = "debugpy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef9ab7df0b9a42ed9c878afd3eaaff471fce3fa73df96022e1f5c9f8f8c87ada"}, + {file = "debugpy-1.8.0-cp310-cp310-win32.whl", hash = "sha256:a8b7a2fd27cd9f3553ac112f356ad4ca93338feadd8910277aff71ab24d8775f"}, + {file = "debugpy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5d9de202f5d42e62f932507ee8b21e30d49aae7e46d5b1dd5c908db1d7068637"}, + {file = "debugpy-1.8.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:ef54404365fae8d45cf450d0544ee40cefbcb9cb85ea7afe89a963c27028261e"}, + {file = "debugpy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60009b132c91951354f54363f8ebdf7457aeb150e84abba5ae251b8e9f29a8a6"}, + {file = "debugpy-1.8.0-cp311-cp311-win32.whl", hash = "sha256:8cd0197141eb9e8a4566794550cfdcdb8b3db0818bdf8c49a8e8f8053e56e38b"}, + {file = "debugpy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:a64093656c4c64dc6a438e11d59369875d200bd5abb8f9b26c1f5f723622e153"}, + {file = "debugpy-1.8.0-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:b05a6b503ed520ad58c8dc682749113d2fd9f41ffd45daec16e558ca884008cd"}, + {file = "debugpy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c6fb41c98ec51dd010d7ed650accfd07a87fe5e93eca9d5f584d0578f28f35f"}, + {file = "debugpy-1.8.0-cp38-cp38-win32.whl", hash = "sha256:46ab6780159eeabb43c1495d9c84cf85d62975e48b6ec21ee10c95767c0590aa"}, + {file = "debugpy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:bdc5ef99d14b9c0fcb35351b4fbfc06ac0ee576aeab6b2511702e5a648a2e595"}, + {file = "debugpy-1.8.0-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:61eab4a4c8b6125d41a34bad4e5fe3d2cc145caecd63c3fe953be4cc53e65bf8"}, + {file = "debugpy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:125b9a637e013f9faac0a3d6a82bd17c8b5d2c875fb6b7e2772c5aba6d082332"}, + {file = "debugpy-1.8.0-cp39-cp39-win32.whl", hash = "sha256:57161629133113c97b387382045649a2b985a348f0c9366e22217c87b68b73c6"}, + {file = "debugpy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:e3412f9faa9ade82aa64a50b602544efcba848c91384e9f93497a458767e6926"}, + {file = "debugpy-1.8.0-py2.py3-none-any.whl", hash = "sha256:9c9b0ac1ce2a42888199df1a1906e45e6f3c9555497643a85e0bf2406e3ffbc4"}, + {file = "debugpy-1.8.0.zip", hash = "sha256:12af2c55b419521e33d5fb21bd022df0b5eb267c3e178f1d374a63a2a6bdccd0"}, +] + +[[package]] +name = "decorator" +version = "5.1.1" +description = "Decorators for Humans" +optional = false +python-versions = ">=3.5" +files = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +description = "XML bomb protection for Python stdlib modules" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, + {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, +] + [[package]] name = "distlib" -version = "0.3.7" +version = "0.3.8" description = "Distribution utilities" optional = false python-versions = "*" files = [ - {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, - {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, + {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 = "dulwich" +version = "0.21.7" +description = "Python Git Library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "dulwich-0.21.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d4c0110798099bb7d36a110090f2688050703065448895c4f53ade808d889dd3"}, + {file = "dulwich-0.21.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2bc12697f0918bee324c18836053644035362bb3983dc1b210318f2fed1d7132"}, + {file = "dulwich-0.21.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:471305af74790827fcbafe330fc2e8bdcee4fb56ca1177c8c481b1c8f806c4a4"}, + {file = "dulwich-0.21.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d54c9d0e845be26f65f954dff13a1cd3f2b9739820c19064257b8fd7435ab263"}, + {file = "dulwich-0.21.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12d61334a575474e707614f2e93d6ed4cdae9eb47214f9277076d9e5615171d3"}, + {file = "dulwich-0.21.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e274cebaf345f0b1e3b70197f2651de92b652386b68020cfd3bf61bc30f6eaaa"}, + {file = "dulwich-0.21.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:817822f970e196e757ae01281ecbf21369383285b9f4a83496312204cf889b8c"}, + {file = "dulwich-0.21.7-cp310-cp310-win32.whl", hash = "sha256:7836da3f4110ce684dcd53489015fb7fa94ed33c5276e3318b8b1cbcb5b71e08"}, + {file = "dulwich-0.21.7-cp310-cp310-win_amd64.whl", hash = "sha256:4a043b90958cec866b4edc6aef5fe3c2c96a664d0b357e1682a46f6c477273c4"}, + {file = "dulwich-0.21.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ce8db196e79c1f381469410d26fb1d8b89c6b87a4e7f00ff418c22a35121405c"}, + {file = "dulwich-0.21.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:62bfb26bdce869cd40be443dfd93143caea7089b165d2dcc33de40f6ac9d812a"}, + {file = "dulwich-0.21.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c01a735b9a171dcb634a97a3cec1b174cfbfa8e840156870384b633da0460f18"}, + {file = "dulwich-0.21.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa4d14767cf7a49c9231c2e52cb2a3e90d0c83f843eb6a2ca2b5d81d254cf6b9"}, + {file = "dulwich-0.21.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bca4b86e96d6ef18c5bc39828ea349efb5be2f9b1f6ac9863f90589bac1084d"}, + {file = "dulwich-0.21.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a7b5624b02ef808cdc62dabd47eb10cd4ac15e8ac6df9e2e88b6ac6b40133673"}, + {file = "dulwich-0.21.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c3a539b4696a42fbdb7412cb7b66a4d4d332761299d3613d90a642923c7560e1"}, + {file = "dulwich-0.21.7-cp311-cp311-win32.whl", hash = "sha256:675a612ce913081beb0f37b286891e795d905691dfccfb9bf73721dca6757cde"}, + {file = "dulwich-0.21.7-cp311-cp311-win_amd64.whl", hash = "sha256:460ba74bdb19f8d498786ae7776745875059b1178066208c0fd509792d7f7bfc"}, + {file = "dulwich-0.21.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4c51058ec4c0b45dc5189225b9e0c671b96ca9713c1daf71d622c13b0ab07681"}, + {file = "dulwich-0.21.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4bc4c5366eaf26dda3fdffe160a3b515666ed27c2419f1d483da285ac1411de0"}, + {file = "dulwich-0.21.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a0650ec77d89cb947e3e4bbd4841c96f74e52b4650830112c3057a8ca891dc2f"}, + {file = "dulwich-0.21.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f18f0a311fb7734b033a3101292b932158cade54b74d1c44db519e42825e5a2"}, + {file = "dulwich-0.21.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c589468e5c0cd84e97eb7ec209ab005a2cb69399e8c5861c3edfe38989ac3a8"}, + {file = "dulwich-0.21.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d62446797163317a397a10080c6397ffaaca51a7804c0120b334f8165736c56a"}, + {file = "dulwich-0.21.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e84cc606b1f581733df4350ca4070e6a8b30be3662bbb81a590b177d0c996c91"}, + {file = "dulwich-0.21.7-cp312-cp312-win32.whl", hash = "sha256:c3d1685f320907a52c40fd5890627945c51f3a5fa4bcfe10edb24fec79caadec"}, + {file = "dulwich-0.21.7-cp312-cp312-win_amd64.whl", hash = "sha256:6bd69921fdd813b7469a3c77bc75c1783cc1d8d72ab15a406598e5a3ba1a1503"}, + {file = "dulwich-0.21.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7d8ab29c660125db52106775caa1f8f7f77a69ed1fe8bc4b42bdf115731a25bf"}, + {file = "dulwich-0.21.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0d2e4485b98695bf95350ce9d38b1bb0aaac2c34ad00a0df789aa33c934469b"}, + {file = "dulwich-0.21.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e138d516baa6b5bafbe8f030eccc544d0d486d6819b82387fc0e285e62ef5261"}, + {file = "dulwich-0.21.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f34bf9b9fa9308376263fd9ac43143c7c09da9bc75037bb75c6c2423a151b92c"}, + {file = "dulwich-0.21.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2e2c66888207b71cd1daa2acb06d3984a6bc13787b837397a64117aa9fc5936a"}, + {file = "dulwich-0.21.7-cp37-cp37m-win32.whl", hash = "sha256:10893105c6566fc95bc2a67b61df7cc1e8f9126d02a1df6a8b2b82eb59db8ab9"}, + {file = "dulwich-0.21.7-cp37-cp37m-win_amd64.whl", hash = "sha256:460b3849d5c3d3818a80743b4f7a0094c893c559f678e56a02fff570b49a644a"}, + {file = "dulwich-0.21.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:74700e4c7d532877355743336c36f51b414d01e92ba7d304c4f8d9a5946dbc81"}, + {file = "dulwich-0.21.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c92e72c43c9e9e936b01a57167e0ea77d3fd2d82416edf9489faa87278a1cdf7"}, + {file = "dulwich-0.21.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d097e963eb6b9fa53266146471531ad9c6765bf390849230311514546ed64db2"}, + {file = "dulwich-0.21.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:808e8b9cc0aa9ac74870b49db4f9f39a52fb61694573f84b9c0613c928d4caf8"}, + {file = "dulwich-0.21.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1957b65f96e36c301e419d7adaadcff47647c30eb072468901bb683b1000bc5"}, + {file = "dulwich-0.21.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4b09bc3a64fb70132ec14326ecbe6e0555381108caff3496898962c4136a48c6"}, + {file = "dulwich-0.21.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5882e70b74ac3c736a42d3fdd4f5f2e6570637f59ad5d3e684760290b58f041"}, + {file = "dulwich-0.21.7-cp38-cp38-win32.whl", hash = "sha256:29bb5c1d70eba155ded41ed8a62be2f72edbb3c77b08f65b89c03976292f6d1b"}, + {file = "dulwich-0.21.7-cp38-cp38-win_amd64.whl", hash = "sha256:25c3ab8fb2e201ad2031ddd32e4c68b7c03cb34b24a5ff477b7a7dcef86372f5"}, + {file = "dulwich-0.21.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8929c37986c83deb4eb500c766ee28b6670285b512402647ee02a857320e377c"}, + {file = "dulwich-0.21.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cc1e11be527ac06316539b57a7688bcb1b6a3e53933bc2f844397bc50734e9ae"}, + {file = "dulwich-0.21.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0fc3078a1ba04c588fabb0969d3530efd5cd1ce2cf248eefb6baf7cbc15fc285"}, + {file = "dulwich-0.21.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40dcbd29ba30ba2c5bfbab07a61a5f20095541d5ac66d813056c122244df4ac0"}, + {file = "dulwich-0.21.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8869fc8ec3dda743e03d06d698ad489b3705775fe62825e00fa95aa158097fc0"}, + {file = "dulwich-0.21.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d96ca5e0dde49376fbcb44f10eddb6c30284a87bd03bb577c59bb0a1f63903fa"}, + {file = "dulwich-0.21.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e0064363bd5e814359657ae32517fa8001e8573d9d040bd997908d488ab886ed"}, + {file = "dulwich-0.21.7-cp39-cp39-win32.whl", hash = "sha256:869eb7be48243e695673b07905d18b73d1054a85e1f6e298fe63ba2843bb2ca1"}, + {file = "dulwich-0.21.7-cp39-cp39-win_amd64.whl", hash = "sha256:404b8edeb3c3a86c47c0a498699fc064c93fa1f8bab2ffe919e8ab03eafaaad3"}, + {file = "dulwich-0.21.7-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e598d743c6c0548ebcd2baf94aa9c8bfacb787ea671eeeb5828cfbd7d56b552f"}, + {file = "dulwich-0.21.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a2d76c96426e791556836ef43542b639def81be4f1d6d4322cd886c115eae1"}, + {file = "dulwich-0.21.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6c88acb60a1f4d31bd6d13bfba465853b3df940ee4a0f2a3d6c7a0778c705b7"}, + {file = "dulwich-0.21.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ecd315847dea406a4decfa39d388a2521e4e31acde3bd9c2609c989e817c6d62"}, + {file = "dulwich-0.21.7-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d05d3c781bc74e2c2a2a8f4e4e2ed693540fbe88e6ac36df81deac574a6dad99"}, + {file = "dulwich-0.21.7-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6de6f8de4a453fdbae8062a6faa652255d22a3d8bce0cd6d2d6701305c75f2b3"}, + {file = "dulwich-0.21.7-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e25953c7acbbe4e19650d0225af1c0c0e6882f8bddd2056f75c1cc2b109b88ad"}, + {file = "dulwich-0.21.7-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:4637cbd8ed1012f67e1068aaed19fcc8b649bcf3e9e26649826a303298c89b9d"}, + {file = "dulwich-0.21.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:858842b30ad6486aacaa607d60bab9c9a29e7c59dc2d9cb77ae5a94053878c08"}, + {file = "dulwich-0.21.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:739b191f61e1c4ce18ac7d520e7a7cbda00e182c3489552408237200ce8411ad"}, + {file = "dulwich-0.21.7-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:274c18ec3599a92a9b67abaf110e4f181a4f779ee1aaab9e23a72e89d71b2bd9"}, + {file = "dulwich-0.21.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:2590e9b431efa94fc356ae33b38f5e64f1834ec3a94a6ac3a64283b206d07aa3"}, + {file = "dulwich-0.21.7-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ed60d1f610ef6437586f7768254c2a93820ccbd4cfdac7d182cf2d6e615969bb"}, + {file = "dulwich-0.21.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8278835e168dd097089f9e53088c7a69c6ca0841aef580d9603eafe9aea8c358"}, + {file = "dulwich-0.21.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffc27fb063f740712e02b4d2f826aee8bbed737ed799962fef625e2ce56e2d29"}, + {file = "dulwich-0.21.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:61e3451bd3d3844f2dca53f131982553be4d1b1e1ebd9db701843dd76c4dba31"}, + {file = "dulwich-0.21.7.tar.gz", hash = "sha256:a9e9c66833cea580c3ac12927e4b9711985d76afca98da971405d414de60e968"}, +] + +[package.dependencies] +urllib3 = ">=1.25" + +[package.extras] +fastimport = ["fastimport"] +https = ["urllib3 (>=1.24.1)"] +paramiko = ["paramiko"] +pgp = ["gpg"] + [[package]] name = "exceptiongroup" -version = "1.1.2" +version = "1.2.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.1.2-py3-none-any.whl", hash = "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"}, - {file = "exceptiongroup-1.1.2.tar.gz", hash = "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5"}, + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, ] [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "executing" +version = "2.0.1" +description = "Get the currently executing AST node of a frame, and other information" +optional = false +python-versions = ">=3.5" +files = [ + {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"}, + {file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"}, +] + +[package.extras] +tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] + +[[package]] +name = "fastjsonschema" +version = "2.19.1" +description = "Fastest Python implementation of JSON schema" +optional = false +python-versions = "*" +files = [ + {file = "fastjsonschema-2.19.1-py3-none-any.whl", hash = "sha256:3672b47bc94178c9f23dbb654bf47440155d4db9df5f7bc47643315f9c405cd0"}, + {file = "fastjsonschema-2.19.1.tar.gz", hash = "sha256:e3126a94bdc4623d3de4485f8d468a12f02a67921315ddc87836d6e456dc789d"}, +] + +[package.extras] +devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"] + [[package]] name = "filelock" -version = "3.12.2" +version = "3.13.1" description = "A platform independent file lock." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, - {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, + {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, + {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, ] [package.extras] -docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] + +[[package]] +name = "fqdn" +version = "1.5.1" +description = "Validates fully-qualified domain names against RFC 1123, so that they are acceptable to modern bowsers" +optional = false +python-versions = ">=2.7, !=3.0, !=3.1, !=3.2, !=3.3, !=3.4, <4" +files = [ + {file = "fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014"}, + {file = "fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f"}, +] [[package]] name = "googlemaps" @@ -360,15 +920,71 @@ files = [ [package.dependencies] requests = ">=2.20.0,<3.0" +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httpcore" +version = "1.0.2" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"}, + {file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<0.23.0)"] + +[[package]] +name = "httpx" +version = "0.25.2" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.25.2-py3-none-any.whl", hash = "sha256:a05d3d052d9b2dfce0e3896636467f8a5342fb2b902c819428e1ac65413ca118"}, + {file = "httpx-0.25.2.tar.gz", hash = "sha256:8b8fcaa0c8ea7b05edd69a094e63a2094c4efcb48129fb757361bc423c0ad9e8"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + [[package]] name = "humanize" -version = "4.7.0" +version = "4.9.0" description = "Python humanize utilities" optional = false python-versions = ">=3.8" files = [ - {file = "humanize-4.7.0-py3-none-any.whl", hash = "sha256:df7c429c2d27372b249d3f26eb53b07b166b661326e0325793e0a988082e3889"}, - {file = "humanize-4.7.0.tar.gz", hash = "sha256:7ca0e43e870981fa684acb5b062deb307218193bca1a01f2b2676479df849b3a"}, + {file = "humanize-4.9.0-py3-none-any.whl", hash = "sha256:ce284a76d5b1377fd8836733b983bfb0b76f1aa1c090de2566fcf008d7f6ab16"}, + {file = "humanize-4.9.0.tar.gz", hash = "sha256:582a265c931c683a7e9b8ed9559089dea7edcf6cc95be39a3cbc2c5d5ac2bcfa"}, ] [package.extras] @@ -376,13 +992,13 @@ tests = ["freezegun", "pytest", "pytest-cov"] [[package]] name = "identify" -version = "2.5.26" +version = "2.5.33" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "identify-2.5.26-py2.py3-none-any.whl", hash = "sha256:c22a8ead0d4ca11f1edd6c9418c3220669b3b7533ada0a0ffa6cc0ef85cf9b54"}, - {file = "identify-2.5.26.tar.gz", hash = "sha256:7243800bce2f58404ed41b7c002e53d4d22bcf3ae1b7900c2d7aefd95394bf7f"}, + {file = "identify-2.5.33-py2.py3-none-any.whl", hash = "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"}, + {file = "identify-2.5.33.tar.gz", hash = "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d"}, ] [package.extras] @@ -390,31 +1006,31 @@ license = ["ukkonen"] [[package]] name = "idna" -version = "3.4" +version = "3.6" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, ] [[package]] name = "importlib-metadata" -version = "6.8.0" +version = "7.0.1" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"}, - {file = "importlib_metadata-6.8.0.tar.gz", hash = "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"}, + {file = "importlib_metadata-7.0.1-py3-none-any.whl", hash = "sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e"}, + {file = "importlib_metadata-7.0.1.tar.gz", hash = "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc"}, ] [package.dependencies] zipp = ">=0.5" [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] @@ -430,40 +1046,764 @@ files = [ ] [[package]] -name = "macholib" -version = "1.16.2" -description = "Mach-O header analysis and editing" +name = "installer" +version = "0.7.0" +description = "A library for installing Python wheels." optional = false -python-versions = "*" +python-versions = ">=3.7" files = [ - {file = "macholib-1.16.2-py2.py3-none-any.whl", hash = "sha256:44c40f2cd7d6726af8fa6fe22549178d3a4dfecc35a9cd15ea916d9c83a688e0"}, - {file = "macholib-1.16.2.tar.gz", hash = "sha256:557bbfa1bb255c20e9abafe7ed6cd8046b48d9525db2f9b77d3122a63a2a8bf8"}, + {file = "installer-0.7.0-py3-none-any.whl", hash = "sha256:05d1933f0a5ba7d8d6296bb6d5018e7c94fa473ceb10cf198a92ccea19c27b53"}, + {file = "installer-0.7.0.tar.gz", hash = "sha256:a26d3e3116289bb08216e0d0f7d925fcef0b0194eedfa0c944bcaaa106c4b631"}, ] -[package.dependencies] -altgraph = ">=0.17" - [[package]] -name = "markdown" -version = "3.4.4" -description = "Python implementation of John Gruber's Markdown." +name = "ipykernel" +version = "6.28.0" +description = "IPython Kernel for Jupyter" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "Markdown-3.4.4-py3-none-any.whl", hash = "sha256:a4c1b65c0957b4bd9e7d86ddc7b3c9868fb9670660f6f99f6d1bca8954d5a941"}, - {file = "Markdown-3.4.4.tar.gz", hash = "sha256:225c6123522495d4119a90b3a3ba31a1e87a70369e03f14799ea9c0d7183a3d6"}, + {file = "ipykernel-6.28.0-py3-none-any.whl", hash = "sha256:c6e9a9c63a7f4095c0a22a79f765f079f9ec7be4f2430a898ddea889e8665661"}, + {file = "ipykernel-6.28.0.tar.gz", hash = "sha256:69c11403d26de69df02225916f916b37ea4b9af417da0a8c827f84328d88e5f3"}, ] [package.dependencies] -importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} +appnope = {version = "*", markers = "platform_system == \"Darwin\""} +comm = ">=0.1.1" +debugpy = ">=1.6.5" +ipython = ">=7.23.1" +jupyter-client = ">=6.1.12" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +matplotlib-inline = ">=0.1" +nest-asyncio = "*" +packaging = "*" +psutil = "*" +pyzmq = ">=24" +tornado = ">=6.1" +traitlets = ">=5.4.0" [package.extras] -docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.0)", "mkdocs-nature (>=0.4)"] -testing = ["coverage", "pyyaml"] +cov = ["coverage[toml]", "curio", "matplotlib", "pytest-cov", "trio"] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "trio"] +pyqt5 = ["pyqt5"] +pyside6 = ["pyside6"] +test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio", "pytest-cov", "pytest-timeout"] [[package]] -name = "nodeenv" -version = "1.8.0" +name = "ipython" +version = "8.18.1" +description = "IPython: Productive Interactive Computing" +optional = false +python-versions = ">=3.9" +files = [ + {file = "ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397"}, + {file = "ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} +prompt-toolkit = ">=3.0.41,<3.1.0" +pygments = ">=2.4.0" +stack-data = "*" +traitlets = ">=5" +typing-extensions = {version = "*", markers = "python_version < \"3.10\""} + +[package.extras] +all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] +black = ["black"] +doc = ["docrepr", "exceptiongroup", "ipykernel", "matplotlib", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] +kernel = ["ipykernel"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["ipywidgets", "notebook"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath"] +test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath", "trio"] + +[[package]] +name = "isoduration" +version = "20.11.0" +description = "Operations with ISO 8601 durations" +optional = false +python-versions = ">=3.7" +files = [ + {file = "isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042"}, + {file = "isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9"}, +] + +[package.dependencies] +arrow = ">=0.15.0" + +[[package]] +name = "jaraco-classes" +version = "3.3.0" +description = "Utility functions for Python class constructs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jaraco.classes-3.3.0-py3-none-any.whl", hash = "sha256:10afa92b6743f25c0cf5f37c6bb6e18e2c5bb84a16527ccfc0040ea377e7aaeb"}, + {file = "jaraco.classes-3.3.0.tar.gz", hash = "sha256:c063dd08e89217cee02c8d5e5ec560f2c8ce6cdc2fcdc2e68f7b2e5547ed3621"}, +] + +[package.dependencies] +more-itertools = "*" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"] + +[[package]] +name = "jedi" +version = "0.19.1" +description = "An autocompletion tool for Python that can be used for text editors." +optional = false +python-versions = ">=3.6" +files = [ + {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, + {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, +] + +[package.dependencies] +parso = ">=0.8.3,<0.9.0" + +[package.extras] +docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] + +[[package]] +name = "jeepney" +version = "0.8.0" +description = "Low-level, pure Python DBus protocol wrapper." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"}, + {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"}, +] + +[package.extras] +test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] +trio = ["async_generator", "trio"] + +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "json5" +version = "0.9.14" +description = "A Python implementation of the JSON5 data format." +optional = false +python-versions = "*" +files = [ + {file = "json5-0.9.14-py2.py3-none-any.whl", hash = "sha256:740c7f1b9e584a468dbb2939d8d458db3427f2c93ae2139d05f47e453eae964f"}, + {file = "json5-0.9.14.tar.gz", hash = "sha256:9ed66c3a6ca3510a976a9ef9b8c0787de24802724ab1860bc0153c7fdd589b02"}, +] + +[package.extras] +dev = ["hypothesis"] + +[[package]] +name = "jsonpointer" +version = "2.4" +description = "Identify specific nodes in a JSON document (RFC 6901)" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" +files = [ + {file = "jsonpointer-2.4-py2.py3-none-any.whl", hash = "sha256:15d51bba20eea3165644553647711d150376234112651b4f1811022aecad7d7a"}, + {file = "jsonpointer-2.4.tar.gz", hash = "sha256:585cee82b70211fa9e6043b7bb89db6e1aa49524340dde8ad6b63206ea689d88"}, +] + +[[package]] +name = "jsonschema" +version = "4.20.0" +description = "An implementation of JSON Schema validation for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jsonschema-4.20.0-py3-none-any.whl", hash = "sha256:ed6231f0429ecf966f5bc8dfef245998220549cbbcf140f913b7464c52c3b6b3"}, + {file = "jsonschema-4.20.0.tar.gz", hash = "sha256:4f614fd46d8d61258610998997743ec5492a648b33cf478c1ddc23ed4598a5fa"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +fqdn = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +idna = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +isoduration = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +jsonpointer = {version = ">1.13", optional = true, markers = "extra == \"format-nongpl\""} +jsonschema-specifications = ">=2023.03.6" +referencing = ">=0.28.4" +rfc3339-validator = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +rfc3986-validator = {version = ">0.1.0", optional = true, markers = "extra == \"format-nongpl\""} +rpds-py = ">=0.7.1" +uri-template = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +webcolors = {version = ">=1.11", optional = true, markers = "extra == \"format-nongpl\""} + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] + +[[package]] +name = "jsonschema-specifications" +version = "2023.12.1" +description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jsonschema_specifications-2023.12.1-py3-none-any.whl", hash = "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c"}, + {file = "jsonschema_specifications-2023.12.1.tar.gz", hash = "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc"}, +] + +[package.dependencies] +referencing = ">=0.31.0" + +[[package]] +name = "jupyter-client" +version = "8.6.0" +description = "Jupyter protocol implementation and client libraries" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_client-8.6.0-py3-none-any.whl", hash = "sha256:909c474dbe62582ae62b758bca86d6518c85234bdee2d908c778db6d72f39d99"}, + {file = "jupyter_client-8.6.0.tar.gz", hash = "sha256:0642244bb83b4764ae60d07e010e15f0e2d275ec4e918a8f7b80fbbef3ca60c7"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +python-dateutil = ">=2.8.2" +pyzmq = ">=23.0" +tornado = ">=6.2" +traitlets = ">=5.3" + +[package.extras] +docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] +test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-commit", "pytest", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"] + +[[package]] +name = "jupyter-core" +version = "5.5.1" +description = "Jupyter core package. A base package on which Jupyter projects rely." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_core-5.5.1-py3-none-any.whl", hash = "sha256:220dfb00c45f0d780ce132bb7976b58263f81a3ada6e90a9b6823785a424f739"}, + {file = "jupyter_core-5.5.1.tar.gz", hash = "sha256:1553311a97ccd12936037f36b9ab4d6ae8ceea6ad2d5c90d94a909e752178e40"}, +] + +[package.dependencies] +platformdirs = ">=2.5" +pywin32 = {version = ">=300", markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""} +traitlets = ">=5.3" + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "traitlets"] +test = ["ipykernel", "pre-commit", "pytest", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "jupyter-events" +version = "0.9.0" +description = "Jupyter Event System library" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_events-0.9.0-py3-none-any.whl", hash = "sha256:d853b3c10273ff9bc8bb8b30076d65e2c9685579db736873de6c2232dde148bf"}, + {file = "jupyter_events-0.9.0.tar.gz", hash = "sha256:81ad2e4bc710881ec274d31c6c50669d71bbaa5dd9d01e600b56faa85700d399"}, +] + +[package.dependencies] +jsonschema = {version = ">=4.18.0", extras = ["format-nongpl"]} +python-json-logger = ">=2.0.4" +pyyaml = ">=5.3" +referencing = "*" +rfc3339-validator = "*" +rfc3986-validator = ">=0.1.1" +traitlets = ">=5.3" + +[package.extras] +cli = ["click", "rich"] +docs = ["jupyterlite-sphinx", "myst-parser", "pydata-sphinx-theme", "sphinxcontrib-spelling"] +test = ["click", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.19.0)", "pytest-console-scripts", "rich"] + +[[package]] +name = "jupyter-lsp" +version = "2.2.1" +description = "Multi-Language Server WebSocket proxy for Jupyter Notebook/Lab server" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter-lsp-2.2.1.tar.gz", hash = "sha256:b17fab6d70fe83c8896b0cff59237640038247c196056b43684a0902b6a9e0fb"}, + {file = "jupyter_lsp-2.2.1-py3-none-any.whl", hash = "sha256:17a689910c5e4ae5e7d334b02f31d08ffbe98108f6f658fb05e4304b4345368b"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} +jupyter-server = ">=1.1.2" + +[[package]] +name = "jupyter-server" +version = "2.12.1" +description = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_server-2.12.1-py3-none-any.whl", hash = "sha256:fd030dd7be1ca572e4598203f718df6630c12bd28a599d7f1791c4d7938e1010"}, + {file = "jupyter_server-2.12.1.tar.gz", hash = "sha256:dc77b7dcc5fc0547acba2b2844f01798008667201eea27c6319ff9257d700a6d"}, +] + +[package.dependencies] +anyio = ">=3.1.0" +argon2-cffi = "*" +jinja2 = "*" +jupyter-client = ">=7.4.4" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +jupyter-events = ">=0.9.0" +jupyter-server-terminals = "*" +nbconvert = ">=6.4.4" +nbformat = ">=5.3.0" +overrides = "*" +packaging = "*" +prometheus-client = "*" +pywinpty = {version = "*", markers = "os_name == \"nt\""} +pyzmq = ">=24" +send2trash = ">=1.8.2" +terminado = ">=0.8.3" +tornado = ">=6.2.0" +traitlets = ">=5.6.0" +websocket-client = "*" + +[package.extras] +docs = ["ipykernel", "jinja2", "jupyter-client", "jupyter-server", "myst-parser", "nbformat", "prometheus-client", "pydata-sphinx-theme", "send2trash", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-openapi (>=0.8.0)", "sphinxcontrib-spelling", "sphinxemoji", "tornado", "typing-extensions"] +test = ["flaky", "ipykernel", "pre-commit", "pytest (>=7.0)", "pytest-console-scripts", "pytest-jupyter[server] (>=0.4)", "pytest-timeout", "requests"] + +[[package]] +name = "jupyter-server-terminals" +version = "0.5.1" +description = "A Jupyter Server Extension Providing Terminals." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_server_terminals-0.5.1-py3-none-any.whl", hash = "sha256:5e63e947ddd97bb2832db5ef837a258d9ccd4192cd608c1270850ad947ae5dd7"}, + {file = "jupyter_server_terminals-0.5.1.tar.gz", hash = "sha256:16d3be9cf48be6a1f943f3a6c93c033be259cf4779184c66421709cf63dccfea"}, +] + +[package.dependencies] +pywinpty = {version = ">=2.0.3", markers = "os_name == \"nt\""} +terminado = ">=0.8.3" + +[package.extras] +docs = ["jinja2", "jupyter-server", "mistune (<4.0)", "myst-parser", "nbformat", "packaging", "pydata-sphinx-theme", "sphinxcontrib-github-alt", "sphinxcontrib-openapi", "sphinxcontrib-spelling", "sphinxemoji", "tornado"] +test = ["jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-jupyter[server] (>=0.5.3)", "pytest-timeout"] + +[[package]] +name = "jupyterlab" +version = "4.0.10" +description = "JupyterLab computational environment" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyterlab-4.0.10-py3-none-any.whl", hash = "sha256:fe010ad9e37017488b468632ef2ead255fc7c671c5b64d9ca13e1f7b7e665c37"}, + {file = "jupyterlab-4.0.10.tar.gz", hash = "sha256:46177eb8ede70dc73be922ac99f8ef943bdc2dfbc6a31b353c4bde848a35dee1"}, +] + +[package.dependencies] +async-lru = ">=1.0.0" +importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} +ipykernel = "*" +jinja2 = ">=3.0.3" +jupyter-core = "*" +jupyter-lsp = ">=2.0.0" +jupyter-server = ">=2.4.0,<3" +jupyterlab-server = ">=2.19.0,<3" +notebook-shim = ">=0.2" +packaging = "*" +tomli = {version = "*", markers = "python_version < \"3.11\""} +tornado = ">=6.2.0" +traitlets = "*" + +[package.extras] +dev = ["build", "bump2version", "coverage", "hatch", "pre-commit", "pytest-cov", "ruff (==0.1.6)"] +docs = ["jsx-lexer", "myst-parser", "pydata-sphinx-theme (>=0.13.0)", "pytest", "pytest-check-links", "pytest-tornasync", "sphinx (>=1.8,<7.2.0)", "sphinx-copybutton"] +docs-screenshots = ["altair (==5.0.1)", "ipython (==8.14.0)", "ipywidgets (==8.0.6)", "jupyterlab-geojson (==3.4.0)", "jupyterlab-language-pack-zh-cn (==4.0.post0)", "matplotlib (==3.7.1)", "nbconvert (>=7.0.0)", "pandas (==2.0.2)", "scipy (==1.10.1)", "vega-datasets (==0.9.0)"] +test = ["coverage", "pytest (>=7.0)", "pytest-check-links (>=0.7)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter (>=0.5.3)", "pytest-timeout", "pytest-tornasync", "requests", "requests-cache", "virtualenv"] + +[[package]] +name = "jupyterlab-pygments" +version = "0.3.0" +description = "Pygments theme using JupyterLab CSS variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780"}, + {file = "jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d"}, +] + +[[package]] +name = "jupyterlab-server" +version = "2.25.2" +description = "A set of server components for JupyterLab and JupyterLab like applications." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyterlab_server-2.25.2-py3-none-any.whl", hash = "sha256:5b1798c9cc6a44f65c757de9f97fc06fc3d42535afbf47d2ace5e964ab447aaf"}, + {file = "jupyterlab_server-2.25.2.tar.gz", hash = "sha256:bd0ec7a99ebcedc8bcff939ef86e52c378e44c2707e053fcd81d046ce979ee63"}, +] + +[package.dependencies] +babel = ">=2.10" +importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} +jinja2 = ">=3.0.3" +json5 = ">=0.9.0" +jsonschema = ">=4.18.0" +jupyter-server = ">=1.21,<3" +packaging = ">=21.3" +requests = ">=2.31" + +[package.extras] +docs = ["autodoc-traits", "jinja2 (<3.2.0)", "mistune (<4)", "myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-copybutton", "sphinxcontrib-openapi (>0.8)"] +openapi = ["openapi-core (>=0.18.0,<0.19.0)", "ruamel-yaml"] +test = ["hatch", "ipykernel", "openapi-core (>=0.18.0,<0.19.0)", "openapi-spec-validator (>=0.6.0,<0.8.0)", "pytest (>=7.0)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter[server] (>=0.6.2)", "pytest-timeout", "requests-mock", "ruamel-yaml", "sphinxcontrib-spelling", "strict-rfc3339", "werkzeug"] + +[[package]] +name = "keyring" +version = "24.3.0" +description = "Store and access your passwords safely." +optional = false +python-versions = ">=3.8" +files = [ + {file = "keyring-24.3.0-py3-none-any.whl", hash = "sha256:4446d35d636e6a10b8bce7caa66913dd9eca5fd222ca03a3d42c38608ac30836"}, + {file = "keyring-24.3.0.tar.gz", hash = "sha256:e730ecffd309658a08ee82535a3b5ec4b4c8669a9be11efb66249d8e0aeb9a25"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} +"jaraco.classes" = "*" +jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} +pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} +SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} + +[package.extras] +completion = ["shtab (>=1.1.0)"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"] + +[[package]] +name = "macholib" +version = "1.16.3" +description = "Mach-O header analysis and editing" +optional = false +python-versions = "*" +files = [ + {file = "macholib-1.16.3-py2.py3-none-any.whl", hash = "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c"}, + {file = "macholib-1.16.3.tar.gz", hash = "sha256:07ae9e15e8e4cd9a788013d81f5908b3609aa76f9b1421bae9c4d7606ec86a30"}, +] + +[package.dependencies] +altgraph = ">=0.17" + +[[package]] +name = "markdown" +version = "3.5.1" +description = "Python implementation of John Gruber's Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Markdown-3.5.1-py3-none-any.whl", hash = "sha256:5874b47d4ee3f0b14d764324d2c94c03ea66bee56f2d929da9f2508d65e722dc"}, + {file = "Markdown-3.5.1.tar.gz", hash = "sha256:b65d7beb248dc22f2e8a31fb706d93798093c308dc1aba295aedeb9d41a813bd"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} + +[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.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, + {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, +] + +[[package]] +name = "matplotlib-inline" +version = "0.1.6" +description = "Inline Matplotlib backend for Jupyter" +optional = false +python-versions = ">=3.5" +files = [ + {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"}, + {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"}, +] + +[package.dependencies] +traitlets = "*" + +[[package]] +name = "mistune" +version = "3.0.2" +description = "A sane and fast Markdown parser with useful plugins and renderers" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mistune-3.0.2-py3-none-any.whl", hash = "sha256:71481854c30fdbc938963d3605b72501f5c10a9320ecd412c121c163a1c7d205"}, + {file = "mistune-3.0.2.tar.gz", hash = "sha256:fc7f93ded930c92394ef2cb6f04a8aabab4117a91449e72dcc8dfa646a508be8"}, +] + +[[package]] +name = "more-itertools" +version = "10.1.0" +description = "More routines for operating on iterables, beyond itertools" +optional = false +python-versions = ">=3.8" +files = [ + {file = "more-itertools-10.1.0.tar.gz", hash = "sha256:626c369fa0eb37bac0291bce8259b332fd59ac792fa5497b59837309cd5b114a"}, + {file = "more_itertools-10.1.0-py3-none-any.whl", hash = "sha256:64e0735fcfdc6f3464ea133afe8ea4483b1c5fe3a3d69852e6503b43a0b222e6"}, +] + +[[package]] +name = "msgpack" +version = "1.0.7" +description = "MessagePack serializer" +optional = false +python-versions = ">=3.8" +files = [ + {file = "msgpack-1.0.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04ad6069c86e531682f9e1e71b71c1c3937d6014a7c3e9edd2aa81ad58842862"}, + {file = "msgpack-1.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cca1b62fe70d761a282496b96a5e51c44c213e410a964bdffe0928e611368329"}, + {file = "msgpack-1.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e50ebce52f41370707f1e21a59514e3375e3edd6e1832f5e5235237db933c98b"}, + {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b4f35de6a304b5533c238bee86b670b75b03d31b7797929caa7a624b5dda6"}, + {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28efb066cde83c479dfe5a48141a53bc7e5f13f785b92ddde336c716663039ee"}, + {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cb14ce54d9b857be9591ac364cb08dc2d6a5c4318c1182cb1d02274029d590d"}, + {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b573a43ef7c368ba4ea06050a957c2a7550f729c31f11dd616d2ac4aba99888d"}, + {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ccf9a39706b604d884d2cb1e27fe973bc55f2890c52f38df742bc1d79ab9f5e1"}, + {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cb70766519500281815dfd7a87d3a178acf7ce95390544b8c90587d76b227681"}, + {file = "msgpack-1.0.7-cp310-cp310-win32.whl", hash = "sha256:b610ff0f24e9f11c9ae653c67ff8cc03c075131401b3e5ef4b82570d1728f8a9"}, + {file = "msgpack-1.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:a40821a89dc373d6427e2b44b572efc36a2778d3f543299e2f24eb1a5de65415"}, + {file = "msgpack-1.0.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:576eb384292b139821c41995523654ad82d1916da6a60cff129c715a6223ea84"}, + {file = "msgpack-1.0.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:730076207cb816138cf1af7f7237b208340a2c5e749707457d70705715c93b93"}, + {file = "msgpack-1.0.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:85765fdf4b27eb5086f05ac0491090fc76f4f2b28e09d9350c31aac25a5aaff8"}, + {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3476fae43db72bd11f29a5147ae2f3cb22e2f1a91d575ef130d2bf49afd21c46"}, + {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d4c80667de2e36970ebf74f42d1088cc9ee7ef5f4e8c35eee1b40eafd33ca5b"}, + {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b0bf0effb196ed76b7ad883848143427a73c355ae8e569fa538365064188b8e"}, + {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f9a7c509542db4eceed3dcf21ee5267ab565a83555c9b88a8109dcecc4709002"}, + {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:84b0daf226913133f899ea9b30618722d45feffa67e4fe867b0b5ae83a34060c"}, + {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ec79ff6159dffcc30853b2ad612ed572af86c92b5168aa3fc01a67b0fa40665e"}, + {file = "msgpack-1.0.7-cp311-cp311-win32.whl", hash = "sha256:3e7bf4442b310ff154b7bb9d81eb2c016b7d597e364f97d72b1acc3817a0fdc1"}, + {file = "msgpack-1.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:3f0c8c6dfa6605ab8ff0611995ee30d4f9fcff89966cf562733b4008a3d60d82"}, + {file = "msgpack-1.0.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f0936e08e0003f66bfd97e74ee530427707297b0d0361247e9b4f59ab78ddc8b"}, + {file = "msgpack-1.0.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98bbd754a422a0b123c66a4c341de0474cad4a5c10c164ceed6ea090f3563db4"}, + {file = "msgpack-1.0.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b291f0ee7961a597cbbcc77709374087fa2a9afe7bdb6a40dbbd9b127e79afee"}, + {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebbbba226f0a108a7366bf4b59bf0f30a12fd5e75100c630267d94d7f0ad20e5"}, + {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e2d69948e4132813b8d1131f29f9101bc2c915f26089a6d632001a5c1349672"}, + {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdf38ba2d393c7911ae989c3bbba510ebbcdf4ecbdbfec36272abe350c454075"}, + {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:993584fc821c58d5993521bfdcd31a4adf025c7d745bbd4d12ccfecf695af5ba"}, + {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:52700dc63a4676669b341ba33520f4d6e43d3ca58d422e22ba66d1736b0a6e4c"}, + {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e45ae4927759289c30ccba8d9fdce62bb414977ba158286b5ddaf8df2cddb5c5"}, + {file = "msgpack-1.0.7-cp312-cp312-win32.whl", hash = "sha256:27dcd6f46a21c18fa5e5deed92a43d4554e3df8d8ca5a47bf0615d6a5f39dbc9"}, + {file = "msgpack-1.0.7-cp312-cp312-win_amd64.whl", hash = "sha256:7687e22a31e976a0e7fc99c2f4d11ca45eff652a81eb8c8085e9609298916dcf"}, + {file = "msgpack-1.0.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5b6ccc0c85916998d788b295765ea0e9cb9aac7e4a8ed71d12e7d8ac31c23c95"}, + {file = "msgpack-1.0.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:235a31ec7db685f5c82233bddf9858748b89b8119bf4538d514536c485c15fe0"}, + {file = "msgpack-1.0.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cab3db8bab4b7e635c1c97270d7a4b2a90c070b33cbc00c99ef3f9be03d3e1f7"}, + {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bfdd914e55e0d2c9e1526de210f6fe8ffe9705f2b1dfcc4aecc92a4cb4b533d"}, + {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36e17c4592231a7dbd2ed09027823ab295d2791b3b1efb2aee874b10548b7524"}, + {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38949d30b11ae5f95c3c91917ee7a6b239f5ec276f271f28638dec9156f82cfc"}, + {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ff1d0899f104f3921d94579a5638847f783c9b04f2d5f229392ca77fba5b82fc"}, + {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:dc43f1ec66eb8440567186ae2f8c447d91e0372d793dfe8c222aec857b81a8cf"}, + {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dd632777ff3beaaf629f1ab4396caf7ba0bdd075d948a69460d13d44357aca4c"}, + {file = "msgpack-1.0.7-cp38-cp38-win32.whl", hash = "sha256:4e71bc4416de195d6e9b4ee93ad3f2f6b2ce11d042b4d7a7ee00bbe0358bd0c2"}, + {file = "msgpack-1.0.7-cp38-cp38-win_amd64.whl", hash = "sha256:8f5b234f567cf76ee489502ceb7165c2a5cecec081db2b37e35332b537f8157c"}, + {file = "msgpack-1.0.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfef2bb6ef068827bbd021017a107194956918ab43ce4d6dc945ffa13efbc25f"}, + {file = "msgpack-1.0.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:484ae3240666ad34cfa31eea7b8c6cd2f1fdaae21d73ce2974211df099a95d81"}, + {file = "msgpack-1.0.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3967e4ad1aa9da62fd53e346ed17d7b2e922cba5ab93bdd46febcac39be636fc"}, + {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dd178c4c80706546702c59529ffc005681bd6dc2ea234c450661b205445a34d"}, + {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6ffbc252eb0d229aeb2f9ad051200668fc3a9aaa8994e49f0cb2ffe2b7867e7"}, + {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:822ea70dc4018c7e6223f13affd1c5c30c0f5c12ac1f96cd8e9949acddb48a61"}, + {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:384d779f0d6f1b110eae74cb0659d9aa6ff35aaf547b3955abf2ab4c901c4819"}, + {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f64e376cd20d3f030190e8c32e1c64582eba56ac6dc7d5b0b49a9d44021b52fd"}, + {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ed82f5a7af3697b1c4786053736f24a0efd0a1b8a130d4c7bfee4b9ded0f08f"}, + {file = "msgpack-1.0.7-cp39-cp39-win32.whl", hash = "sha256:f26a07a6e877c76a88e3cecac8531908d980d3d5067ff69213653649ec0f60ad"}, + {file = "msgpack-1.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:1dc93e8e4653bdb5910aed79f11e165c85732067614f180f70534f056da97db3"}, + {file = "msgpack-1.0.7.tar.gz", hash = "sha256:572efc93db7a4d27e404501975ca6d2d9775705c2d922390d878fcf768d92c87"}, +] + +[[package]] +name = "nbclient" +version = "0.9.0" +description = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "nbclient-0.9.0-py3-none-any.whl", hash = "sha256:a3a1ddfb34d4a9d17fc744d655962714a866639acd30130e9be84191cd97cd15"}, + {file = "nbclient-0.9.0.tar.gz", hash = "sha256:4b28c207877cf33ef3a9838cdc7a54c5ceff981194a82eac59d558f05487295e"}, +] + +[package.dependencies] +jupyter-client = ">=6.1.12" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +nbformat = ">=5.1" +traitlets = ">=5.4" + +[package.extras] +dev = ["pre-commit"] +docs = ["autodoc-traits", "mock", "moto", "myst-parser", "nbclient[test]", "sphinx (>=1.7)", "sphinx-book-theme", "sphinxcontrib-spelling"] +test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>=7.0.0)", "pytest (>=7.0)", "pytest-asyncio", "pytest-cov (>=4.0)", "testpath", "xmltodict"] + +[[package]] +name = "nbconvert" +version = "7.13.1" +description = "Converting Jupyter Notebooks" +optional = false +python-versions = ">=3.8" +files = [ + {file = "nbconvert-7.13.1-py3-none-any.whl", hash = "sha256:3c50eb2d326478cc90b8759cf2ab9dde3d892c6537cd6a5bc0991db8ef734bcc"}, + {file = "nbconvert-7.13.1.tar.gz", hash = "sha256:2dc8267dbdfeedce2dcd34c9e3f1b51af18f43cb105549d1c5a18189ec23ba85"}, +] + +[package.dependencies] +beautifulsoup4 = "*" +bleach = "!=5.0.0" +defusedxml = "*" +importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} +jinja2 = ">=3.0" +jupyter-core = ">=4.7" +jupyterlab-pygments = "*" +markupsafe = ">=2.0" +mistune = ">=2.0.3,<4" +nbclient = ">=0.5.0" +nbformat = ">=5.7" +packaging = "*" +pandocfilters = ">=1.4.1" +pygments = ">=2.4.1" +tinycss2 = "*" +traitlets = ">=5.1" + +[package.extras] +all = ["nbconvert[docs,qtpdf,serve,test,webpdf]"] +docs = ["ipykernel", "ipython", "myst-parser", "nbsphinx (>=0.2.12)", "pydata-sphinx-theme", "sphinx (==5.0.2)", "sphinxcontrib-spelling"] +qtpdf = ["nbconvert[qtpng]"] +qtpng = ["pyqtwebengine (>=5.15)"] +serve = ["tornado (>=6.1)"] +test = ["flaky", "ipykernel", "ipywidgets (>=7.5)", "pytest"] +webpdf = ["playwright"] + +[[package]] +name = "nbformat" +version = "5.9.2" +description = "The Jupyter Notebook format" +optional = false +python-versions = ">=3.8" +files = [ + {file = "nbformat-5.9.2-py3-none-any.whl", hash = "sha256:1c5172d786a41b82bcfd0c23f9e6b6f072e8fb49c39250219e4acfff1efe89e9"}, + {file = "nbformat-5.9.2.tar.gz", hash = "sha256:5f98b5ba1997dff175e77e0c17d5c10a96eaed2cbd1de3533d1fc35d5e111192"}, +] + +[package.dependencies] +fastjsonschema = "*" +jsonschema = ">=2.6" +jupyter-core = "*" +traitlets = ">=5.1" + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] +test = ["pep440", "pre-commit", "pytest", "testpath"] + +[[package]] +name = "nest-asyncio" +version = "1.5.8" +description = "Patch asyncio to allow nested event loops" +optional = false +python-versions = ">=3.5" +files = [ + {file = "nest_asyncio-1.5.8-py3-none-any.whl", hash = "sha256:accda7a339a70599cb08f9dd09a67e0c2ef8d8d6f4c07f96ab203f2ae254e48d"}, + {file = "nest_asyncio-1.5.8.tar.gz", hash = "sha256:25aa2ca0d2a5b5531956b9e273b45cf664cae2b145101d73b86b199978d48fdb"}, +] + +[[package]] +name = "nodeenv" +version = "1.8.0" description = "Node.js virtual environment builder" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" @@ -475,6 +1815,46 @@ files = [ [package.dependencies] setuptools = "*" +[[package]] +name = "notebook" +version = "7.0.6" +description = "Jupyter Notebook - A web-based notebook environment for interactive computing" +optional = false +python-versions = ">=3.8" +files = [ + {file = "notebook-7.0.6-py3-none-any.whl", hash = "sha256:0fe8f67102fea3744fedf652e4c15339390902ca70c5a31c4f547fa23da697cc"}, + {file = "notebook-7.0.6.tar.gz", hash = "sha256:ec6113b06529019f7f287819af06c97a2baf7a95ac21a8f6e32192898e9f9a58"}, +] + +[package.dependencies] +jupyter-server = ">=2.4.0,<3" +jupyterlab = ">=4.0.2,<5" +jupyterlab-server = ">=2.22.1,<3" +notebook-shim = ">=0.2,<0.3" +tornado = ">=6.2.0" + +[package.extras] +dev = ["hatch", "pre-commit"] +docs = ["myst-parser", "nbsphinx", "pydata-sphinx-theme", "sphinx (>=1.3.6)", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] +test = ["importlib-resources (>=5.0)", "ipykernel", "jupyter-server[test] (>=2.4.0,<3)", "jupyterlab-server[test] (>=2.22.1,<3)", "nbval", "pytest (>=7.0)", "pytest-console-scripts", "pytest-timeout", "pytest-tornasync", "requests"] + +[[package]] +name = "notebook-shim" +version = "0.2.3" +description = "A shim layer for notebook traits and config" +optional = false +python-versions = ">=3.7" +files = [ + {file = "notebook_shim-0.2.3-py3-none-any.whl", hash = "sha256:a83496a43341c1674b093bfcebf0fe8e74cbe7eda5fd2bbc56f8e39e1486c0c7"}, + {file = "notebook_shim-0.2.3.tar.gz", hash = "sha256:f69388ac283ae008cd506dda10d0288b09a017d822d5e8c7129a152cbd3ce7e9"}, +] + +[package.dependencies] +jupyter-server = ">=1.8,<3" + +[package.extras] +test = ["pytest", "pytest-console-scripts", "pytest-jupyter", "pytest-tornasync"] + [[package]] name = "oauthlib" version = "3.2.2" @@ -491,17 +1871,54 @@ rsa = ["cryptography (>=3.0.0)"] signals = ["blinker (>=1.4.0)"] signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] +[[package]] +name = "overrides" +version = "7.4.0" +description = "A decorator to automatically detect mismatch when overriding a method." +optional = false +python-versions = ">=3.6" +files = [ + {file = "overrides-7.4.0-py3-none-any.whl", hash = "sha256:3ad24583f86d6d7a49049695efe9933e67ba62f0c7625d53c59fa832ce4b8b7d"}, + {file = "overrides-7.4.0.tar.gz", hash = "sha256:9502a3cca51f4fac40b5feca985b6703a5c1f6ad815588a7ca9e285b9dca6757"}, +] + [[package]] name = "packaging" -version = "23.1" +version = "23.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, - {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pandocfilters" +version = "1.5.0" +description = "Utilities for writing pandoc filters in python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pandocfilters-1.5.0-py2.py3-none-any.whl", hash = "sha256:33aae3f25fd1a026079f5d27bdd52496f0e0803b3469282162bafdcbdf6ef14f"}, + {file = "pandocfilters-1.5.0.tar.gz", hash = "sha256:0b679503337d233b4339a817bfc8c50064e2eff681314376a47cb582305a7a38"}, ] +[[package]] +name = "parso" +version = "0.8.3" +description = "A Python Parser" +optional = false +python-versions = ">=3.6" +files = [ + {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, + {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, +] + +[package.extras] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["docopt", "pytest (<6.0.0)"] + [[package]] name = "pefile" version = "2023.2.7" @@ -513,78 +1930,247 @@ files = [ {file = "pefile-2023.2.7.tar.gz", hash = "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc"}, ] +[[package]] +name = "pexpect" +version = "4.9.0" +description = "Pexpect allows easy control of interactive console applications." +optional = false +python-versions = "*" +files = [ + {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, + {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, +] + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +name = "pkginfo" +version = "1.9.6" +description = "Query metadata from sdists / bdists / installed packages." +optional = false +python-versions = ">=3.6" +files = [ + {file = "pkginfo-1.9.6-py3-none-any.whl", hash = "sha256:4b7a555a6d5a22169fcc9cf7bfd78d296b0361adad412a346c1226849af5e546"}, + {file = "pkginfo-1.9.6.tar.gz", hash = "sha256:8fd5896e8718a4372f0ea9cc9d96f6417c9b986e23a4d116dda26b62cc29d046"}, +] + +[package.extras] +testing = ["pytest", "pytest-cov"] + [[package]] name = "platformdirs" -version = "3.9.1" +version = "3.11.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-3.9.1-py3-none-any.whl", hash = "sha256:ad8291ae0ae5072f66c16945166cb11c63394c7a3ad1b1bc9828ca3162da8c2f"}, - {file = "platformdirs-3.9.1.tar.gz", hash = "sha256:1b42b450ad933e981d56e59f1b97495428c9bd60698baab9f3eb3d00d5822421"}, + {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, + {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, ] [package.extras] -docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] [[package]] name = "pluggy" -version = "1.2.0" +version = "1.3.0" description = "plugin and hook calling mechanisms for python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "poetry" +version = "1.7.1" +description = "Python dependency management and packaging made easy." +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "poetry-1.7.1-py3-none-any.whl", hash = "sha256:03d3807a0fb3bc1028cc3707dfd646aae629d58e476f7e7f062437680741c561"}, + {file = "poetry-1.7.1.tar.gz", hash = "sha256:b348a70e7d67ad9c0bd3d0ea255bc6df84c24cf4b16f8d104adb30b425d6ff32"}, +] + +[package.dependencies] +build = ">=1.0.3,<2.0.0" +cachecontrol = {version = ">=0.13.0,<0.14.0", extras = ["filecache"]} +cleo = ">=2.1.0,<3.0.0" +crashtest = ">=0.4.1,<0.5.0" +dulwich = ">=0.21.2,<0.22.0" +fastjsonschema = ">=2.18.0,<3.0.0" +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} +installer = ">=0.7.0,<0.8.0" +keyring = ">=24.0.0,<25.0.0" +packaging = ">=20.5" +pexpect = ">=4.7.0,<5.0.0" +pkginfo = ">=1.9.4,<2.0.0" +platformdirs = ">=3.0.0,<4.0.0" +poetry-core = "1.8.1" +poetry-plugin-export = ">=1.6.0,<2.0.0" +pyproject-hooks = ">=1.0.0,<2.0.0" +requests = ">=2.26,<3.0" +requests-toolbelt = ">=0.9.1,<2" +shellingham = ">=1.5,<2.0" +tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""} +tomlkit = ">=0.11.4,<1.0.0" +trove-classifiers = ">=2022.5.19" +virtualenv = ">=20.23.0,<21.0.0" +xattr = {version = ">=0.10.0,<0.11.0", markers = "sys_platform == \"darwin\""} + +[[package]] +name = "poetry-core" +version = "1.8.1" +description = "Poetry PEP 517 Build Backend" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "poetry_core-1.8.1-py3-none-any.whl", hash = "sha256:194832b24f3283e01c5402eae71a6aae850ecdfe53f50a979c76bf7aa5010ffa"}, + {file = "poetry_core-1.8.1.tar.gz", hash = "sha256:67a76c671da2a70e55047cddda83566035b701f7e463b32a2abfeac6e2a16376"}, +] + +[[package]] +name = "poetry-plugin-export" +version = "1.6.0" +description = "Poetry plugin to export the dependencies to various formats" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "poetry_plugin_export-1.6.0-py3-none-any.whl", hash = "sha256:2dce6204c9318f1f6509a11a03921fb3f461b201840b59f1c237b6ab454dabcf"}, + {file = "poetry_plugin_export-1.6.0.tar.gz", hash = "sha256:091939434984267a91abf2f916a26b00cff4eee8da63ec2a24ba4b17cf969a59"}, +] + +[package.dependencies] +poetry = ">=1.6.0,<2.0.0" +poetry-core = ">=1.7.0,<2.0.0" + +[[package]] +name = "pre-commit" +version = "3.6.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pre_commit-3.6.0-py2.py3-none-any.whl", hash = "sha256:c255039ef399049a5544b6ce13d135caba8f2c28c3b4033277a788f434308376"}, + {file = "pre_commit-3.6.0.tar.gz", hash = "sha256:d30bad9abf165f7785c15a21a1f46da7d0677cb00ee7ff4c579fd38922efe15d"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "progress" +version = "1.6" +description = "Easy to use progress bars" +optional = false +python-versions = "*" +files = [ + {file = "progress-1.6.tar.gz", hash = "sha256:c9c86e98b5c03fa1fe11e3b67c1feda4788b8d0fe7336c2ff7d5644ccfba34cd"}, +] + +[[package]] +name = "prometheus-client" +version = "0.19.0" +description = "Python client for the Prometheus monitoring system." +optional = false +python-versions = ">=3.8" +files = [ + {file = "prometheus_client-0.19.0-py3-none-any.whl", hash = "sha256:c88b1e6ecf6b41cd8fb5731c7ae919bf66df6ec6fafa555cd6c0e16ca169ae92"}, + {file = "prometheus_client-0.19.0.tar.gz", hash = "sha256:4585b0d1223148c27a225b10dbec5ae9bc4c81a99a3fa80774fa6209935324e1"}, +] + +[package.extras] +twisted = ["twisted"] + +[[package]] +name = "prompt-toolkit" +version = "3.0.43" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "prompt_toolkit-3.0.43-py3-none-any.whl", hash = "sha256:a11a29cb3bf0a28a387fe5122cdb649816a957cd9261dcedf8c9f1fef33eacf6"}, + {file = "prompt_toolkit-3.0.43.tar.gz", hash = "sha256:3527b7af26106cbc65a040bcc84839a3566ec1b051bb0bfe953631e704b0ff7d"}, +] + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "psutil" +version = "5.9.7" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ - {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, - {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, + {file = "psutil-5.9.7-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0bd41bf2d1463dfa535942b2a8f0e958acf6607ac0be52265ab31f7923bcd5e6"}, + {file = "psutil-5.9.7-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:5794944462509e49d4d458f4dbfb92c47539e7d8d15c796f141f474010084056"}, + {file = "psutil-5.9.7-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:fe361f743cb3389b8efda21980d93eb55c1f1e3898269bc9a2a1d0bb7b1f6508"}, + {file = "psutil-5.9.7-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:e469990e28f1ad738f65a42dcfc17adaed9d0f325d55047593cb9033a0ab63df"}, + {file = "psutil-5.9.7-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:3c4747a3e2ead1589e647e64aad601981f01b68f9398ddf94d01e3dc0d1e57c7"}, + {file = "psutil-5.9.7-cp27-none-win32.whl", hash = "sha256:1d4bc4a0148fdd7fd8f38e0498639ae128e64538faa507df25a20f8f7fb2341c"}, + {file = "psutil-5.9.7-cp27-none-win_amd64.whl", hash = "sha256:4c03362e280d06bbbfcd52f29acd79c733e0af33d707c54255d21029b8b32ba6"}, + {file = "psutil-5.9.7-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ea36cc62e69a13ec52b2f625c27527f6e4479bca2b340b7a452af55b34fcbe2e"}, + {file = "psutil-5.9.7-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1132704b876e58d277168cd729d64750633d5ff0183acf5b3c986b8466cd0284"}, + {file = "psutil-5.9.7-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe8b7f07948f1304497ce4f4684881250cd859b16d06a1dc4d7941eeb6233bfe"}, + {file = "psutil-5.9.7-cp36-cp36m-win32.whl", hash = "sha256:b27f8fdb190c8c03914f908a4555159327d7481dac2f01008d483137ef3311a9"}, + {file = "psutil-5.9.7-cp36-cp36m-win_amd64.whl", hash = "sha256:44969859757f4d8f2a9bd5b76eba8c3099a2c8cf3992ff62144061e39ba8568e"}, + {file = "psutil-5.9.7-cp37-abi3-win32.whl", hash = "sha256:c727ca5a9b2dd5193b8644b9f0c883d54f1248310023b5ad3e92036c5e2ada68"}, + {file = "psutil-5.9.7-cp37-abi3-win_amd64.whl", hash = "sha256:f37f87e4d73b79e6c5e749440c3113b81d1ee7d26f21c19c47371ddea834f414"}, + {file = "psutil-5.9.7-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:032f4f2c909818c86cea4fe2cc407f1c0f0cde8e6c6d702b28b8ce0c0d143340"}, + {file = "psutil-5.9.7.tar.gz", hash = "sha256:3f02134e82cfb5d089fddf20bb2e03fd5cd52395321d1c8458a9e58500ff417c"}, ] [package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] +test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] [[package]] -name = "pre-commit" -version = "3.3.3" -description = "A framework for managing and maintaining multi-language pre-commit hooks." +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" optional = false -python-versions = ">=3.8" +python-versions = "*" files = [ - {file = "pre_commit-3.3.3-py2.py3-none-any.whl", hash = "sha256:10badb65d6a38caff29703362271d7dca483d01da88f9d7e05d0b97171c136cb"}, - {file = "pre_commit-3.3.3.tar.gz", hash = "sha256:a2256f489cd913d575c145132ae196fe335da32d91a8294b7afe6622335dd023"}, + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, ] -[package.dependencies] -cfgv = ">=2.0.0" -identify = ">=1.0.0" -nodeenv = ">=0.11.1" -pyyaml = ">=5.1" -virtualenv = ">=20.10.0" - [[package]] -name = "progress" -version = "1.6" -description = "Easy to use progress bars" +name = "pure-eval" +version = "0.2.2" +description = "Safely evaluate AST nodes without side effects" optional = false python-versions = "*" files = [ - {file = "progress-1.6.tar.gz", hash = "sha256:c9c86e98b5c03fa1fe11e3b67c1feda4788b8d0fe7336c2ff7d5644ccfba34cd"}, + {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, + {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, ] +[package.extras] +tests = ["pytest"] + [[package]] -name = "prometheus-client" -version = "0.17.1" -description = "Python client for the Prometheus monitoring system." +name = "pycparser" +version = "2.21" +description = "C parser in Python" optional = false -python-versions = ">=3.6" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ - {file = "prometheus_client-0.17.1-py3-none-any.whl", hash = "sha256:e537f37160f6807b8202a6fc4764cdd19bac5480ddd3e0d463c3002b34462101"}, - {file = "prometheus_client-0.17.1.tar.gz", hash = "sha256:21e674f39831ae3f8acde238afd9a27a37d0d2fb5a28ea094f0ce25d2cbf2091"}, + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] -[package.extras] -twisted = ["twisted"] - [[package]] name = "pycron" version = "3.0.0" @@ -595,59 +2181,90 @@ files = [ {file = "pycron-3.0.0.tar.gz", hash = "sha256:b916044e3e8253d5409c68df3ac64a3472c4e608dab92f40e8f595e5d3acb3de"}, ] +[[package]] +name = "pygments" +version = "2.17.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, +] + +[package.extras] +plugins = ["importlib-metadata"] +windows-terminal = ["colorama (>=0.4.6)"] + [[package]] name = "pyinstaller" -version = "5.13.0" +version = "6.3.0" description = "PyInstaller bundles a Python application and all its dependencies into a single package." optional = false -python-versions = "<3.13,>=3.7" +python-versions = "<3.13,>=3.8" files = [ - {file = "pyinstaller-5.13.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:7fdd319828de679f9c5e381eff998ee9b4164bf4457e7fca56946701cf002c3f"}, - {file = "pyinstaller-5.13.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0df43697c4914285ecd333be968d2cd042ab9b2670124879ee87931d2344eaf5"}, - {file = "pyinstaller-5.13.0-py3-none-manylinux2014_i686.whl", hash = "sha256:28d9742c37e9fb518444b12f8c8ab3cb4ba212d752693c34475c08009aa21ccf"}, - {file = "pyinstaller-5.13.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e5fb17de6c325d3b2b4ceaeb55130ad7100a79096490e4c5b890224406fa42f4"}, - {file = "pyinstaller-5.13.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:78975043edeb628e23a73fb3ef0a273cda50e765f1716f75212ea3e91b09dede"}, - {file = "pyinstaller-5.13.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:cd7d5c06f2847195a23d72ede17c60857d6f495d6f0727dc6c9bc1235f2eb79c"}, - {file = "pyinstaller-5.13.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:24009eba63cfdbcde6d2634e9c87f545eb67249ddf3b514e0cd3b2cdaa595828"}, - {file = "pyinstaller-5.13.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:1fde4381155f21d6354dc450dcaa338cd8a40aaacf6bd22b987b0f3e1f96f3ee"}, - {file = "pyinstaller-5.13.0-py3-none-win32.whl", hash = "sha256:2d03419904d1c25c8968b0ad21da0e0f33d8d65716e29481b5bd83f7f342b0c5"}, - {file = "pyinstaller-5.13.0-py3-none-win_amd64.whl", hash = "sha256:9fc27c5a853b14a90d39c252707673c7a0efec921cd817169aff3af0fca8c127"}, - {file = "pyinstaller-5.13.0-py3-none-win_arm64.whl", hash = "sha256:3a331951f9744bc2379ea5d65d36f3c828eaefe2785f15039592cdc08560b262"}, - {file = "pyinstaller-5.13.0.tar.gz", hash = "sha256:5e446df41255e815017d96318e39f65a3eb807e74a796c7e7ff7f13b6366a2e9"}, + {file = "pyinstaller-6.3.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:75a6f2a6f835a2e6e0899d10e60c10caf5defd25aced38b1dd48fbbabc89de07"}, + {file = "pyinstaller-6.3.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:de25beb176f73a944758553caacec46cc665bf3910ad8a174706d79cf6e95340"}, + {file = "pyinstaller-6.3.0-py3-none-manylinux2014_i686.whl", hash = "sha256:e436fcc0ea87c3f132baac916d508c24c84a8f6d8a06c3154fbc753f169b76c7"}, + {file = "pyinstaller-6.3.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:b721d793a33b6d9946c7dd95d3ea7589c0424b51cf1b9fe580f03c544f1336b2"}, + {file = "pyinstaller-6.3.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:96c37a1ee5b2fd5bb25c098ef510661d6d17b6515d0b86d8fc93727dd2475ba3"}, + {file = "pyinstaller-6.3.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:abe91106a3bbccc3f3a27af4325676ecdb6f46cb842ac663625002a870fc503b"}, + {file = "pyinstaller-6.3.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:41c937fe8f07ae02009b3b5a96ac3eb0800a4f8a97af142d4100060fe2135bb9"}, + {file = "pyinstaller-6.3.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:886b3b995b674905a20ad5b720b47cc395897d7b391117831027a4c8c5d67a58"}, + {file = "pyinstaller-6.3.0-py3-none-win32.whl", hash = "sha256:0597fb04337695e5cc5250253e0655530bf14f264b7a5b7d219cc65f6889c4bd"}, + {file = "pyinstaller-6.3.0-py3-none-win_amd64.whl", hash = "sha256:156b32ba943e0090bcc68e40ae1cb68fd92b7f1ab6fe0bdf8faf3d3cfc4e12dd"}, + {file = "pyinstaller-6.3.0-py3-none-win_arm64.whl", hash = "sha256:1eadbd1fae84e2e6c678d8b4ed6a232ec5c8fe3a839aea5a3071c4c0282f98cc"}, + {file = "pyinstaller-6.3.0.tar.gz", hash = "sha256:914d4c96cc99472e37ac552fdd82fbbe09e67bb592d0717fcffaa99ea74273df"}, ] [package.dependencies] altgraph = "*" +importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} +packaging = ">=22.0" pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""} pyinstaller-hooks-contrib = ">=2021.4" pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""} setuptools = ">=42.0.0" [package.extras] -encryption = ["tinyaes (>=1.0.0)"] +completion = ["argcomplete"] hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] [[package]] name = "pyinstaller-hooks-contrib" -version = "2023.6" +version = "2023.11" description = "Community maintained hooks for PyInstaller" optional = false python-versions = ">=3.7" files = [ - {file = "pyinstaller-hooks-contrib-2023.6.tar.gz", hash = "sha256:596a72009d8692b043e0acbf5e1b476d93149900142ba01845dded91a0770cb5"}, - {file = "pyinstaller_hooks_contrib-2023.6-py2.py3-none-any.whl", hash = "sha256:aa6d7d038814df6aa7bec7bdbebc7cb4c693d3398df858f6062957f0797d397b"}, + {file = "pyinstaller-hooks-contrib-2023.11.tar.gz", hash = "sha256:5dd7a8a054a65c19cdaa381cabcfbe76f44d5f88d18214b0c570a0cd139be77f"}, + {file = "pyinstaller_hooks_contrib-2023.11-py2.py3-none-any.whl", hash = "sha256:f2a75dac2968ec81f92dcd3768906f654fa4204bc496126ae8483e87a5d89602"}, +] + +[[package]] +name = "pyproject-hooks" +version = "1.0.0" +description = "Wrappers to call pyproject.toml-based build backend hooks." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyproject_hooks-1.0.0-py3-none-any.whl", hash = "sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8"}, + {file = "pyproject_hooks-1.0.0.tar.gz", hash = "sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5"}, ] +[package.dependencies] +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} + [[package]] name = "pytest" -version = "7.4.0" +version = "7.4.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, - {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, ] [package.dependencies] @@ -681,13 +2298,13 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale [[package]] name = "pytest-mock" -version = "3.11.1" +version = "3.12.0" description = "Thin-wrapper around the mock package for easier use with pytest" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pytest-mock-3.11.1.tar.gz", hash = "sha256:7f6b125602ac6d743e523ae0bfa71e1a697a2f5534064528c6ff84c2f7c2fc7f"}, - {file = "pytest_mock-3.11.1-py3-none-any.whl", hash = "sha256:21c279fff83d70763b05f8874cc9cfb3fcacd6d354247a976f9529d19f9acf39"}, + {file = "pytest-mock-3.12.0.tar.gz", hash = "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"}, + {file = "pytest_mock-3.12.0-py3-none-any.whl", hash = "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f"}, ] [package.dependencies] @@ -696,6 +2313,31 @@ pytest = ">=5.0" [package.extras] dev = ["pre-commit", "pytest-asyncio", "tox"] +[[package]] +name = "python-dateutil" +version = "2.8.2" +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.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-json-logger" +version = "2.0.7" +description = "A python library adding a json log formatter" +optional = false +python-versions = ">=3.6" +files = [ + {file = "python-json-logger-2.0.7.tar.gz", hash = "sha256:23e7ec02d34237c5aa1e29a070193a4ea87583bb4e7f8fd06d3de8264c4b2e1c"}, + {file = "python_json_logger-2.0.7-py3-none-any.whl", hash = "sha256:f380b826a991ebbe3de4d897aeec42760035ac760345e57b812938dc8b35e2bd"}, +] + [[package]] name = "python-pushsafer" version = "1.1" @@ -712,36 +2354,51 @@ requests = ">=1.0" [[package]] name = "python-telegram-bot" -version = "13.15" +version = "20.7" description = "We have made you a wrapper you can't refuse" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "python-telegram-bot-13.15.tar.gz", hash = "sha256:b4047606b8081b62bbd6aa361f7ca1efe87fa8f1881ec9d932d35844bf57a154"}, - {file = "python_telegram_bot-13.15-py3-none-any.whl", hash = "sha256:06780c258d3f2a3c6c79a7aeb45714f4cd1dd6275941b7dc4628bba64fddd465"}, + {file = "python-telegram-bot-20.7.tar.gz", hash = "sha256:4f146c39de5f5e0b3723c2abedaf78046ebd30a6a49d2281ee4b3af5eb116b68"}, + {file = "python_telegram_bot-20.7-py3-none-any.whl", hash = "sha256:462326c65671c8c39e76c8c96756ee918be6797d225f8db84d2ec0f883383b8c"}, ] [package.dependencies] -APScheduler = "3.6.3" -cachetools = "4.2.2" -certifi = "*" -pytz = ">=2018.6" -tornado = "6.1" +cachetools = {version = ">=5.3.2,<5.4.0", optional = true, markers = "extra == \"callback-data\""} +httpx = ">=0.25.2,<0.26.0" [package.extras] -json = ["ujson"] -passport = ["cryptography (!=3.4,!=3.4.1,!=3.4.2,!=3.4.3)"] -socks = ["PySocks"] +all = ["APScheduler (>=3.10.4,<3.11.0)", "aiolimiter (>=1.1.0,<1.2.0)", "cachetools (>=5.3.2,<5.4.0)", "cryptography (>=39.0.1)", "httpx[http2]", "httpx[socks]", "pytz (>=2018.6)", "tornado (>=6.3.3,<6.4.0)"] +callback-data = ["cachetools (>=5.3.2,<5.4.0)"] +ext = ["APScheduler (>=3.10.4,<3.11.0)", "aiolimiter (>=1.1.0,<1.2.0)", "cachetools (>=5.3.2,<5.4.0)", "pytz (>=2018.6)", "tornado (>=6.3.3,<6.4.0)"] +http2 = ["httpx[http2]"] +job-queue = ["APScheduler (>=3.10.4,<3.11.0)", "pytz (>=2018.6)"] +passport = ["cryptography (>=39.0.1)"] +rate-limiter = ["aiolimiter (>=1.1.0,<1.2.0)"] +socks = ["httpx[socks]"] +webhooks = ["tornado (>=6.3.3,<6.4.0)"] [[package]] -name = "pytz" -version = "2023.3" -description = "World timezone definitions, modern and historical" +name = "pywin32" +version = "306" +description = "Python for Window Extensions" optional = false python-versions = "*" files = [ - {file = "pytz-2023.3-py2.py3-none-any.whl", hash = "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"}, - {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, + {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, + {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, + {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, + {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, + {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, + {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, + {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, + {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, + {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, + {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, + {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, + {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, + {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, + {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, ] [[package]] @@ -755,6 +2412,21 @@ files = [ {file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"}, ] +[[package]] +name = "pywinpty" +version = "2.0.12" +description = "Pseudo terminal support for Windows from Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pywinpty-2.0.12-cp310-none-win_amd64.whl", hash = "sha256:21319cd1d7c8844fb2c970fb3a55a3db5543f112ff9cfcd623746b9c47501575"}, + {file = "pywinpty-2.0.12-cp311-none-win_amd64.whl", hash = "sha256:853985a8f48f4731a716653170cd735da36ffbdc79dcb4c7b7140bce11d8c722"}, + {file = "pywinpty-2.0.12-cp312-none-win_amd64.whl", hash = "sha256:1617b729999eb6713590e17665052b1a6ae0ad76ee31e60b444147c5b6a35dca"}, + {file = "pywinpty-2.0.12-cp38-none-win_amd64.whl", hash = "sha256:189380469ca143d06e19e19ff3fba0fcefe8b4a8cc942140a6b863aed7eebb2d"}, + {file = "pywinpty-2.0.12-cp39-none-win_amd64.whl", hash = "sha256:7520575b6546db23e693cbd865db2764097bd6d4ef5dc18c92555904cd62c3d4"}, + {file = "pywinpty-2.0.12.tar.gz", hash = "sha256:8197de460ae8ebb7f5d1701dfa1b5df45b157bb832e92acba316305e18ca00dd"}, +] + [[package]] name = "pyyaml" version = "6.0.1" @@ -767,6 +2439,7 @@ files = [ {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"}, @@ -774,8 +2447,15 @@ files = [ {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_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"}, @@ -792,6 +2472,7 @@ files = [ {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"}, @@ -799,11 +2480,234 @@ files = [ {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 = "pyzmq" +version = "25.1.2" +description = "Python bindings for 0MQ" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pyzmq-25.1.2-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:e624c789359f1a16f83f35e2c705d07663ff2b4d4479bad35621178d8f0f6ea4"}, + {file = "pyzmq-25.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:49151b0efece79f6a79d41a461d78535356136ee70084a1c22532fc6383f4ad0"}, + {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9a5f194cf730f2b24d6af1f833c14c10f41023da46a7f736f48b6d35061e76e"}, + {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:faf79a302f834d9e8304fafdc11d0d042266667ac45209afa57e5efc998e3872"}, + {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f51a7b4ead28d3fca8dda53216314a553b0f7a91ee8fc46a72b402a78c3e43d"}, + {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0ddd6d71d4ef17ba5a87becf7ddf01b371eaba553c603477679ae817a8d84d75"}, + {file = "pyzmq-25.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:246747b88917e4867e2367b005fc8eefbb4a54b7db363d6c92f89d69abfff4b6"}, + {file = "pyzmq-25.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:00c48ae2fd81e2a50c3485de1b9d5c7c57cd85dc8ec55683eac16846e57ac979"}, + {file = "pyzmq-25.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5a68d491fc20762b630e5db2191dd07ff89834086740f70e978bb2ef2668be08"}, + {file = "pyzmq-25.1.2-cp310-cp310-win32.whl", hash = "sha256:09dfe949e83087da88c4a76767df04b22304a682d6154de2c572625c62ad6886"}, + {file = "pyzmq-25.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:fa99973d2ed20417744fca0073390ad65ce225b546febb0580358e36aa90dba6"}, + {file = "pyzmq-25.1.2-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:82544e0e2d0c1811482d37eef297020a040c32e0687c1f6fc23a75b75db8062c"}, + {file = "pyzmq-25.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:01171fc48542348cd1a360a4b6c3e7d8f46cdcf53a8d40f84db6707a6768acc1"}, + {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc69c96735ab501419c432110016329bf0dea8898ce16fab97c6d9106dc0b348"}, + {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3e124e6b1dd3dfbeb695435dff0e383256655bb18082e094a8dd1f6293114642"}, + {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7598d2ba821caa37a0f9d54c25164a4fa351ce019d64d0b44b45540950458840"}, + {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d1299d7e964c13607efd148ca1f07dcbf27c3ab9e125d1d0ae1d580a1682399d"}, + {file = "pyzmq-25.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4e6f689880d5ad87918430957297c975203a082d9a036cc426648fcbedae769b"}, + {file = "pyzmq-25.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cc69949484171cc961e6ecd4a8911b9ce7a0d1f738fcae717177c231bf77437b"}, + {file = "pyzmq-25.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9880078f683466b7f567b8624bfc16cad65077be046b6e8abb53bed4eeb82dd3"}, + {file = "pyzmq-25.1.2-cp311-cp311-win32.whl", hash = "sha256:4e5837af3e5aaa99a091302df5ee001149baff06ad22b722d34e30df5f0d9097"}, + {file = "pyzmq-25.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:25c2dbb97d38b5ac9fd15586e048ec5eb1e38f3d47fe7d92167b0c77bb3584e9"}, + {file = "pyzmq-25.1.2-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:11e70516688190e9c2db14fcf93c04192b02d457b582a1f6190b154691b4c93a"}, + {file = "pyzmq-25.1.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:313c3794d650d1fccaaab2df942af9f2c01d6217c846177cfcbc693c7410839e"}, + {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b3cbba2f47062b85fe0ef9de5b987612140a9ba3a9c6d2543c6dec9f7c2ab27"}, + {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc31baa0c32a2ca660784d5af3b9487e13b61b3032cb01a115fce6588e1bed30"}, + {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02c9087b109070c5ab0b383079fa1b5f797f8d43e9a66c07a4b8b8bdecfd88ee"}, + {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f8429b17cbb746c3e043cb986328da023657e79d5ed258b711c06a70c2ea7537"}, + {file = "pyzmq-25.1.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5074adeacede5f810b7ef39607ee59d94e948b4fd954495bdb072f8c54558181"}, + {file = "pyzmq-25.1.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7ae8f354b895cbd85212da245f1a5ad8159e7840e37d78b476bb4f4c3f32a9fe"}, + {file = "pyzmq-25.1.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b264bf2cc96b5bc43ce0e852be995e400376bd87ceb363822e2cb1964fcdc737"}, + {file = "pyzmq-25.1.2-cp312-cp312-win32.whl", hash = "sha256:02bbc1a87b76e04fd780b45e7f695471ae6de747769e540da909173d50ff8e2d"}, + {file = "pyzmq-25.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:ced111c2e81506abd1dc142e6cd7b68dd53747b3b7ae5edbea4578c5eeff96b7"}, + {file = "pyzmq-25.1.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7b6d09a8962a91151f0976008eb7b29b433a560fde056ec7a3db9ec8f1075438"}, + {file = "pyzmq-25.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:967668420f36878a3c9ecb5ab33c9d0ff8d054f9c0233d995a6d25b0e95e1b6b"}, + {file = "pyzmq-25.1.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5edac3f57c7ddaacdb4d40f6ef2f9e299471fc38d112f4bc6d60ab9365445fb0"}, + {file = "pyzmq-25.1.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0dabfb10ef897f3b7e101cacba1437bd3a5032ee667b7ead32bbcdd1a8422fe7"}, + {file = "pyzmq-25.1.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2c6441e0398c2baacfe5ba30c937d274cfc2dc5b55e82e3749e333aabffde561"}, + {file = "pyzmq-25.1.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:16b726c1f6c2e7625706549f9dbe9b06004dfbec30dbed4bf50cbdfc73e5b32a"}, + {file = "pyzmq-25.1.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:a86c2dd76ef71a773e70551a07318b8e52379f58dafa7ae1e0a4be78efd1ff16"}, + {file = "pyzmq-25.1.2-cp36-cp36m-win32.whl", hash = "sha256:359f7f74b5d3c65dae137f33eb2bcfa7ad9ebefd1cab85c935f063f1dbb245cc"}, + {file = "pyzmq-25.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:55875492f820d0eb3417b51d96fea549cde77893ae3790fd25491c5754ea2f68"}, + {file = "pyzmq-25.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b8c8a419dfb02e91b453615c69568442e897aaf77561ee0064d789705ff37a92"}, + {file = "pyzmq-25.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8807c87fa893527ae8a524c15fc505d9950d5e856f03dae5921b5e9aa3b8783b"}, + {file = "pyzmq-25.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5e319ed7d6b8f5fad9b76daa0a68497bc6f129858ad956331a5835785761e003"}, + {file = "pyzmq-25.1.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3c53687dde4d9d473c587ae80cc328e5b102b517447456184b485587ebd18b62"}, + {file = "pyzmq-25.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9add2e5b33d2cd765ad96d5eb734a5e795a0755f7fc49aa04f76d7ddda73fd70"}, + {file = "pyzmq-25.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e690145a8c0c273c28d3b89d6fb32c45e0d9605b2293c10e650265bf5c11cfec"}, + {file = "pyzmq-25.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:00a06faa7165634f0cac1abb27e54d7a0b3b44eb9994530b8ec73cf52e15353b"}, + {file = "pyzmq-25.1.2-cp37-cp37m-win32.whl", hash = "sha256:0f97bc2f1f13cb16905a5f3e1fbdf100e712d841482b2237484360f8bc4cb3d7"}, + {file = "pyzmq-25.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6cc0020b74b2e410287e5942e1e10886ff81ac77789eb20bec13f7ae681f0fdd"}, + {file = "pyzmq-25.1.2-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:bef02cfcbded83473bdd86dd8d3729cd82b2e569b75844fb4ea08fee3c26ae41"}, + {file = "pyzmq-25.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e10a4b5a4b1192d74853cc71a5e9fd022594573926c2a3a4802020360aa719d8"}, + {file = "pyzmq-25.1.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8c5f80e578427d4695adac6fdf4370c14a2feafdc8cb35549c219b90652536ae"}, + {file = "pyzmq-25.1.2-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5dde6751e857910c1339890f3524de74007958557593b9e7e8c5f01cd919f8a7"}, + {file = "pyzmq-25.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea1608dd169da230a0ad602d5b1ebd39807ac96cae1845c3ceed39af08a5c6df"}, + {file = "pyzmq-25.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0f513130c4c361201da9bc69df25a086487250e16b5571ead521b31ff6b02220"}, + {file = "pyzmq-25.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:019744b99da30330798bb37df33549d59d380c78e516e3bab9c9b84f87a9592f"}, + {file = "pyzmq-25.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2e2713ef44be5d52dd8b8e2023d706bf66cb22072e97fc71b168e01d25192755"}, + {file = "pyzmq-25.1.2-cp38-cp38-win32.whl", hash = "sha256:07cd61a20a535524906595e09344505a9bd46f1da7a07e504b315d41cd42eb07"}, + {file = "pyzmq-25.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb7e49a17fb8c77d3119d41a4523e432eb0c6932187c37deb6fbb00cc3028088"}, + {file = "pyzmq-25.1.2-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:94504ff66f278ab4b7e03e4cba7e7e400cb73bfa9d3d71f58d8972a8dc67e7a6"}, + {file = "pyzmq-25.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6dd0d50bbf9dca1d0bdea219ae6b40f713a3fb477c06ca3714f208fd69e16fd8"}, + {file = "pyzmq-25.1.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:004ff469d21e86f0ef0369717351073e0e577428e514c47c8480770d5e24a565"}, + {file = "pyzmq-25.1.2-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c0b5ca88a8928147b7b1e2dfa09f3b6c256bc1135a1338536cbc9ea13d3b7add"}, + {file = "pyzmq-25.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c9a79f1d2495b167119d02be7448bfba57fad2a4207c4f68abc0bab4b92925b"}, + {file = "pyzmq-25.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:518efd91c3d8ac9f9b4f7dd0e2b7b8bf1a4fe82a308009016b07eaa48681af82"}, + {file = "pyzmq-25.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1ec23bd7b3a893ae676d0e54ad47d18064e6c5ae1fadc2f195143fb27373f7f6"}, + {file = "pyzmq-25.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db36c27baed588a5a8346b971477b718fdc66cf5b80cbfbd914b4d6d355e44e2"}, + {file = "pyzmq-25.1.2-cp39-cp39-win32.whl", hash = "sha256:39b1067f13aba39d794a24761e385e2eddc26295826530a8c7b6c6c341584289"}, + {file = "pyzmq-25.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:8e9f3fabc445d0ce320ea2c59a75fe3ea591fdbdeebec5db6de530dd4b09412e"}, + {file = "pyzmq-25.1.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a8c1d566344aee826b74e472e16edae0a02e2a044f14f7c24e123002dcff1c05"}, + {file = "pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:759cfd391a0996345ba94b6a5110fca9c557ad4166d86a6e81ea526c376a01e8"}, + {file = "pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c61e346ac34b74028ede1c6b4bcecf649d69b707b3ff9dc0fab453821b04d1e"}, + {file = "pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cb8fc1f8d69b411b8ec0b5f1ffbcaf14c1db95b6bccea21d83610987435f1a4"}, + {file = "pyzmq-25.1.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3c00c9b7d1ca8165c610437ca0c92e7b5607b2f9076f4eb4b095c85d6e680a1d"}, + {file = "pyzmq-25.1.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:df0c7a16ebb94452d2909b9a7b3337940e9a87a824c4fc1c7c36bb4404cb0cde"}, + {file = "pyzmq-25.1.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:45999e7f7ed5c390f2e87ece7f6c56bf979fb213550229e711e45ecc7d42ccb8"}, + {file = "pyzmq-25.1.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ac170e9e048b40c605358667aca3d94e98f604a18c44bdb4c102e67070f3ac9b"}, + {file = "pyzmq-25.1.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1b604734bec94f05f81b360a272fc824334267426ae9905ff32dc2be433ab96"}, + {file = "pyzmq-25.1.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:a793ac733e3d895d96f865f1806f160696422554e46d30105807fdc9841b9f7d"}, + {file = "pyzmq-25.1.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0806175f2ae5ad4b835ecd87f5f85583316b69f17e97786f7443baaf54b9bb98"}, + {file = "pyzmq-25.1.2-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ef12e259e7bc317c7597d4f6ef59b97b913e162d83b421dd0db3d6410f17a244"}, + {file = "pyzmq-25.1.2-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea253b368eb41116011add00f8d5726762320b1bda892f744c91997b65754d73"}, + {file = "pyzmq-25.1.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b9b1f2ad6498445a941d9a4fee096d387fee436e45cc660e72e768d3d8ee611"}, + {file = "pyzmq-25.1.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8b14c75979ce932c53b79976a395cb2a8cd3aaf14aef75e8c2cb55a330b9b49d"}, + {file = "pyzmq-25.1.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:889370d5174a741a62566c003ee8ddba4b04c3f09a97b8000092b7ca83ec9c49"}, + {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a18fff090441a40ffda8a7f4f18f03dc56ae73f148f1832e109f9bffa85df15"}, + {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99a6b36f95c98839ad98f8c553d8507644c880cf1e0a57fe5e3a3f3969040882"}, + {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4345c9a27f4310afbb9c01750e9461ff33d6fb74cd2456b107525bbeebcb5be3"}, + {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3516e0b6224cf6e43e341d56da15fd33bdc37fa0c06af4f029f7d7dfceceabbc"}, + {file = "pyzmq-25.1.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:146b9b1f29ead41255387fb07be56dc29639262c0f7344f570eecdcd8d683314"}, + {file = "pyzmq-25.1.2.tar.gz", hash = "sha256:93f1aa311e8bb912e34f004cf186407a4e90eec4f0ecc0efd26056bf7eda0226"}, +] + +[package.dependencies] +cffi = {version = "*", markers = "implementation_name == \"pypy\""} + +[[package]] +name = "rapidfuzz" +version = "3.6.1" +description = "rapid fuzzy string matching" +optional = false +python-versions = ">=3.8" +files = [ + {file = "rapidfuzz-3.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ac434fc71edda30d45db4a92ba5e7a42c7405e1a54cb4ec01d03cc668c6dcd40"}, + {file = "rapidfuzz-3.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2a791168e119cfddf4b5a40470620c872812042f0621e6a293983a2d52372db0"}, + {file = "rapidfuzz-3.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5a2f3e9df346145c2be94e4d9eeffb82fab0cbfee85bd4a06810e834fe7c03fa"}, + {file = "rapidfuzz-3.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23de71e7f05518b0bbeef55d67b5dbce3bcd3e2c81e7e533051a2e9401354eb0"}, + {file = "rapidfuzz-3.6.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d056e342989248d2bdd67f1955bb7c3b0ecfa239d8f67a8dfe6477b30872c607"}, + {file = "rapidfuzz-3.6.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01835d02acd5d95c1071e1da1bb27fe213c84a013b899aba96380ca9962364bc"}, + {file = "rapidfuzz-3.6.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed0f712e0bb5fea327e92aec8a937afd07ba8de4c529735d82e4c4124c10d5a0"}, + {file = "rapidfuzz-3.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96cd19934f76a1264e8ecfed9d9f5291fde04ecb667faef5f33bdbfd95fe2d1f"}, + {file = "rapidfuzz-3.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e06c4242a1354cf9d48ee01f6f4e6e19c511d50bb1e8d7d20bcadbb83a2aea90"}, + {file = "rapidfuzz-3.6.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:d73dcfe789d37c6c8b108bf1e203e027714a239e50ad55572ced3c004424ed3b"}, + {file = "rapidfuzz-3.6.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:06e98ff000e2619e7cfe552d086815671ed09b6899408c2c1b5103658261f6f3"}, + {file = "rapidfuzz-3.6.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:08b6fb47dd889c69fbc0b915d782aaed43e025df6979b6b7f92084ba55edd526"}, + {file = "rapidfuzz-3.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a1788ebb5f5b655a15777e654ea433d198f593230277e74d51a2a1e29a986283"}, + {file = "rapidfuzz-3.6.1-cp310-cp310-win32.whl", hash = "sha256:c65f92881753aa1098c77818e2b04a95048f30edbe9c3094dc3707d67df4598b"}, + {file = "rapidfuzz-3.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:4243a9c35667a349788461aae6471efde8d8800175b7db5148a6ab929628047f"}, + {file = "rapidfuzz-3.6.1-cp310-cp310-win_arm64.whl", hash = "sha256:f59d19078cc332dbdf3b7b210852ba1f5db8c0a2cd8cc4c0ed84cc00c76e6802"}, + {file = "rapidfuzz-3.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fbc07e2e4ac696497c5f66ec35c21ddab3fc7a406640bffed64c26ab2f7ce6d6"}, + {file = "rapidfuzz-3.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:40cced1a8852652813f30fb5d4b8f9b237112a0bbaeebb0f4cc3611502556764"}, + {file = "rapidfuzz-3.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82300e5f8945d601c2daaaac139d5524d7c1fdf719aa799a9439927739917460"}, + {file = "rapidfuzz-3.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edf97c321fd641fea2793abce0e48fa4f91f3c202092672f8b5b4e781960b891"}, + {file = "rapidfuzz-3.6.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7420e801b00dee4a344ae2ee10e837d603461eb180e41d063699fb7efe08faf0"}, + {file = "rapidfuzz-3.6.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:060bd7277dc794279fa95522af355034a29c90b42adcb7aa1da358fc839cdb11"}, + {file = "rapidfuzz-3.6.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7e3375e4f2bfec77f907680328e4cd16cc64e137c84b1886d547ab340ba6928"}, + {file = "rapidfuzz-3.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a490cd645ef9d8524090551016f05f052e416c8adb2d8b85d35c9baa9d0428ab"}, + {file = "rapidfuzz-3.6.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2e03038bfa66d2d7cffa05d81c2f18fd6acbb25e7e3c068d52bb7469e07ff382"}, + {file = "rapidfuzz-3.6.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:2b19795b26b979c845dba407fe79d66975d520947b74a8ab6cee1d22686f7967"}, + {file = "rapidfuzz-3.6.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:064c1d66c40b3a0f488db1f319a6e75616b2e5fe5430a59f93a9a5e40a656d15"}, + {file = "rapidfuzz-3.6.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3c772d04fb0ebeece3109d91f6122b1503023086a9591a0b63d6ee7326bd73d9"}, + {file = "rapidfuzz-3.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:841eafba6913c4dfd53045835545ba01a41e9644e60920c65b89c8f7e60c00a9"}, + {file = "rapidfuzz-3.6.1-cp311-cp311-win32.whl", hash = "sha256:266dd630f12696ea7119f31d8b8e4959ef45ee2cbedae54417d71ae6f47b9848"}, + {file = "rapidfuzz-3.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:d79aec8aeee02ab55d0ddb33cea3ecd7b69813a48e423c966a26d7aab025cdfe"}, + {file = "rapidfuzz-3.6.1-cp311-cp311-win_arm64.whl", hash = "sha256:484759b5dbc5559e76fefaa9170147d1254468f555fd9649aea3bad46162a88b"}, + {file = "rapidfuzz-3.6.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b2ef4c0fd3256e357b70591ffb9e8ed1d439fb1f481ba03016e751a55261d7c1"}, + {file = "rapidfuzz-3.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:588c4b20fa2fae79d60a4e438cf7133d6773915df3cc0a7f1351da19eb90f720"}, + {file = "rapidfuzz-3.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7142ee354e9c06e29a2636b9bbcb592bb00600a88f02aa5e70e4f230347b373e"}, + {file = "rapidfuzz-3.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1dfc557c0454ad22382373ec1b7df530b4bbd974335efe97a04caec936f2956a"}, + {file = "rapidfuzz-3.6.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:03f73b381bdeccb331a12c3c60f1e41943931461cdb52987f2ecf46bfc22f50d"}, + {file = "rapidfuzz-3.6.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b0ccc2ec1781c7e5370d96aef0573dd1f97335343e4982bdb3a44c133e27786"}, + {file = "rapidfuzz-3.6.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da3e8c9f7e64bb17faefda085ff6862ecb3ad8b79b0f618a6cf4452028aa2222"}, + {file = "rapidfuzz-3.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fde9b14302a31af7bdafbf5cfbb100201ba21519be2b9dedcf4f1048e4fbe65d"}, + {file = "rapidfuzz-3.6.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c1a23eee225dfb21c07f25c9fcf23eb055d0056b48e740fe241cbb4b22284379"}, + {file = "rapidfuzz-3.6.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e49b9575d16c56c696bc7b06a06bf0c3d4ef01e89137b3ddd4e2ce709af9fe06"}, + {file = "rapidfuzz-3.6.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:0a9fc714b8c290261669f22808913aad49553b686115ad0ee999d1cb3df0cd66"}, + {file = "rapidfuzz-3.6.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:a3ee4f8f076aa92184e80308fc1a079ac356b99c39408fa422bbd00145be9854"}, + {file = "rapidfuzz-3.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f056ba42fd2f32e06b2c2ba2443594873cfccc0c90c8b6327904fc2ddf6d5799"}, + {file = "rapidfuzz-3.6.1-cp312-cp312-win32.whl", hash = "sha256:5d82b9651e3d34b23e4e8e201ecd3477c2baa17b638979deeabbb585bcb8ba74"}, + {file = "rapidfuzz-3.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:dad55a514868dae4543ca48c4e1fc0fac704ead038dafedf8f1fc0cc263746c1"}, + {file = "rapidfuzz-3.6.1-cp312-cp312-win_arm64.whl", hash = "sha256:3c84294f4470fcabd7830795d754d808133329e0a81d62fcc2e65886164be83b"}, + {file = "rapidfuzz-3.6.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e19d519386e9db4a5335a4b29f25b8183a1c3f78cecb4c9c3112e7f86470e37f"}, + {file = "rapidfuzz-3.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:01eb03cd880a294d1bf1a583fdd00b87169b9cc9c9f52587411506658c864d73"}, + {file = "rapidfuzz-3.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:be368573255f8fbb0125a78330a1a40c65e9ba3c5ad129a426ff4289099bfb41"}, + {file = "rapidfuzz-3.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3e5af946f419c30f5cb98b69d40997fe8580efe78fc83c2f0f25b60d0e56efb"}, + {file = "rapidfuzz-3.6.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f382f7ffe384ce34345e1c0b2065451267d3453cadde78946fbd99a59f0cc23c"}, + {file = "rapidfuzz-3.6.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be156f51f3a4f369e758505ed4ae64ea88900dcb2f89d5aabb5752676d3f3d7e"}, + {file = "rapidfuzz-3.6.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1936d134b6c513fbe934aeb668b0fee1ffd4729a3c9d8d373f3e404fbb0ce8a0"}, + {file = "rapidfuzz-3.6.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ff8eaf4a9399eb2bebd838f16e2d1ded0955230283b07376d68947bbc2d33d"}, + {file = "rapidfuzz-3.6.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae598a172e3a95df3383634589660d6b170cc1336fe7578115c584a99e0ba64d"}, + {file = "rapidfuzz-3.6.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cd4ba4c18b149da11e7f1b3584813159f189dc20833709de5f3df8b1342a9759"}, + {file = "rapidfuzz-3.6.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:0402f1629e91a4b2e4aee68043a30191e5e1b7cd2aa8dacf50b1a1bcf6b7d3ab"}, + {file = "rapidfuzz-3.6.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:1e12319c6b304cd4c32d5db00b7a1e36bdc66179c44c5707f6faa5a889a317c0"}, + {file = "rapidfuzz-3.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0bbfae35ce4de4c574b386c43c78a0be176eeddfdae148cb2136f4605bebab89"}, + {file = "rapidfuzz-3.6.1-cp38-cp38-win32.whl", hash = "sha256:7fec74c234d3097612ea80f2a80c60720eec34947066d33d34dc07a3092e8105"}, + {file = "rapidfuzz-3.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:a553cc1a80d97459d587529cc43a4c7c5ecf835f572b671107692fe9eddf3e24"}, + {file = "rapidfuzz-3.6.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:757dfd7392ec6346bd004f8826afb3bf01d18a723c97cbe9958c733ab1a51791"}, + {file = "rapidfuzz-3.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2963f4a3f763870a16ee076796be31a4a0958fbae133dbc43fc55c3968564cf5"}, + {file = "rapidfuzz-3.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d2f0274595cc5b2b929c80d4e71b35041104b577e118cf789b3fe0a77b37a4c5"}, + {file = "rapidfuzz-3.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f211e366e026de110a4246801d43a907cd1a10948082f47e8a4e6da76fef52"}, + {file = "rapidfuzz-3.6.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a59472b43879012b90989603aa5a6937a869a72723b1bf2ff1a0d1edee2cc8e6"}, + {file = "rapidfuzz-3.6.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a03863714fa6936f90caa7b4b50ea59ea32bb498cc91f74dc25485b3f8fccfe9"}, + {file = "rapidfuzz-3.6.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5dd95b6b7bfb1584f806db89e1e0c8dbb9d25a30a4683880c195cc7f197eaf0c"}, + {file = "rapidfuzz-3.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7183157edf0c982c0b8592686535c8b3e107f13904b36d85219c77be5cefd0d8"}, + {file = "rapidfuzz-3.6.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ad9d74ef7c619b5b0577e909582a1928d93e07d271af18ba43e428dc3512c2a1"}, + {file = "rapidfuzz-3.6.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b53137d81e770c82189e07a8f32722d9e4260f13a0aec9914029206ead38cac3"}, + {file = "rapidfuzz-3.6.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:49b9ed2472394d306d5dc967a7de48b0aab599016aa4477127b20c2ed982dbf9"}, + {file = "rapidfuzz-3.6.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:dec307b57ec2d5054d77d03ee4f654afcd2c18aee00c48014cb70bfed79597d6"}, + {file = "rapidfuzz-3.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4381023fa1ff32fd5076f5d8321249a9aa62128eb3f21d7ee6a55373e672b261"}, + {file = "rapidfuzz-3.6.1-cp39-cp39-win32.whl", hash = "sha256:8d7a072f10ee57c8413c8ab9593086d42aaff6ee65df4aa6663eecdb7c398dca"}, + {file = "rapidfuzz-3.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:ebcfb5bfd0a733514352cfc94224faad8791e576a80ffe2fd40b2177bf0e7198"}, + {file = "rapidfuzz-3.6.1-cp39-cp39-win_arm64.whl", hash = "sha256:1c47d592e447738744905c18dda47ed155620204714e6df20eb1941bb1ba315e"}, + {file = "rapidfuzz-3.6.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:eef8b346ab331bec12bbc83ac75641249e6167fab3d84d8f5ca37fd8e6c7a08c"}, + {file = "rapidfuzz-3.6.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53251e256017e2b87f7000aee0353ba42392c442ae0bafd0f6b948593d3f68c6"}, + {file = "rapidfuzz-3.6.1-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6dede83a6b903e3ebcd7e8137e7ff46907ce9316e9d7e7f917d7e7cdc570ee05"}, + {file = "rapidfuzz-3.6.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4da90e4c2b444d0a171d7444ea10152e07e95972bb40b834a13bdd6de1110c"}, + {file = "rapidfuzz-3.6.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:ca3dfcf74f2b6962f411c33dd95b0adf3901266e770da6281bc96bb5a8b20de9"}, + {file = "rapidfuzz-3.6.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bcc957c0a8bde8007f1a8a413a632a1a409890f31f73fe764ef4eac55f59ca87"}, + {file = "rapidfuzz-3.6.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:692c9a50bea7a8537442834f9bc6b7d29d8729a5b6379df17c31b6ab4df948c2"}, + {file = "rapidfuzz-3.6.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c23ceaea27e790ddd35ef88b84cf9d721806ca366199a76fd47cfc0457a81b"}, + {file = "rapidfuzz-3.6.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b155e67fff215c09f130555002e42f7517d0ea72cbd58050abb83cb7c880cec"}, + {file = "rapidfuzz-3.6.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3028ee8ecc48250607fa8a0adce37b56275ec3b1acaccd84aee1f68487c8557b"}, + {file = "rapidfuzz-3.6.1.tar.gz", hash = "sha256:35660bee3ce1204872574fa041c7ad7ec5175b3053a4cb6e181463fc07013de7"}, +] + +[package.extras] +full = ["numpy"] + +[[package]] +name = "referencing" +version = "0.32.0" +description = "JSON Referencing + Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "referencing-0.32.0-py3-none-any.whl", hash = "sha256:bdcd3efb936f82ff86f993093f6da7435c7de69a3b3a5a06678a6050184bee99"}, + {file = "referencing-0.32.0.tar.gz", hash = "sha256:689e64fe121843dcfd57b71933318ef1f91188ffb45367332700a86ac8fd6161"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +rpds-py = ">=0.7.0" + [[package]] name = "requests" version = "2.31.0" @@ -843,41 +2747,229 @@ requests = ">=2.0.0" [package.extras] rsa = ["oauthlib[signedtoken] (>=3.0.0)"] +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +description = "A utility belt for advanced users of python-requests" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, + {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, +] + +[package.dependencies] +requests = ">=2.0.1,<3.0.0" + [[package]] name = "responses" -version = "0.23.2" +version = "0.24.1" description = "A utility library for mocking out the `requests` Python library." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "responses-0.23.2-py3-none-any.whl", hash = "sha256:9d49c218ba3079022bd63427f12b0a43b43d2f6aaf5ed859b9df9d733b4dd775"}, - {file = "responses-0.23.2.tar.gz", hash = "sha256:5d5a2ce3285f84e1f107d2e942476b6c7dff3747f289c0eae997cb77d2ab68e8"}, + {file = "responses-0.24.1-py3-none-any.whl", hash = "sha256:a2b43f4c08bfb9c9bd242568328c65a34b318741d3fab884ac843c5ceeb543f9"}, + {file = "responses-0.24.1.tar.gz", hash = "sha256:b127c6ca3f8df0eb9cc82fd93109a3007a86acb24871834c47b77765152ecf8c"}, ] [package.dependencies] pyyaml = "*" requests = ">=2.30.0,<3.0" -types-PyYAML = "*" -urllib3 = ">=2.0.0,<3.0" +urllib3 = ">=1.25.10,<3.0" + +[package.extras] +tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli", "tomli-w", "types-PyYAML", "types-requests"] + +[[package]] +name = "rfc3339-validator" +version = "0.1.4" +description = "A pure python RFC3339 validator" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa"}, + {file = "rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b"}, +] + +[package.dependencies] +six = "*" + +[[package]] +name = "rfc3986-validator" +version = "0.1.1" +description = "Pure python rfc3986 validator" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9"}, + {file = "rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055"}, +] + +[[package]] +name = "rpds-py" +version = "0.16.2" +description = "Python bindings to Rust's persistent data structures (rpds)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "rpds_py-0.16.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:509b617ac787cd1149600e731db9274ebbef094503ca25158e6f23edaba1ca8f"}, + {file = "rpds_py-0.16.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:413b9c17388bbd0d87a329d8e30c1a4c6e44e2bb25457f43725a8e6fe4161e9e"}, + {file = "rpds_py-0.16.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2946b120718eba9af2b4dd103affc1164a87b9e9ebff8c3e4c05d7b7a7e274e2"}, + {file = "rpds_py-0.16.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:35ae5ece284cf36464eb160880018cf6088a9ac5ddc72292a6092b6ef3f4da53"}, + {file = "rpds_py-0.16.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc6a7620ba7639a3db6213da61312cb4aa9ac0ca6e00dc1cbbdc21c2aa6eb57"}, + {file = "rpds_py-0.16.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8cb6fe8ecdfffa0e711a75c931fb39f4ba382b4b3ccedeca43f18693864fe850"}, + {file = "rpds_py-0.16.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dace7b26a13353e24613417ce2239491b40a6ad44e5776a18eaff7733488b44"}, + {file = "rpds_py-0.16.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1bdbc5fcb04a7309074de6b67fa9bc4b418ab3fc435fec1f2779a0eced688d04"}, + {file = "rpds_py-0.16.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f42e25c016927e2a6b1ce748112c3ab134261fc2ddc867e92d02006103e1b1b7"}, + {file = "rpds_py-0.16.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:eab36eae3f3e8e24b05748ec9acc66286662f5d25c52ad70cadab544e034536b"}, + {file = "rpds_py-0.16.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0474df4ade9a3b4af96c3d36eb81856cb9462e4c6657d4caecfd840d2a13f3c9"}, + {file = "rpds_py-0.16.2-cp310-none-win32.whl", hash = "sha256:84c5a4d1f9dd7e2d2c44097fb09fffe728629bad31eb56caf97719e55575aa82"}, + {file = "rpds_py-0.16.2-cp310-none-win_amd64.whl", hash = "sha256:2bd82db36cd70b3628c0c57d81d2438e8dd4b7b32a6a9f25f24ab0e657cb6c4e"}, + {file = "rpds_py-0.16.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:adc0c3d6fc6ae35fee3e4917628983f6ce630d513cbaad575b4517d47e81b4bb"}, + {file = "rpds_py-0.16.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ec23fcad480e77ede06cf4127a25fc440f7489922e17fc058f426b5256ee0edb"}, + {file = "rpds_py-0.16.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07aab64e2808c3ebac2a44f67e9dc0543812b715126dfd6fe4264df527556cb6"}, + {file = "rpds_py-0.16.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a4ebb8b20bd09c5ce7884c8f0388801100f5e75e7f733b1b6613c713371feefc"}, + {file = "rpds_py-0.16.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3d7e2ea25d3517c6d7e5a1cc3702cffa6bd18d9ef8d08d9af6717fc1c700eed"}, + {file = "rpds_py-0.16.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f28ac0e8e7242d140f99402a903a2c596ab71550272ae9247ad78f9a932b5698"}, + {file = "rpds_py-0.16.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19f00f57fdd38db4bb5ad09f9ead1b535332dbf624200e9029a45f1f35527ebb"}, + {file = "rpds_py-0.16.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3da5a4c56953bdbf6d04447c3410309616c54433146ccdb4a277b9cb499bc10e"}, + {file = "rpds_py-0.16.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec2e1cf025b2c0f48ec17ff3e642661da7ee332d326f2e6619366ce8e221f018"}, + {file = "rpds_py-0.16.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e0441fb4fdd39a230477b2ca9be90868af64425bfe7b122b57e61e45737a653b"}, + {file = "rpds_py-0.16.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9f0350ef2fba5f34eb0c9000ea328e51b9572b403d2f7f3b19f24085f6f598e8"}, + {file = "rpds_py-0.16.2-cp311-none-win32.whl", hash = "sha256:5a80e2f83391ad0808b4646732af2a7b67550b98f0cae056cb3b40622a83dbb3"}, + {file = "rpds_py-0.16.2-cp311-none-win_amd64.whl", hash = "sha256:e04e56b4ca7a770593633556e8e9e46579d66ec2ada846b401252a2bdcf70a6d"}, + {file = "rpds_py-0.16.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:5e6caa3809e50690bd92fa490f5c38caa86082c8c3315aa438bce43786d5e90d"}, + {file = "rpds_py-0.16.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e53b9b25cac9065328901713a7e9e3b12e4f57ef4280b370fbbf6fef2052eef"}, + {file = "rpds_py-0.16.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af27423662f32d7501a00c5e7342f7dbd1e4a718aea7a239781357d15d437133"}, + {file = "rpds_py-0.16.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d4dd5fb16eb3825742bad8339d454054261ab59fed2fbac84e1d84d5aae7ba"}, + {file = "rpds_py-0.16.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e061de3b745fe611e23cd7318aec2c8b0e4153939c25c9202a5811ca911fd733"}, + {file = "rpds_py-0.16.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b811d182ad17ea294f2ec63c0621e7be92a1141e1012383461872cead87468f"}, + {file = "rpds_py-0.16.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5552f328eaef1a75ff129d4d0c437bf44e43f9436d3996e8eab623ea0f5fcf73"}, + {file = "rpds_py-0.16.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dcbe1f8dd179e4d69b70b1f1d9bb6fd1e7e1bdc9c9aad345cdeb332e29d40748"}, + {file = "rpds_py-0.16.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8aad80645a011abae487d356e0ceb359f4938dfb6f7bcc410027ed7ae4f7bb8b"}, + {file = "rpds_py-0.16.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b6f5549d6ed1da9bfe3631ca9483ae906f21410be2445b73443fa9f017601c6f"}, + {file = "rpds_py-0.16.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d452817e0d9c749c431a1121d56a777bd7099b720b3d1c820f1725cb40928f58"}, + {file = "rpds_py-0.16.2-cp312-none-win32.whl", hash = "sha256:888a97002e986eca10d8546e3c8b97da1d47ad8b69726dcfeb3e56348ebb28a3"}, + {file = "rpds_py-0.16.2-cp312-none-win_amd64.whl", hash = "sha256:d8dda2a806dfa4a9b795950c4f5cc56d6d6159f7d68080aedaff3bdc9b5032f5"}, + {file = "rpds_py-0.16.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:071980663c273bf3d388fe5c794c547e6f35ba3335477072c713a3176bf14a60"}, + {file = "rpds_py-0.16.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:726ac36e8a3bb8daef2fd482534cabc5e17334052447008405daca7ca04a3108"}, + {file = "rpds_py-0.16.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9e557db6a177470316c82f023e5d571811c9a4422b5ea084c85da9aa3c035fc"}, + {file = "rpds_py-0.16.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:90123853fc8b1747f80b0d354be3d122b4365a93e50fc3aacc9fb4c2488845d6"}, + {file = "rpds_py-0.16.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a61f659665a39a4d17d699ab3593d7116d66e1e2e3f03ef3fb8f484e91908808"}, + {file = "rpds_py-0.16.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc97f0640e91d7776530f06e6836c546c1c752a52de158720c4224c9e8053cad"}, + {file = "rpds_py-0.16.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44a54e99a2b9693a37ebf245937fd6e9228b4cbd64b9cc961e1f3391ec6c7391"}, + {file = "rpds_py-0.16.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd4b677d929cf1f6bac07ad76e0f2d5de367e6373351c01a9c0a39f6b21b4a8b"}, + {file = "rpds_py-0.16.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:5ef00873303d678aaf8b0627e111fd434925ca01c657dbb2641410f1cdaef261"}, + {file = "rpds_py-0.16.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:349cb40897fd529ca15317c22c0eab67f5ac5178b5bd2c6adc86172045210acc"}, + {file = "rpds_py-0.16.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:2ddef620e70eaffebed5932ce754d539c0930f676aae6212f8e16cd9743dd365"}, + {file = "rpds_py-0.16.2-cp38-none-win32.whl", hash = "sha256:882ce6e25e585949c3d9f9abd29202367175e0aab3aba0c58c9abbb37d4982ff"}, + {file = "rpds_py-0.16.2-cp38-none-win_amd64.whl", hash = "sha256:f4bd4578e44f26997e9e56c96dedc5f1af43cc9d16c4daa29c771a00b2a26851"}, + {file = "rpds_py-0.16.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:69ac7ea9897ec201ce68b48582f3eb34a3f9924488a5432a93f177bf76a82a7e"}, + {file = "rpds_py-0.16.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a9880b4656efe36ccad41edc66789e191e5ee19a1ea8811e0aed6f69851a82f4"}, + {file = "rpds_py-0.16.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee94cb58c0ba2c62ee108c2b7c9131b2c66a29e82746e8fa3aa1a1effbd3dcf1"}, + {file = "rpds_py-0.16.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24f7a2eb3866a9e91f4599851e0c8d39878a470044875c49bd528d2b9b88361c"}, + {file = "rpds_py-0.16.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ca57468da2d9a660bcf8961637c85f2fbb2aa64d9bc3f9484e30c3f9f67b1dd7"}, + {file = "rpds_py-0.16.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ccd4e400309e1f34a5095bf9249d371f0fd60f8a3a5c4a791cad7b99ce1fd38d"}, + {file = "rpds_py-0.16.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80443fe2f7b3ea3934c5d75fb0e04a5dbb4a8e943e5ff2de0dec059202b70a8b"}, + {file = "rpds_py-0.16.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4d6a9f052e72d493efd92a77f861e45bab2f6be63e37fa8ecf0c6fd1a58fedb0"}, + {file = "rpds_py-0.16.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:35953f4f2b3216421af86fd236b7c0c65935936a94ea83ddbd4904ba60757773"}, + {file = "rpds_py-0.16.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:981d135c7cdaf6cd8eadae1c950de43b976de8f09d8e800feed307140d3d6d00"}, + {file = "rpds_py-0.16.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d0dd7ed2f16df2e129496e7fbe59a34bc2d7fc8db443a606644d069eb69cbd45"}, + {file = "rpds_py-0.16.2-cp39-none-win32.whl", hash = "sha256:703d95c75a72e902544fda08e965885525e297578317989fd15a6ce58414b41d"}, + {file = "rpds_py-0.16.2-cp39-none-win_amd64.whl", hash = "sha256:e93ec1b300acf89730cf27975ef574396bc04edecc358e9bd116fb387a123239"}, + {file = "rpds_py-0.16.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:44627b6ca7308680a70766454db5249105fa6344853af6762eaad4158a2feebe"}, + {file = "rpds_py-0.16.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:3f91df8e6dbb7360e176d1affd5fb0246d2b88d16aa5ebc7db94fd66b68b61da"}, + {file = "rpds_py-0.16.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d904c5693e08bad240f16d79305edba78276be87061c872a4a15e2c301fa2c0"}, + {file = "rpds_py-0.16.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:290a81cfbe4673285cdf140ec5cd1658ffbf63ab359f2b352ebe172e7cfa5bf0"}, + {file = "rpds_py-0.16.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b634c5ec0103c5cbebc24ebac4872b045cccb9456fc59efdcf6fe39775365bd2"}, + {file = "rpds_py-0.16.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a297a4d08cc67c7466c873c78039d87840fb50d05473db0ec1b7b03d179bf322"}, + {file = "rpds_py-0.16.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2e75e17bd0bb66ee34a707da677e47c14ee51ccef78ed6a263a4cc965a072a1"}, + {file = "rpds_py-0.16.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f1b9d9260e06ea017feb7172976ab261e011c1dc2f8883c7c274f6b2aabfe01a"}, + {file = "rpds_py-0.16.2-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:162d7cd9cd311c1b0ff1c55a024b8f38bd8aad1876b648821da08adc40e95734"}, + {file = "rpds_py-0.16.2-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:9b32f742ce5b57201305f19c2ef7a184b52f6f9ba6871cc042c2a61f0d6b49b8"}, + {file = "rpds_py-0.16.2-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac08472f41ea77cd6a5dae36ae7d4ed3951d6602833af87532b556c1b4601d63"}, + {file = "rpds_py-0.16.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:495a14b72bbe217f2695dcd9b5ab14d4f8066a00f5d209ed94f0aca307f85f6e"}, + {file = "rpds_py-0.16.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:8d6b6937ae9eac6d6c0ca3c42774d89fa311f55adff3970fb364b34abde6ed3d"}, + {file = "rpds_py-0.16.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a61226465bda9283686db8f17d02569a98e4b13c637be5a26d44aa1f1e361c2"}, + {file = "rpds_py-0.16.2-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5cf6af100ffb5c195beec11ffaa8cf8523057f123afa2944e6571d54da84cdc9"}, + {file = "rpds_py-0.16.2-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6df15846ee3fb2e6397fe25d7ca6624af9f89587f3f259d177b556fed6bebe2c"}, + {file = "rpds_py-0.16.2-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1be2f033df1b8be8c3167ba3c29d5dca425592ee31e35eac52050623afba5772"}, + {file = "rpds_py-0.16.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96f957d6ab25a78b9e7fc9749d754b98eac825a112b4e666525ce89afcbd9ed5"}, + {file = "rpds_py-0.16.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:088396c7c70e59872f67462fcac3ecbded5233385797021976a09ebd55961dfe"}, + {file = "rpds_py-0.16.2-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4c46ad6356e1561f2a54f08367d1d2e70a0a1bb2db2282d2c1972c1d38eafc3b"}, + {file = "rpds_py-0.16.2-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:47713dc4fce213f5c74ca8a1f6a59b622fc1b90868deb8e8e4d993e421b4b39d"}, + {file = "rpds_py-0.16.2-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:f811771019f063bbd0aa7bb72c8a934bc13ebacb4672d712fc1639cfd314cccc"}, + {file = "rpds_py-0.16.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f19afcfc0dd0dca35694df441e9b0f95bc231b512f51bded3c3d8ca32153ec19"}, + {file = "rpds_py-0.16.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a4b682c5775d6a3d21e314c10124599976809455ee67020e8e72df1769b87bc3"}, + {file = "rpds_py-0.16.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c647ca87fc0ebe808a41de912e9a1bfef9acb85257e5d63691364ac16b81c1f0"}, + {file = "rpds_py-0.16.2-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:302bd4983bbd47063e452c38be66153760112f6d3635c7eeefc094299fa400a9"}, + {file = "rpds_py-0.16.2-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bf721ede3eb7b829e4a9b8142bd55db0bdc82902720548a703f7e601ee13bdc3"}, + {file = "rpds_py-0.16.2-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:358dafc89ce3894c7f486c615ba914609f38277ef67f566abc4c854d23b997fa"}, + {file = "rpds_py-0.16.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cad0f59ee3dc35526039f4bc23642d52d5f6616b5f687d846bfc6d0d6d486db0"}, + {file = "rpds_py-0.16.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cffa76b385dfe1e38527662a302b19ffb0e7f5cf7dd5e89186d2c94a22dd9d0c"}, + {file = "rpds_py-0.16.2-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:83640a5d7cd3bff694747d50436b8b541b5b9b9782b0c8c1688931d6ee1a1f2d"}, + {file = "rpds_py-0.16.2-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:ed99b4f7179d2111702020fd7d156e88acd533f5a7d3971353e568b6051d5c97"}, + {file = "rpds_py-0.16.2-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:4022b9dc620e14f30201a8a73898a873c8e910cb642bcd2f3411123bc527f6ac"}, + {file = "rpds_py-0.16.2.tar.gz", hash = "sha256:781ef8bfc091b19960fc0142a23aedadafa826bc32b433fdfe6fd7f964d7ef44"}, +] + +[[package]] +name = "secretstorage" +version = "3.3.3" +description = "Python bindings to FreeDesktop.org Secret Service API" +optional = false +python-versions = ">=3.6" +files = [ + {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, + {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, +] + +[package.dependencies] +cryptography = ">=2.0" +jeepney = ">=0.6" + +[[package]] +name = "send2trash" +version = "1.8.2" +description = "Send file to trash natively under Mac OS X, Windows and Linux" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +files = [ + {file = "Send2Trash-1.8.2-py3-none-any.whl", hash = "sha256:a384719d99c07ce1eefd6905d2decb6f8b7ed054025bb0e618919f945de4f679"}, + {file = "Send2Trash-1.8.2.tar.gz", hash = "sha256:c132d59fa44b9ca2b1699af5c86f57ce9f4c5eb56629d5d55fbb7a35f84e2312"}, +] [package.extras] -tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli", "tomli-w", "types-requests"] +nativelib = ["pyobjc-framework-Cocoa", "pywin32"] +objc = ["pyobjc-framework-Cocoa"] +win32 = ["pywin32"] [[package]] name = "setuptools" -version = "68.0.0" +version = "69.0.3" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, - {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, + {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"}, + {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "shellingham" +version = "1.5.4" +description = "Tool to Detect Surrounding Shell" +optional = false +python-versions = ">=3.7" +files = [ + {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, + {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, +] [[package]] name = "six" @@ -890,6 +2982,86 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "sniffio" +version = "1.3.0" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, + {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, +] + +[[package]] +name = "soupsieve" +version = "2.5" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.8" +files = [ + {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, + {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +description = "Extract data from python stack frames and tracebacks for informative displays" +optional = false +python-versions = "*" +files = [ + {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, + {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, +] + +[package.dependencies] +asttokens = ">=2.1.0" +executing = ">=1.2.0" +pure-eval = "*" + +[package.extras] +tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] + +[[package]] +name = "terminado" +version = "0.18.0" +description = "Tornado websocket backend for the Xterm.js Javascript terminal emulator library." +optional = false +python-versions = ">=3.8" +files = [ + {file = "terminado-0.18.0-py3-none-any.whl", hash = "sha256:87b0d96642d0fe5f5abd7783857b9cab167f221a39ff98e3b9619a788a3c0f2e"}, + {file = "terminado-0.18.0.tar.gz", hash = "sha256:1ea08a89b835dd1b8c0c900d92848147cef2537243361b2e3f4dc15df9b6fded"}, +] + +[package.dependencies] +ptyprocess = {version = "*", markers = "os_name != \"nt\""} +pywinpty = {version = ">=1.1.0", markers = "os_name == \"nt\""} +tornado = ">=6.1.0" + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["pre-commit", "pytest (>=7.0)", "pytest-timeout"] +typing = ["mypy (>=1.6,<2.0)", "traitlets (>=5.11.1)"] + +[[package]] +name = "tinycss2" +version = "1.2.1" +description = "A tiny CSS parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tinycss2-1.2.1-py3-none-any.whl", hash = "sha256:2b80a96d41e7c3914b8cda8bc7f705a4d9c49275616e886103dd839dfc847847"}, + {file = "tinycss2-1.2.1.tar.gz", hash = "sha256:8cff3a8f066c2ec677c06dbc7b45619804a6938478d9d73c284b29d14ecb0627"}, +] + +[package.dependencies] +webencodings = ">=0.4" + +[package.extras] +doc = ["sphinx", "sphinx_rtd_theme"] +test = ["flake8", "isort", "pytest"] + [[package]] name = "tomli" version = "2.0.1" @@ -901,148 +3073,288 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "tomlkit" +version = "0.12.3" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomlkit-0.12.3-py3-none-any.whl", hash = "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba"}, + {file = "tomlkit-0.12.3.tar.gz", hash = "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4"}, +] + [[package]] name = "tornado" -version = "6.1" +version = "6.4" description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." optional = false -python-versions = ">= 3.5" -files = [ - {file = "tornado-6.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32"}, - {file = "tornado-6.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c"}, - {file = "tornado-6.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9de9e5188a782be6b1ce866e8a51bc76a0fbaa0e16613823fc38e4fc2556ad05"}, - {file = "tornado-6.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:61b32d06ae8a036a6607805e6720ef00a3c98207038444ba7fd3d169cd998910"}, - {file = "tornado-6.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:3e63498f680547ed24d2c71e6497f24bca791aca2fe116dbc2bd0ac7f191691b"}, - {file = "tornado-6.1-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:6c77c9937962577a6a76917845d06af6ab9197702a42e1346d8ae2e76b5e3675"}, - {file = "tornado-6.1-cp35-cp35m-win32.whl", hash = "sha256:6286efab1ed6e74b7028327365cf7346b1d777d63ab30e21a0f4d5b275fc17d5"}, - {file = "tornado-6.1-cp35-cp35m-win_amd64.whl", hash = "sha256:fa2ba70284fa42c2a5ecb35e322e68823288a4251f9ba9cc77be04ae15eada68"}, - {file = "tornado-6.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb"}, - {file = "tornado-6.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:748290bf9112b581c525e6e6d3820621ff020ed95af6f17fedef416b27ed564c"}, - {file = "tornado-6.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e385b637ac3acaae8022e7e47dfa7b83d3620e432e3ecb9a3f7f58f150e50921"}, - {file = "tornado-6.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:25ad220258349a12ae87ede08a7b04aca51237721f63b1808d39bdb4b2164558"}, - {file = "tornado-6.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:65d98939f1a2e74b58839f8c4dab3b6b3c1ce84972ae712be02845e65391ac7c"}, - {file = "tornado-6.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:e519d64089b0876c7b467274468709dadf11e41d65f63bba207e04217f47c085"}, - {file = "tornado-6.1-cp36-cp36m-win32.whl", hash = "sha256:b87936fd2c317b6ee08a5741ea06b9d11a6074ef4cc42e031bc6403f82a32575"}, - {file = "tornado-6.1-cp36-cp36m-win_amd64.whl", hash = "sha256:cc0ee35043162abbf717b7df924597ade8e5395e7b66d18270116f8745ceb795"}, - {file = "tornado-6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7250a3fa399f08ec9cb3f7b1b987955d17e044f1ade821b32e5f435130250d7f"}, - {file = "tornado-6.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ed3ad863b1b40cd1d4bd21e7498329ccaece75db5a5bf58cd3c9f130843e7102"}, - {file = "tornado-6.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:dcef026f608f678c118779cd6591c8af6e9b4155c44e0d1bc0c87c036fb8c8c4"}, - {file = "tornado-6.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:70dec29e8ac485dbf57481baee40781c63e381bebea080991893cd297742b8fd"}, - {file = "tornado-6.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d3f7594930c423fd9f5d1a76bee85a2c36fd8b4b16921cae7e965f22575e9c01"}, - {file = "tornado-6.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3447475585bae2e77ecb832fc0300c3695516a47d46cefa0528181a34c5b9d3d"}, - {file = "tornado-6.1-cp37-cp37m-win32.whl", hash = "sha256:e7229e60ac41a1202444497ddde70a48d33909e484f96eb0da9baf8dc68541df"}, - {file = "tornado-6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:cb5ec8eead331e3bb4ce8066cf06d2dfef1bfb1b2a73082dfe8a161301b76e37"}, - {file = "tornado-6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:20241b3cb4f425e971cb0a8e4ffc9b0a861530ae3c52f2b0434e6c1b57e9fd95"}, - {file = "tornado-6.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c77da1263aa361938476f04c4b6c8916001b90b2c2fdd92d8d535e1af48fba5a"}, - {file = "tornado-6.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:fba85b6cd9c39be262fcd23865652920832b61583de2a2ca907dbd8e8a8c81e5"}, - {file = "tornado-6.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:1e8225a1070cd8eec59a996c43229fe8f95689cb16e552d130b9793cb570a288"}, - {file = "tornado-6.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d14d30e7f46a0476efb0deb5b61343b1526f73ebb5ed84f23dc794bdb88f9d9f"}, - {file = "tornado-6.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8f959b26f2634a091bb42241c3ed8d3cedb506e7c27b8dd5c7b9f745318ddbb6"}, - {file = "tornado-6.1-cp38-cp38-win32.whl", hash = "sha256:34ca2dac9e4d7afb0bed4677512e36a52f09caa6fded70b4e3e1c89dbd92c326"}, - {file = "tornado-6.1-cp38-cp38-win_amd64.whl", hash = "sha256:6196a5c39286cc37c024cd78834fb9345e464525d8991c21e908cc046d1cc02c"}, - {file = "tornado-6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5"}, - {file = "tornado-6.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe"}, - {file = "tornado-6.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea"}, - {file = "tornado-6.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2"}, - {file = "tornado-6.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0"}, - {file = "tornado-6.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd"}, - {file = "tornado-6.1-cp39-cp39-win32.whl", hash = "sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c"}, - {file = "tornado-6.1-cp39-cp39-win_amd64.whl", hash = "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4"}, - {file = "tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791"}, -] - -[[package]] -name = "types-pyyaml" -version = "6.0.12.11" -description = "Typing stubs for PyYAML" +python-versions = ">= 3.8" +files = [ + {file = "tornado-6.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:02ccefc7d8211e5a7f9e8bc3f9e5b0ad6262ba2fbb683a6443ecc804e5224ce0"}, + {file = "tornado-6.4-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:27787de946a9cffd63ce5814c33f734c627a87072ec7eed71f7fc4417bb16263"}, + {file = "tornado-6.4-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7894c581ecdcf91666a0912f18ce5e757213999e183ebfc2c3fdbf4d5bd764e"}, + {file = "tornado-6.4-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e43bc2e5370a6a8e413e1e1cd0c91bedc5bd62a74a532371042a18ef19e10579"}, + {file = "tornado-6.4-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0251554cdd50b4b44362f73ad5ba7126fc5b2c2895cc62b14a1c2d7ea32f212"}, + {file = "tornado-6.4-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fd03192e287fbd0899dd8f81c6fb9cbbc69194d2074b38f384cb6fa72b80e9c2"}, + {file = "tornado-6.4-cp38-abi3-musllinux_1_1_i686.whl", hash = "sha256:88b84956273fbd73420e6d4b8d5ccbe913c65d31351b4c004ae362eba06e1f78"}, + {file = "tornado-6.4-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:71ddfc23a0e03ef2df1c1397d859868d158c8276a0603b96cf86892bff58149f"}, + {file = "tornado-6.4-cp38-abi3-win32.whl", hash = "sha256:6f8a6c77900f5ae93d8b4ae1196472d0ccc2775cc1dfdc9e7727889145c45052"}, + {file = "tornado-6.4-cp38-abi3-win_amd64.whl", hash = "sha256:10aeaa8006333433da48dec9fe417877f8bcc21f48dda8d661ae79da357b2a63"}, + {file = "tornado-6.4.tar.gz", hash = "sha256:72291fa6e6bc84e626589f1c29d90a5a6d593ef5ae68052ee2ef000dfd273dee"}, +] + +[[package]] +name = "traitlets" +version = "5.14.0" +description = "Traitlets Python configuration system" +optional = false +python-versions = ">=3.8" +files = [ + {file = "traitlets-5.14.0-py3-none-any.whl", hash = "sha256:f14949d23829023013c47df20b4a76ccd1a85effb786dc060f34de7948361b33"}, + {file = "traitlets-5.14.0.tar.gz", hash = "sha256:fcdaa8ac49c04dfa0ed3ee3384ef6dfdb5d6f3741502be247279407679296772"}, +] + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<7.5)", "pytest-mock", "pytest-mypy-testing"] + +[[package]] +name = "trove-classifiers" +version = "2023.11.29" +description = "Canonical source for classifiers on PyPI (pypi.org)." +optional = false +python-versions = "*" +files = [ + {file = "trove-classifiers-2023.11.29.tar.gz", hash = "sha256:ff8f7fd82c7932113b46e7ef6742c70091cc63640c8c65db00d91f2e940b9514"}, + {file = "trove_classifiers-2023.11.29-py3-none-any.whl", hash = "sha256:02307750cbbac2b3d13078662f8a5bf077732bf506e9c33c97204b7f68f3699e"}, +] + +[[package]] +name = "types-python-dateutil" +version = "2.8.19.14" +description = "Typing stubs for python-dateutil" optional = false python-versions = "*" files = [ - {file = "types-PyYAML-6.0.12.11.tar.gz", hash = "sha256:7d340b19ca28cddfdba438ee638cd4084bde213e501a3978738543e27094775b"}, - {file = "types_PyYAML-6.0.12.11-py3-none-any.whl", hash = "sha256:a461508f3096d1d5810ec5ab95d7eeecb651f3a15b71959999988942063bf01d"}, + {file = "types-python-dateutil-2.8.19.14.tar.gz", hash = "sha256:1f4f10ac98bb8b16ade9dbee3518d9ace017821d94b057a425b069f834737f4b"}, + {file = "types_python_dateutil-2.8.19.14-py3-none-any.whl", hash = "sha256:f977b8de27787639986b4e28963263fd0e5158942b3ecef91b9335c130cb1ce9"}, ] [[package]] -name = "tzdata" -version = "2023.3" -description = "Provider of IANA time zone data" +name = "typing-extensions" +version = "4.9.0" +description = "Backported and Experimental Type Hints for Python 3.8+" optional = false -python-versions = ">=2" +python-versions = ">=3.8" files = [ - {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, - {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, + {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, + {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, ] [[package]] -name = "tzlocal" -version = "5.0.1" -description = "tzinfo object for the local timezone" +name = "uri-template" +version = "1.3.0" +description = "RFC 6570 URI Template Processor" optional = false python-versions = ">=3.7" files = [ - {file = "tzlocal-5.0.1-py3-none-any.whl", hash = "sha256:f3596e180296aaf2dbd97d124fe76ae3a0e3d32b258447de7b939b3fd4be992f"}, - {file = "tzlocal-5.0.1.tar.gz", hash = "sha256:46eb99ad4bdb71f3f72b7d24f4267753e240944ecfc16f25d2719ba89827a803"}, + {file = "uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7"}, + {file = "uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363"}, ] -[package.dependencies] -tzdata = {version = "*", markers = "platform_system == \"Windows\""} - [package.extras] -devenv = ["black", "check-manifest", "flake8", "pyroma", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] +dev = ["flake8", "flake8-annotations", "flake8-bandit", "flake8-bugbear", "flake8-commas", "flake8-comprehensions", "flake8-continuation", "flake8-datetimez", "flake8-docstrings", "flake8-import-order", "flake8-literal", "flake8-modern-annotations", "flake8-noqa", "flake8-pyproject", "flake8-requirements", "flake8-typechecking-import", "flake8-use-fstring", "mypy", "pep8-naming", "types-PyYAML"] [[package]] name = "urllib3" -version = "2.0.4" +version = "2.1.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "urllib3-2.0.4-py3-none-any.whl", hash = "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"}, - {file = "urllib3-2.0.4.tar.gz", hash = "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11"}, + {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"}, + {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"}, ] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.24.2" +version = "20.25.0" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.24.2-py3-none-any.whl", hash = "sha256:43a3052be36080548bdee0b42919c88072037d50d56c28bd3f853cbe92b953ff"}, - {file = "virtualenv-20.24.2.tar.gz", hash = "sha256:fd8a78f46f6b99a67b7ec5cf73f92357891a7b3a40fd97637c27f854aae3b9e0"}, + {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, + {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, ] [package.dependencies] distlib = ">=0.3.7,<1" filelock = ">=3.12.2,<4" -platformdirs = ">=3.9.1,<4" +platformdirs = ">=3.9.1,<5" [package.extras] -docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "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 = "wcwidth" +version = "0.2.12" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.12-py2.py3-none-any.whl", hash = "sha256:f26ec43d96c8cbfed76a5075dac87680124fa84e0855195a6184da9c187f133c"}, + {file = "wcwidth-0.2.12.tar.gz", hash = "sha256:f01c104efdf57971bcb756f054dd58ddec5204dd15fa31d6503ea57947d97c02"}, +] + +[[package]] +name = "webcolors" +version = "1.13" +description = "A library for working with the color formats defined by HTML and CSS." +optional = false +python-versions = ">=3.7" +files = [ + {file = "webcolors-1.13-py3-none-any.whl", hash = "sha256:29bc7e8752c0a1bd4a1f03c14d6e6a72e93d82193738fa860cbff59d0fcc11bf"}, + {file = "webcolors-1.13.tar.gz", hash = "sha256:c225b674c83fa923be93d235330ce0300373d02885cef23238813b0d5668304a"}, +] + +[package.extras] +docs = ["furo", "sphinx", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-notfound-page", "sphinxext-opengraph"] +tests = ["pytest", "pytest-cov"] + +[[package]] +name = "webencodings" +version = "0.5.1" +description = "Character encoding aliases for legacy web content" +optional = false +python-versions = "*" +files = [ + {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, + {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, +] + +[[package]] +name = "websocket-client" +version = "1.7.0" +description = "WebSocket client for Python with low level API options" +optional = false +python-versions = ">=3.8" +files = [ + {file = "websocket-client-1.7.0.tar.gz", hash = "sha256:10e511ea3a8c744631d3bd77e61eb17ed09304c413ad42cf6ddfa4c7787e8fe6"}, + {file = "websocket_client-1.7.0-py3-none-any.whl", hash = "sha256:f4c3d22fec12a2461427a29957ff07d35098ee2d976d3ba244e688b8b4057588"}, +] + +[package.extras] +docs = ["Sphinx (>=6.0)", "sphinx-rtd-theme (>=1.1.0)"] +optional = ["python-socks", "wsaccel"] +test = ["websockets"] + +[[package]] +name = "xattr" +version = "0.10.1" +description = "Python wrapper for extended filesystem attributes" +optional = false +python-versions = "*" +files = [ + {file = "xattr-0.10.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:16a660a883e703b311d1bbbcafc74fa877585ec081cd96e8dd9302c028408ab1"}, + {file = "xattr-0.10.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:1e2973e72faa87ca29d61c23b58c3c89fe102d1b68e091848b0e21a104123503"}, + {file = "xattr-0.10.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:13279fe8f7982e3cdb0e088d5cb340ce9cbe5ef92504b1fd80a0d3591d662f68"}, + {file = "xattr-0.10.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:1dc9b9f580ef4b8ac5e2c04c16b4d5086a611889ac14ecb2e7e87170623a0b75"}, + {file = "xattr-0.10.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:485539262c2b1f5acd6b6ea56e0da2bc281a51f74335c351ea609c23d82c9a79"}, + {file = "xattr-0.10.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:295b3ab335fcd06ca0a9114439b34120968732e3f5e9d16f456d5ec4fa47a0a2"}, + {file = "xattr-0.10.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:a126eb38e14a2f273d584a692fe36cff760395bf7fc061ef059224efdb4eb62c"}, + {file = "xattr-0.10.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:b0e919c24f5b74428afa91507b15e7d2ef63aba98e704ad13d33bed1288dca81"}, + {file = "xattr-0.10.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:e31d062cfe1aaeab6ba3db6bd255f012d105271018e647645941d6609376af18"}, + {file = "xattr-0.10.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:209fb84c09b41c2e4cf16dd2f481bb4a6e2e81f659a47a60091b9bcb2e388840"}, + {file = "xattr-0.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c4120090dac33eddffc27e487f9c8f16b29ff3f3f8bcb2251b2c6c3f974ca1e1"}, + {file = "xattr-0.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3e739d624491267ec5bb740f4eada93491de429d38d2fcdfb97b25efe1288eca"}, + {file = "xattr-0.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2677d40b95636f3482bdaf64ed9138fb4d8376fb7933f434614744780e46e42d"}, + {file = "xattr-0.10.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40039f1532c4456fd0f4c54e9d4e01eb8201248c321c6c6856262d87e9a99593"}, + {file = "xattr-0.10.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:148466e5bb168aba98f80850cf976e931469a3c6eb11e9880d9f6f8b1e66bd06"}, + {file = "xattr-0.10.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0aedf55b116beb6427e6f7958ccd80a8cbc80e82f87a4cd975ccb61a8d27b2ee"}, + {file = "xattr-0.10.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c3024a9ff157247c8190dd0eb54db4a64277f21361b2f756319d9d3cf20e475f"}, + {file = "xattr-0.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f1be6e733e9698f645dbb98565bb8df9b75e80e15a21eb52787d7d96800e823b"}, + {file = "xattr-0.10.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7880c8a54c18bc091a4ce0adc5c6d81da1c748aec2fe7ac586d204d6ec7eca5b"}, + {file = "xattr-0.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:89c93b42c3ba8aedbc29da759f152731196c2492a2154371c0aae3ef8ba8301b"}, + {file = "xattr-0.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6b905e808df61b677eb972f915f8a751960284358b520d0601c8cbc476ba2df6"}, + {file = "xattr-0.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ef954d0655f93a34d07d0cc7e02765ec779ff0b59dc898ee08c6326ad614d5"}, + {file = "xattr-0.10.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:199b20301b6acc9022661412346714ce764d322068ef387c4de38062474db76c"}, + {file = "xattr-0.10.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec0956a8ab0f0d3f9011ba480f1e1271b703d11542375ef73eb8695a6bd4b78b"}, + {file = "xattr-0.10.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ffcb57ca1be338d69edad93cf59aac7c6bb4dbb92fd7bf8d456c69ea42f7e6d2"}, + {file = "xattr-0.10.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f0563196ee54756fe2047627d316977dc77d11acd7a07970336e1a711e934db"}, + {file = "xattr-0.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc354f086f926a1c7f04886f97880fed1a26d20e3bc338d0d965fd161dbdb8ab"}, + {file = "xattr-0.10.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c0cd2d02ef2fb45ecf2b0da066a58472d54682c6d4f0452dfe7ae2f3a76a42ea"}, + {file = "xattr-0.10.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49626096ddd72dcc1654aadd84b103577d8424f26524a48d199847b5d55612d0"}, + {file = "xattr-0.10.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ceaa26bef8fcb17eb59d92a7481c2d15d20211e217772fb43c08c859b01afc6a"}, + {file = "xattr-0.10.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8c014c371391f28f8cd27d73ea59f42b30772cd640b5a2538ad4f440fd9190b"}, + {file = "xattr-0.10.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:46c32cd605673606b9388a313b0050ee7877a0640d7561eea243ace4fa2cc5a6"}, + {file = "xattr-0.10.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:772b22c4ff791fe5816a7c2a1c9fcba83f9ab9bea138eb44d4d70f34676232b4"}, + {file = "xattr-0.10.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:183ad611a2d70b5a3f5f7aadef0fcef604ea33dcf508228765fd4ddac2c7321d"}, + {file = "xattr-0.10.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8068df3ebdfa9411e58d5ae4a05d807ec5994645bb01af66ec9f6da718b65c5b"}, + {file = "xattr-0.10.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bc40570155beb85e963ae45300a530223d9822edfdf09991b880e69625ba38a"}, + {file = "xattr-0.10.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:436e1aaf23c07e15bed63115f1712d2097e207214fc6bcde147c1efede37e2c5"}, + {file = "xattr-0.10.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7298455ccf3a922d403339781b10299b858bb5ec76435445f2da46fb768e31a5"}, + {file = "xattr-0.10.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:986c2305c6c1a08f78611eb38ef9f1f47682774ce954efb5a4f3715e8da00d5f"}, + {file = "xattr-0.10.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:5dc6099e76e33fa3082a905fe59df766b196534c705cf7a2e3ad9bed2b8a180e"}, + {file = "xattr-0.10.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:042ad818cda6013162c0bfd3816f6b74b7700e73c908cde6768da824686885f8"}, + {file = "xattr-0.10.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9d4c306828a45b41b76ca17adc26ac3dc00a80e01a5ba85d71df2a3e948828f2"}, + {file = "xattr-0.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a606280b0c9071ef52572434ecd3648407b20df3d27af02c6592e84486b05894"}, + {file = "xattr-0.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5b49d591cf34cda2079fd7a5cb2a7a1519f54dc2e62abe3e0720036f6ed41a85"}, + {file = "xattr-0.10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8705ac6791426559c1a5c2b88bb2f0e83dc5616a09b4500899bfff6a929302"}, + {file = "xattr-0.10.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5ea974930e876bc5c146f54ac0f85bb39b7b5de2b6fc63f90364712ae368ebe"}, + {file = "xattr-0.10.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f55a2dd73a12a1ae5113c5d9cd4b4ab6bf7950f4d76d0a1a0c0c4264d50da61d"}, + {file = "xattr-0.10.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:475c38da0d3614cc5564467c4efece1e38bd0705a4dbecf8deeb0564a86fb010"}, + {file = "xattr-0.10.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:925284a4a28e369459b2b7481ea22840eed3e0573a4a4c06b6b0614ecd27d0a7"}, + {file = "xattr-0.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa32f1b45fed9122bed911de0fcc654da349e1f04fa4a9c8ef9b53e1cc98b91e"}, + {file = "xattr-0.10.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c5d3d0e728bace64b74c475eb4da6148cd172b2d23021a1dcd055d92f17619ac"}, + {file = "xattr-0.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8faaacf311e2b5cc67c030c999167a78a9906073e6abf08eaa8cf05b0416515c"}, + {file = "xattr-0.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cc6b8d5ca452674e1a96e246a3d2db5f477aecbc7c945c73f890f56323e75203"}, + {file = "xattr-0.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3725746a6502f40f72ef27e0c7bfc31052a239503ff3eefa807d6b02a249be22"}, + {file = "xattr-0.10.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:789bd406d1aad6735e97b20c6d6a1701e1c0661136be9be862e6a04564da771f"}, + {file = "xattr-0.10.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9a7a807ab538210ff8532220d8fc5e2d51c212681f63dbd4e7ede32543b070f"}, + {file = "xattr-0.10.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3e5825b5fc99ecdd493b0cc09ec35391e7a451394fdf623a88b24726011c950d"}, + {file = "xattr-0.10.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:80638d1ce7189dc52f26c234cee3522f060fadab6a8bc3562fe0ddcbe11ba5a4"}, + {file = "xattr-0.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3ff0dbe4a6ce2ce065c6de08f415bcb270ecfd7bf1655a633ddeac695ce8b250"}, + {file = "xattr-0.10.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5267e5f9435c840d2674194150b511bef929fa7d3bc942a4a75b9eddef18d8d8"}, + {file = "xattr-0.10.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b27dfc13b193cb290d5d9e62f806bb9a99b00cd73bb6370d556116ad7bb5dc12"}, + {file = "xattr-0.10.1-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:636ebdde0277bce4d12d2ef2550885804834418fee0eb456b69be928e604ecc4"}, + {file = "xattr-0.10.1-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d60c27922ec80310b45574351f71e0dd3a139c5295e8f8b19d19c0010196544f"}, + {file = "xattr-0.10.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b34df5aad035d0343bd740a95ca30db99b776e2630dca9cc1ba8e682c9cc25ea"}, + {file = "xattr-0.10.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f24a7c04ff666d0fe905dfee0a84bc899d624aeb6dccd1ea86b5c347f15c20c1"}, + {file = "xattr-0.10.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3878e1aff8eca64badad8f6d896cb98c52984b1e9cd9668a3ab70294d1ef92d"}, + {file = "xattr-0.10.1-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4abef557028c551d59cf2fb3bf63f2a0c89f00d77e54c1c15282ecdd56943496"}, + {file = "xattr-0.10.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0e14bd5965d3db173d6983abdc1241c22219385c22df8b0eb8f1846c15ce1fee"}, + {file = "xattr-0.10.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f9be588a4b6043b03777d50654c6079af3da60cc37527dbb80d36ec98842b1e"}, + {file = "xattr-0.10.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bc4ae264aa679aacf964abf3ea88e147eb4a22aea6af8c6d03ebdebd64cfd6"}, + {file = "xattr-0.10.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:827b5a97673b9997067fde383a7f7dc67342403093b94ea3c24ae0f4f1fec649"}, + {file = "xattr-0.10.1.tar.gz", hash = "sha256:c12e7d81ffaa0605b3ac8c22c2994a8e18a9cf1c59287a1b7722a2289c952ec5"}, +] + +[package.dependencies] +cffi = ">=1.0" + [[package]] name = "zipp" -version = "3.16.2" +version = "3.17.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.16.2-py3-none-any.whl", hash = "sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0"}, - {file = "zipp-3.16.2.tar.gz", hash = "sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147"}, + {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, + {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "f71573229da7f84ac223f2e0907cdf10f037895a52beb3fb40d9903819e36a98" +content-hash = "294922f7456806215f968845cfa5c123a289a54a0012f30a1af7a845da268bef" diff --git a/poetry.toml b/poetry.toml new file mode 100644 index 00000000..53b35d37 --- /dev/null +++ b/poetry.toml @@ -0,0 +1,3 @@ +[virtualenvs] +create = true +in-project = true diff --git a/pyproject.toml b/pyproject.toml index e9822a84..ac187cdc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ name = "tgtg-scanner" packages = [{include = "tgtg_scanner"}] readme = "README.md" repository = "https://github.com/Der-Henning/tgtg" -version = "1.18.0_rc1" +version = "1.18.0_rc3" [tool.poetry.dependencies] apprise = "^1.4.0" @@ -22,25 +22,31 @@ googlemaps = "^4.10.0" humanize = "^4.7.0" packaging = "^23.1" progress = "^1.6" -prometheus-client = "^0.17.0" +prometheus-client = "^0.19.0" pycron = "^3.0.0" python = ">=3.9,<3.13" python-pushsafer = "^1.1" -python-telegram-bot = "^13.15" +python-telegram-bot = {extras = ["callback-data"], version = "^20.4"} requests = "^2.31.0" [tool.poetry.group.build.dependencies] -pyinstaller = "^5.13.0" +pyinstaller = "^6.3.0" + +[tool.poetry.group.dev.dependencies] +ipykernel = "^6.25.1" +notebook = "^7.0.6" [tool.poetry.group.test.dependencies] +poetry-plugin-export = "^1.6.0" pre-commit = "^3.3.3" pytest = "^7.4.0" pytest-cov = "^4.1.0" pytest-mock = "^3.11.1" -responses = "^0.23.1" +responses = "^0.24.1" [tool.poetry.scripts] scanner = "tgtg_scanner.__main__:main" +tgtg_server = "tests.tgtg_server:main" [tool.pytest.ini_options] addopts = [ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..eec9df6b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,228 @@ +anyio==4.2.0 ; python_version >= "3.9" and python_version < "3.13" \ + --hash=sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee \ + --hash=sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f +apprise==1.7.1 ; python_version >= "3.9" and python_version < "3.13" \ + --hash=sha256:8d439d08550470524425dedee4bc8a72766c216c218f3772c37404eb2fd86e5a \ + --hash=sha256:eb2a7b546c6d4f426abb8b1006957e6a480c21215b5d780358445531611d1db7 +cachetools==5.3.2 ; python_version >= "3.9" and python_version < "3.13" \ + --hash=sha256:086ee420196f7b2ab9ca2db2520aca326318b68fe5ba8bc4d49cca91add450f2 \ + --hash=sha256:861f35a13a451f94e301ce2bec7cac63e881232ccce7ed67fab9b5df4d3beaa1 +certifi==2023.11.17 ; python_version >= "3.9" and python_version < "3.13" \ + --hash=sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1 \ + --hash=sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474 +charset-normalizer==3.3.2 ; python_version >= "3.9" and python_version < "3.13" \ + --hash=sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027 \ + --hash=sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087 \ + --hash=sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786 \ + --hash=sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8 \ + --hash=sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09 \ + --hash=sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185 \ + --hash=sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574 \ + --hash=sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e \ + --hash=sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519 \ + --hash=sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898 \ + --hash=sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269 \ + --hash=sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3 \ + --hash=sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f \ + --hash=sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6 \ + --hash=sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8 \ + --hash=sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a \ + --hash=sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73 \ + --hash=sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc \ + --hash=sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714 \ + --hash=sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2 \ + --hash=sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc \ + --hash=sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce \ + --hash=sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d \ + --hash=sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e \ + --hash=sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6 \ + --hash=sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269 \ + --hash=sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96 \ + --hash=sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d \ + --hash=sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a \ + --hash=sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4 \ + --hash=sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77 \ + --hash=sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d \ + --hash=sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0 \ + --hash=sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed \ + --hash=sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068 \ + --hash=sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac \ + --hash=sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25 \ + --hash=sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8 \ + --hash=sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab \ + --hash=sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26 \ + --hash=sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2 \ + --hash=sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db \ + --hash=sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f \ + --hash=sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5 \ + --hash=sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99 \ + --hash=sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c \ + --hash=sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d \ + --hash=sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811 \ + --hash=sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa \ + --hash=sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a \ + --hash=sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03 \ + --hash=sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b \ + --hash=sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04 \ + --hash=sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c \ + --hash=sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001 \ + --hash=sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458 \ + --hash=sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389 \ + --hash=sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99 \ + --hash=sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985 \ + --hash=sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537 \ + --hash=sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238 \ + --hash=sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f \ + --hash=sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d \ + --hash=sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796 \ + --hash=sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a \ + --hash=sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143 \ + --hash=sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8 \ + --hash=sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c \ + --hash=sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5 \ + --hash=sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5 \ + --hash=sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711 \ + --hash=sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4 \ + --hash=sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6 \ + --hash=sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c \ + --hash=sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7 \ + --hash=sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4 \ + --hash=sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b \ + --hash=sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae \ + --hash=sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12 \ + --hash=sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c \ + --hash=sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae \ + --hash=sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8 \ + --hash=sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887 \ + --hash=sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b \ + --hash=sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4 \ + --hash=sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f \ + --hash=sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5 \ + --hash=sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33 \ + --hash=sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519 \ + --hash=sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561 +click==8.1.7 ; python_version >= "3.9" and python_version < "3.13" \ + --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \ + --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de +colorama==0.4.6 ; python_version >= "3.9" and python_version < "3.13" and (sys_platform == "win32" or platform_system == "Windows") \ + --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ + --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 +colorlog==6.8.0 ; python_version >= "3.9" and python_version < "3.13" \ + --hash=sha256:4ed23b05a1154294ac99f511fabe8c1d6d4364ec1f7fc989c7fb515ccc29d375 \ + --hash=sha256:fbb6fdf9d5685f2517f388fb29bb27d54e8654dd31f58bc2a3b217e967a95ca6 +cron-descriptor==1.4.0 ; python_version >= "3.9" and python_version < "3.13" \ + --hash=sha256:b6ff4e3a988d7ca04a4ab150248e9f166fb7a5c828a85090e75bcc25aa93b4dd +exceptiongroup==1.2.0 ; python_version >= "3.9" and python_version < "3.11" \ + --hash=sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14 \ + --hash=sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68 +googlemaps==4.10.0 ; python_version >= "3.9" and python_version < "3.13" \ + --hash=sha256:3055fcbb1aa262a9159b589b5e6af762b10e80634ae11c59495bd44867e47d88 +h11==0.14.0 ; python_version >= "3.9" and python_version < "3.13" \ + --hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \ + --hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761 +httpcore==1.0.2 ; python_version >= "3.9" and python_version < "3.13" \ + --hash=sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7 \ + --hash=sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535 +httpx==0.25.2 ; python_version >= "3.9" and python_version < "3.13" \ + --hash=sha256:8b8fcaa0c8ea7b05edd69a094e63a2094c4efcb48129fb757361bc423c0ad9e8 \ + --hash=sha256:a05d3d052d9b2dfce0e3896636467f8a5342fb2b902c819428e1ac65413ca118 +humanize==4.9.0 ; python_version >= "3.9" and python_version < "3.13" \ + --hash=sha256:582a265c931c683a7e9b8ed9559089dea7edcf6cc95be39a3cbc2c5d5ac2bcfa \ + --hash=sha256:ce284a76d5b1377fd8836733b983bfb0b76f1aa1c090de2566fcf008d7f6ab16 +idna==3.6 ; python_version >= "3.9" and python_version < "3.13" \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f +importlib-metadata==7.0.1 ; python_version >= "3.9" and python_version < "3.10" \ + --hash=sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e \ + --hash=sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc +markdown==3.5.1 ; python_version >= "3.9" and python_version < "3.13" \ + --hash=sha256:5874b47d4ee3f0b14d764324d2c94c03ea66bee56f2d929da9f2508d65e722dc \ + --hash=sha256:b65d7beb248dc22f2e8a31fb706d93798093c308dc1aba295aedeb9d41a813bd +oauthlib==3.2.2 ; python_version >= "3.9" and python_version < "3.13" \ + --hash=sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca \ + --hash=sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918 +packaging==23.2 ; python_version >= "3.9" and python_version < "3.13" \ + --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \ + --hash=sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7 +progress==1.6 ; python_version >= "3.9" and python_version < "3.13" \ + --hash=sha256:c9c86e98b5c03fa1fe11e3b67c1feda4788b8d0fe7336c2ff7d5644ccfba34cd +prometheus-client==0.19.0 ; python_version >= "3.9" and python_version < "3.13" \ + --hash=sha256:4585b0d1223148c27a225b10dbec5ae9bc4c81a99a3fa80774fa6209935324e1 \ + --hash=sha256:c88b1e6ecf6b41cd8fb5731c7ae919bf66df6ec6fafa555cd6c0e16ca169ae92 +pycron==3.0.0 ; python_version >= "3.9" and python_version < "3.13" \ + --hash=sha256:b916044e3e8253d5409c68df3ac64a3472c4e608dab92f40e8f595e5d3acb3de +python-pushsafer==1.1 ; python_version >= "3.9" and python_version < "3.13" \ + --hash=sha256:833362db904896d9b598b9da12385cf1f4673829bfc8efee3c928d83f88e6f12 \ + --hash=sha256:a0f92e9118c1a0d50f90968dbcfc67c80d034fc787155b893cff7316f0827864 +python-telegram-bot[callback-data]==20.7 ; python_version >= "3.9" and python_version < "3.13" \ + --hash=sha256:462326c65671c8c39e76c8c96756ee918be6797d225f8db84d2ec0f883383b8c \ + --hash=sha256:4f146c39de5f5e0b3723c2abedaf78046ebd30a6a49d2281ee4b3af5eb116b68 +pyyaml==6.0.1 ; python_version >= "3.9" and python_version < "3.13" \ + --hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \ + --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \ + --hash=sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df \ + --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \ + --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \ + --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \ + --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \ + --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \ + --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \ + --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \ + --hash=sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290 \ + --hash=sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9 \ + --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \ + --hash=sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6 \ + --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \ + --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \ + --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \ + --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \ + --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \ + --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \ + --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \ + --hash=sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0 \ + --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \ + --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \ + --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \ + --hash=sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28 \ + --hash=sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4 \ + --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \ + --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \ + --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \ + --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \ + --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \ + --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \ + --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \ + --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \ + --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \ + --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \ + --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \ + --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \ + --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \ + --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \ + --hash=sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54 \ + --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \ + --hash=sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b \ + --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \ + --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \ + --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \ + --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \ + --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \ + --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f +requests-oauthlib==1.3.1 ; python_version >= "3.9" and python_version < "3.13" \ + --hash=sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5 \ + --hash=sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a +requests==2.31.0 ; python_version >= "3.9" and python_version < "3.13" \ + --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ + --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 +sniffio==1.3.0 ; python_version >= "3.9" and python_version < "3.13" \ + --hash=sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101 \ + --hash=sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384 +typing-extensions==4.9.0 ; python_version >= "3.9" and python_version < "3.11" \ + --hash=sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783 \ + --hash=sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd +urllib3==2.1.0 ; python_version >= "3.9" and python_version < "3.13" \ + --hash=sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3 \ + --hash=sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54 +zipp==3.17.0 ; python_version >= "3.9" and python_version < "3.10" \ + --hash=sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31 \ + --hash=sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0 diff --git a/scanner.spec b/scanner.spec index a69b3572..d5ce9d93 100644 --- a/scanner.spec +++ b/scanner.spec @@ -41,7 +41,7 @@ exe = EXE( [], name='scanner', debug=False, - bootloader_ignore_signals=False, + bootloader_ignore_signals=True, strip=False, upx=True, upx_exclude=[], diff --git a/tests/conftest.py b/tests/conftest.py index d8b0f145..c158d252 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,3 @@ -import shutil -from pathlib import Path - import pytest from tgtg_scanner.models import Item @@ -15,32 +12,14 @@ def item_properties(): "display_name", "pickup_location", "items_available", - "favorite" - ], - "STORE_PROPERTIES": [ - "store_id" + "favorite", ], - "ITEM_PROPERTIES": [ - "item_id", - "price_including_taxes", - "description" - ], - "PRICE_PROPERTIES": [ - "code", - "minor_units", - "decimals" - ] + "STORE_PROPERTIES": ["store_id"], + "ITEM_PROPERTIES": ["item_id", "price_including_taxes", "description"], + "PRICE_PROPERTIES": ["code", "minor_units", "decimals"], } -@pytest.fixture -def temp_path(): - temp_path = Path("./pytest_tmp") - temp_path.mkdir(exist_ok=True) - yield temp_path - shutil.rmtree(temp_path) - - @pytest.fixture def test_item(tgtg_item: dict): return Item(tgtg_item) @@ -49,136 +28,111 @@ def test_item(tgtg_item: dict): @pytest.fixture def tgtg_item(): return { - 'item': { - 'item_id': '774625', - 'sales_taxes': [ - {'tax_description': 'USt.', 'tax_percentage': 7.0} - ], - 'tax_amount': { - 'code': 'EUR', - 'minor_units': 20, - 'decimals': 2 - }, - 'price_excluding_taxes': { - 'code': 'EUR', - 'minor_units': 280, - 'decimals': 2 + "item": { + "item_id": "774625", + "sales_taxes": [{"tax_description": "USt.", "tax_percentage": 7.0}], + "tax_amount": {"code": "EUR", "minor_units": 20, "decimals": 2}, + "price_excluding_taxes": {"code": "EUR", "minor_units": 280, "decimals": 2}, + "price_including_taxes": {"code": "EUR", "minor_units": 300, "decimals": 2}, + "value_excluding_taxes": {"code": "EUR", "minor_units": 841, "decimals": 2}, + "value_including_taxes": {"code": "EUR", "minor_units": 900, "decimals": 2}, + "taxation_policy": "PRICE_INCLUDES_TAXES", + "show_sales_taxes": False, + "cover_picture": { + "picture_id": "282115", + "current_url": "https://images.tgtg.ninja/standard_images/Chinese/korean-food-1699781_1280.jpg", + "is_automatically_created": False, }, - 'price_including_taxes': { - 'code': 'EUR', - 'minor_units': 300, - 'decimals': 2 + "logo_picture": { + "picture_id": "768433", + "current_url": "https://images.tgtg.ninja/store/e7fee96e-318a-4056-aaff-496794906be1.png", + "is_automatically_created": False, }, - 'value_excluding_taxes': { - 'code': 'EUR', - 'minor_units': 841, - 'decimals': 2 - }, - 'value_including_taxes': { - 'code': 'EUR', - 'minor_units': 900, - 'decimals': 2 - }, - 'taxation_policy': 'PRICE_INCLUDES_TAXES', - 'show_sales_taxes': False, - 'cover_picture': { - 'picture_id': '282115', - 'current_url': 'https://images.tgtg.ninja/standard_images' - '/Chinese/korean-food-1699781_1280.jpg', - 'is_automatically_created': False - }, - 'logo_picture': { - 'picture_id': '768433', - 'current_url': 'https://images.tgtg.ninja/store/e7fee96e-' - '318a-4056-aaff-496794906be1.png', - 'is_automatically_created': False - }, - 'name': '', - 'description': 'Rette eine Magic Bag mit leckerem ' - 'indischen Essen.', - 'food_handling_instructions': '', - 'can_user_supply_packaging': False, - 'packaging_option': 'BAG_ALLOWED', - 'collection_info': 'Wir befinden uns im 2. Obergeschoss ' - 'in der Europapassage.', - 'diet_categories': [], - 'item_category': 'MEAL', - 'buffet': False, - 'badges': [ + "name": "", + "description": "Rette eine Magic Bag mit leckerem indischen Essen.", + "food_handling_instructions": "", + "can_user_supply_packaging": False, + "packaging_option": "BAG_ALLOWED", + "collection_info": "Wir befinden uns im 2. Obergeschoss in der Europapassage.", + "diet_categories": [], + "item_category": "MEAL", + "buffet": False, + "badges": [ { - 'badge_type': 'SERVICE_RATING_SCORE', - 'rating_group': 'LIKED', - 'percentage': 85, - 'user_count': 162, - 'month_count': 5 + "badge_type": "SERVICE_RATING_SCORE", + "rating_group": "LIKED", + "percentage": 85, + "user_count": 162, + "month_count": 5, } ], - 'positive_rating_reasons': [ - 'POSITIVE_FEEDBACK_DELICIOUS_FOOD', - 'POSITIVE_FEEDBACK_GREAT_VALUE', - 'POSITIVE_FEEDBACK_QUICK_COLLECTION', - 'POSITIVE_FEEDBACK_FRIENDLY_STAFF', - 'POSITIVE_FEEDBACK_GREAT_QUANTITY', - 'POSITIVE_FEEDBACK_GREAT_VARIETY' + "positive_rating_reasons": [ + "POSITIVE_FEEDBACK_DELICIOUS_FOOD", + "POSITIVE_FEEDBACK_GREAT_VALUE", + "POSITIVE_FEEDBACK_QUICK_COLLECTION", + "POSITIVE_FEEDBACK_FRIENDLY_STAFF", + "POSITIVE_FEEDBACK_GREAT_QUANTITY", + "POSITIVE_FEEDBACK_GREAT_VARIETY", ], - 'average_overall_rating': { - 'average_overall_rating': 3.3333333333333335, - 'rating_count': 162, - 'month_count': 6 + "average_overall_rating": { + "average_overall_rating": 3.3333333333333335, + "rating_count": 162, + "month_count": 6, }, - 'favorite_count': 0 + "favorite_count": 0, }, - 'store': { - 'store_id': '758373', - 'store_name': 'Chutney Indian Food', - 'branch': 'Hamburg – Europapassage 2.OG', - 'description': '', - 'tax_identifier': 'DE252292855', - 'website': '', - 'store_location': { - 'address': { - 'country': {'iso_code': 'DE', 'name': 'Germany'}, - 'address_line': 'Ballindamm 40, 20095 Hamburg, ' - 'Deutschland', - 'city': '', - 'postal_code': '' - }, - 'location': {'longitude': 9.99532, 'latitude': 53.55182}}, - 'logo_picture': { - 'picture_id': '768433', - 'current_url': 'https://images.tgtg.ninja/store/e7fee96e-' - '318a-4056-aaff-496794906be1.png', - 'is_automatically_created': False + "store": { + "store_id": "758373", + "store_name": "Chutney Indian Food", + "branch": "Hamburg – Europapassage 2.OG", + "description": "", + "tax_identifier": "DE252292855", + "website": "", + "store_location": { + "address": { + "country": {"iso_code": "DE", "name": "Germany"}, + "address_line": "Ballindamm 40, 20095 Hamburg, Deutschland", + "city": "", + "postal_code": "", + }, + "location": {"longitude": 9.99532, "latitude": 53.55182}, }, - 'store_time_zone': 'Europe/Berlin', - 'hidden': False, - 'favorite_count': 0, - 'we_care': False, - 'distance': 0.13365150729215916, - 'cover_picture': { - 'picture_id': '282115', - 'current_url': 'https://images.tgtg.ninja/standard_images/' - 'Chinese/korean-food-1699781_1280.jpg', - 'is_automatically_created': False + "logo_picture": { + "picture_id": "768433", + "current_url": "https://images.tgtg.ninja/store/e7fee96e-318a-4056-aaff-496794906be1.png", + "is_automatically_created": False, }, - 'is_manufacturer': False}, - 'display_name': 'Chutney Indian Food - Hamburg - Europapassage 2.OG', - 'pickup_interval': { - 'start': '2022-12-30T19:00:00Z', - 'end': '2022-12-30T19:30:00Z' + "store_time_zone": "Europe/Berlin", + "hidden": False, + "favorite_count": 0, + "we_care": False, + "distance": 0.13365150729215916, + "cover_picture": { + "picture_id": "282115", + "current_url": "https://images.tgtg.ninja/standard_images/Chinese/korean-food-1699781_1280.jpg", + "is_automatically_created": False, + }, + "is_manufacturer": False, + }, + "display_name": "Chutney Indian Food - Hamburg - Europapassage 2.OG", + "pickup_interval": { + "start": "2022-12-30T19:00:00Z", + "end": "2022-12-30T19:30:00Z", }, - 'pickup_location': { - 'address': { - 'country': {'iso_code': 'DE', 'name': 'Germany'}, - 'address_line': 'Ballindamm 40, 20095 Hamburg, Deutschland', - 'city': '', - 'postal_code': '' + "pickup_location": { + "address": { + "country": {"iso_code": "DE", "name": "Germany"}, + "address_line": "Ballindamm 40, 20095 Hamburg, Deutschland", + "city": "", + "postal_code": "", }, - 'location': {'longitude': 9.99532, 'latitude': 53.55182}}, - 'purchase_end': '2022-12-30T19:30:00Z', - 'items_available': 3, - 'distance': 0.13365150729215916, - 'favorite': False, - 'in_sales_window': True, - 'new_item': False, - 'item_type': 'MAGIC_BAG'} + "location": {"longitude": 9.99532, "latitude": 53.55182}, + }, + "purchase_end": "2022-12-30T19:30:00Z", + "items_available": 3, + "distance": 0.13365150729215916, + "favorite": False, + "in_sales_window": True, + "new_item": False, + "item_type": "MAGIC_BAG", + } diff --git a/tests/test_config.py b/tests/test_config.py index 6acf7a71..538a0b8f 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,123 +1,115 @@ import configparser -from importlib import reload -from pathlib import Path +import platform +import tempfile +from random import randint +from uuid import uuid4 import pytest -import tgtg_scanner.models.config -from tgtg_scanner.models.cron import Cron +from tgtg_scanner.models import Config, Cron + +SYS_PLATFORM = platform.system() +IS_WINDOWS = SYS_PLATFORM.lower() in {"windows", "cygwin"} def test_default_ini_config(): - reload(tgtg_scanner.models.config) - config = tgtg_scanner.models.config.Config("") - for key in tgtg_scanner.models.config.DEFAULT_CONFIG: - assert hasattr(config, key) - assert getattr( - config, key) == tgtg_scanner.models.config.DEFAULT_CONFIG.get(key) + with tempfile.NamedTemporaryFile(delete=not IS_WINDOWS) as temp_file: + config = Config(temp_file.name) + assert hasattr(config, "metrics_port") + assert config.metrics_port == 8000 def test_default_env_config(): - reload(tgtg_scanner.models.config) - config = tgtg_scanner.models.config.Config() - for key in tgtg_scanner.models.config.DEFAULT_CONFIG: - assert hasattr(config, key) - assert getattr( - config, key) == tgtg_scanner.models.config.DEFAULT_CONFIG.get(key) - - -def test_config_set(temp_path: Path): - reload(tgtg_scanner.models.config) - config_path = Path(temp_path, "config.ini") - config_path.touch(exist_ok=True) - config = tgtg_scanner.models.config.Config(config_path.absolute()) - - assert config.set("MAIN", "debug", True) - - config_parser = configparser.ConfigParser() - config_parser.read(config_path, encoding='utf-8') - - assert config_parser.getboolean("MAIN", "debug") - - -def test_save_tokens_to_ini(temp_path: Path): - reload(tgtg_scanner.models.config) - config_path = Path(temp_path, "config.ini") - config_path.touch(exist_ok=True) - config = tgtg_scanner.models.config.Config(config_path.absolute()) - config.save_tokens("test_access_token", "test_refresh_token", - "test_user_id", "test_cookie") - - config_parser = configparser.ConfigParser() - config_parser.read(config_path, encoding='utf-8') - - assert config_parser.get("TGTG", "AccessToken") == "test_access_token" - assert config_parser.get("TGTG", "RefreshToken") == "test_refresh_token" - assert config_parser.get("TGTG", "UserId") == "test_user_id" - assert config_parser.get("TGTG", "Datadome") == "test_cookie" - - -def test_token_path(temp_path: Path, monkeypatch: pytest.MonkeyPatch): - reload(tgtg_scanner.models.config) - monkeypatch.setenv("TGTG_TOKEN_PATH", str(temp_path.absolute())) - - config = tgtg_scanner.models.config.Config() - config.save_tokens("test_access_token", "test_refresh_token", - "test_user_id", "test_cookie") - config._load_tokens() - - assert config.tgtg.get("access_token") == "test_access_token" - assert config.tgtg.get("refresh_token") == "test_refresh_token" - assert config.tgtg.get("user_id") == "test_user_id" - assert config.tgtg.get("datadome") == "test_cookie" - - -def test_ini_get(temp_path: Path): - reload(tgtg_scanner.models.config) - config_path = Path(temp_path, "config.ini") - - with open(config_path, 'w', encoding='utf-8') as file: - file.writelines([ - "[MAIN]\n", - "Debug = true\n", - "ItemIDs = 23423, 32432, 234532\n", - "[WEBHOOK]\n", - "timeout = 42\n", - 'headers = {"Accept": "json"}\n', - "cron = * * 1-5 * *\n", - 'body = {"content": "${{items_available}} panier(s) à ' - '${{price}} € \\nÀ récupérer"}' - ]) - - config = tgtg_scanner.models.config.Config(config_path.absolute()) - - assert config.debug is True - assert config.item_ids == ["23423", "32432", "234532"] - assert config.webhook.get("timeout") == 42 - assert config.webhook.get("headers") == {"Accept": "json"} - assert config.webhook.get("cron") == Cron("* * 1-5 * *") - assert config.webhook.get("body") == ('{"content": "${{items_available}} ' - 'panier(s) à ${{price}} € \n' - 'À récupérer"}') + config = Config() + assert hasattr(config, "metrics_port") + assert config.metrics_port == 8000 + + +def test_config_set(): + with tempfile.NamedTemporaryFile(delete=not IS_WINDOWS) as temp_file: + config = Config(temp_file.name) + + assert config.set("MAIN", "debug", True) + + config_parser = configparser.ConfigParser() + config_parser.read(temp_file.name, encoding="utf-8") + + assert config_parser.getboolean("MAIN", "debug") + + +def test_save_tokens_to_ini(): + with tempfile.NamedTemporaryFile(delete=not IS_WINDOWS) as temp_file: + access_token = uuid4().hex + refresh_token = uuid4().hex + user_id = str(randint(10**9, 10**10 - 1)) + datadome = uuid4().hex + config = Config(temp_file.name) + config.save_tokens(access_token, refresh_token, user_id, datadome) + + config_parser = configparser.ConfigParser() + config_parser.read(temp_file.name, encoding="utf-8") + + assert config_parser.get("TGTG", "AccessToken") == access_token + assert config_parser.get("TGTG", "RefreshToken") == refresh_token + assert config_parser.get("TGTG", "UserId") == user_id + assert config_parser.get("TGTG", "Datadome") == datadome + + +def test_token_path(monkeypatch: pytest.MonkeyPatch): + with tempfile.TemporaryDirectory() as temp_dir: + monkeypatch.setenv("TGTG_TOKEN_PATH", temp_dir) + access_token = uuid4().hex + refresh_token = uuid4().hex + user_id = uuid4().hex + datadome = uuid4().hex + config = Config() + config.save_tokens(access_token, refresh_token, user_id, datadome) + config._load_tokens() + + assert config.tgtg.access_token == access_token + assert config.tgtg.refresh_token == refresh_token + assert config.tgtg.user_id == user_id + assert config.tgtg.datadome == datadome + + +def test_ini_get(): + with tempfile.NamedTemporaryFile(delete=not IS_WINDOWS) as temp_file: + content = ( + "[MAIN]\n" + "Debug = true\n" + "ItemIDs = 23423, 32432, 234532\n" + "[WEBHOOK]\n" + "timeout = 42\n" + 'headers = {"Accept": "json"}\n' + "cron = * * 1-5 * *\n" + 'body = {"content": "${{items_available}} panier(s) à ${{price}} € \\nÀ récupérer 🍔"}' + ) + + temp_file.write(content.encode("utf-8")) + temp_file.seek(0) + config = Config(temp_file.name) + + assert config.debug is True + assert config.item_ids == ["23423", "32432", "234532"] + assert config.webhook.timeout == 42 + assert config.webhook.headers == {"Accept": "json"} + assert config.webhook.cron == Cron("* * 1-5 * *") + assert config.webhook.body == '{"content": "${{items_available}} panier(s) à ${{price}} € \nÀ récupérer 🍔"}' def test_env_get(monkeypatch: pytest.MonkeyPatch): - reload(tgtg_scanner.models.config) monkeypatch.setenv("DEBUG", "true") monkeypatch.setenv("ITEM_IDS", "23423, 32432, 234532") monkeypatch.setenv("WEBHOOK_TIMEOUT", "42") monkeypatch.setenv("WEBHOOK_HEADERS", '{"Accept": "json"}') monkeypatch.setenv("WEBHOOK_CRON", "* * 1-5 * *") - monkeypatch.setenv("WEBHOOK_BODY", '{"content": "${{items_available}} ' - 'panier(s) à ${{price}} € \\nÀ récupérer"}') + monkeypatch.setenv("WEBHOOK_BODY", '{"content": "${{items_available}} panier(s) à ${{price}} € \\nÀ récupérer 🍔"}') - config = tgtg_scanner.models.config.Config() + config = Config() assert config.debug is True assert config.item_ids == ["23423", "32432", "234532"] - assert config.webhook.get("timeout") == 42 - assert config.webhook.get("headers") == {"Accept": "json"} - assert config.webhook.get("cron") == Cron("* * 1-5 * *") - assert config.webhook.get("body") == ('{"content": "${{items_available}} ' - 'panier(s) à ${{price}} € \n' - 'À récupérer"}') + assert config.webhook.timeout == 42 + assert config.webhook.headers == {"Accept": "json"} + assert config.webhook.cron == Cron("* * 1-5 * *") + assert config.webhook.body == '{"content": "${{items_available}} panier(s) à ${{price}} € \nÀ récupérer 🍔"}' diff --git a/tests/test_cron.py b/tests/test_cron.py index e8a9551c..386b7411 100644 --- a/tests/test_cron.py +++ b/tests/test_cron.py @@ -1,36 +1,29 @@ - import pytest from tgtg_scanner.models.cron import Cron -from tgtg_scanner.models.errors import ConfigurationError def test_description(): assert Cron().get_description() == "Every minute" assert Cron("0 0 * * *").get_description() == "At 00:00" - assert Cron("0 0 * * 0").get_description() == \ - "At 00:00, only on Sunday" - assert Cron("0 0 * * 0-1").get_description() == \ - "At 00:00, Sunday through Monday" - assert Cron("0 0 * * 0-1").get_description("de_DE") == \ - "Um 00:00, Sunday bis Monday" - assert Cron("0 0 * * 0-1").get_description("fr_FR") == \ - "À 00:00, de Sunday à Monday" - assert Cron("0 0 * * 0-1").get_description("it_IT") == \ - "Alle 00:00, Sunday al Monday" - assert Cron("* * * * 0-6").get_description() == \ - "Every minute, Sunday through Saturday" - assert Cron("* * * * 1-5").get_description() == \ - "Every minute, Monday through Friday" - assert Cron("* 6-22 * * *").get_description() == \ - "Every minute, between 06:00 and 22:59" - assert Cron("* 6-22 * * 1-5; * 19-22 * * 0,6").get_description() == \ - ("Every minute, between 06:00 and 22:59, Monday through Friday; " - "Every minute, between 19:00 and 22:59, only on Sunday and Saturday") - with pytest.raises(ConfigurationError): + assert Cron("0 0 * * 0").get_description() == "At 00:00, only on Sunday" + assert Cron("0 0 * * 0-1").get_description() == "At 00:00, Sunday through Monday" + assert Cron("0 0 * * 0-1").get_description("de_DE") == "Um 00:00, Sunday bis Monday" + assert Cron("0 0 * * 0-1").get_description("fr_FR") == "À 00:00, de Sunday à Monday" + assert Cron("0 0 * * 0-1").get_description("it_IT") == "Alle 00:00, Sunday al Monday" + assert Cron("* * * * 0-6").get_description() == "Every minute, Sunday through Saturday" + assert Cron("* * * * 1-5").get_description() == "Every minute, Monday through Friday" + assert Cron("* 6-22 * * *").get_description() == "Every minute, between 06:00 and 22:59" + assert Cron("* 6-22 * * 1-5; * 19-22 * * 0,6").get_description() == ( + "Every minute, between 06:00 and 22:59, Monday through Friday; " + "Every minute, between 19:00 and 22:59, only on Sunday and Saturday" + ) + with pytest.raises(ValueError): Cron("* * * * 0-7") - with pytest.raises(ConfigurationError): + with pytest.raises(ValueError): Cron("* * * * 7") + with pytest.raises(ValueError): + Cron("abc") def test_is_now(): diff --git a/tests/test_favorites.py b/tests/test_favorites.py index b4ae8fb2..35992439 100644 --- a/tests/test_favorites.py +++ b/tests/test_favorites.py @@ -2,7 +2,7 @@ import pytest -from tgtg_scanner.models.errors import TgtgAPIError +from tgtg_scanner.errors import TgtgAPIError from tgtg_scanner.models.favorites import Favorites @@ -13,9 +13,8 @@ def favorites(): def test_is_item_favorite(favorites: Favorites, tgtg_item: dict): - favorites.client.get_favorites.return_value = [] - is_favorite = favorites.is_item_favorite( - tgtg_item.get("item", {}).get("item_id")) + favorites.client.get_favorites.return_value = [] # type: ignore[attr-defined] + is_favorite = favorites.is_item_favorite(tgtg_item.get("item", {}).get("item_id")) assert is_favorite == tgtg_item.get("favorite") is_favorite = favorites.is_item_favorite("123") @@ -24,8 +23,13 @@ def test_is_item_favorite(favorites: Favorites, tgtg_item: dict): def test_get_item_by_id(favorites: Favorites, tgtg_item: dict): item_id = tgtg_item.get("item", {}).get("item_id") - favorites.client.get_item.side_effect = lambda x: tgtg_item \ - if x == item_id else (_ for _ in ()).throw(TgtgAPIError()) + + def side_effect(x): + if x == item_id: + return tgtg_item + raise TgtgAPIError() + + favorites.client.get_item.side_effect = side_effect # type: ignore[attr-defined] item = favorites.get_item_by_id(item_id) assert item.item_id == item_id assert item.display_name == tgtg_item.get("display_name") @@ -38,24 +42,23 @@ def test_get_item_by_id(favorites: Favorites, tgtg_item: dict): def test_get_favorites(favorites: Favorites, tgtg_item: dict): - favorites.client.get_favorites.return_value = [tgtg_item] - favorites = favorites.get_favorites() - assert len(favorites) == 1 - assert favorites[0].item_id == tgtg_item.get("item", {}).get("item_id") - assert favorites[0].display_name == tgtg_item.get("display_name") - assert favorites[0].items_available == tgtg_item.get("items_available") + favorites.client.get_favorites.return_value = [tgtg_item] # type: ignore[attr-defined] + items = favorites.get_favorites() + assert len(items) == 1 + assert items[0].item_id == tgtg_item.get("item", {}).get("item_id") + assert items[0].display_name == tgtg_item.get("display_name") + assert items[0].items_available == tgtg_item.get("items_available") def test_add_favorites(favorites: Favorites): set_favorite_mock = MagicMock() - favorites.client.set_favorite = set_favorite_mock + favorites.client.set_favorite = set_favorite_mock # type: ignore[method-assign] favorites.add_favorites(["123", "234"]) set_favorite_mock.assert_has_calls([call("123", True), call("234", True)]) def test_remove_favorites(favorites: Favorites): set_favorite_mock = MagicMock() - favorites.client.set_favorite = set_favorite_mock + favorites.client.set_favorite = set_favorite_mock # type: ignore[method-assign] favorites.remove_favorite(["123", "234"]) - set_favorite_mock.assert_has_calls( - [call("123", False), call("234", False)]) + set_favorite_mock.assert_has_calls([call("123", False), call("234", False)]) diff --git a/tests/test_location.py b/tests/test_location.py index 7470a626..cc00bd7b 100644 --- a/tests/test_location.py +++ b/tests/test_location.py @@ -7,19 +7,16 @@ def test_calculate_distance_time(mocker: MockerFixture): google_api_key = "AIza123456" location = "Hauptstraße 1, 20099 Hamburg, Germany" - mocker.patch('googlemaps.Client.geocode', return_value=[{}]) - mocker.patch('googlemaps.Client.directions', return_value=[{ - "legs": [{ - "distance": {"value": 1500}, - "duration": {"value": 1200} - }] - }]) + mocker.patch("googlemaps.Client.geocode", return_value=[{}]) + mocker.patch( + "googlemaps.Client.directions", + return_value=[{"legs": [{"distance": {"value": 1500}, "duration": {"value": 1200}}]}], + ) - distance_time_calculator = Location( - True, google_api_key, location) - distance_time = distance_time_calculator.calculate_distance_time( - "München", Location.WALKING_MODE) + distance_time_calculator = Location(True, google_api_key, location) + distance_time = distance_time_calculator.calculate_distance_time("München", Location.WALKING_MODE) + assert distance_time is not None assert distance_time.distance == 1500 assert distance_time.duration == 1200 assert distance_time.travel_mode == Location.WALKING_MODE diff --git a/tests/test_notifiers.py b/tests/test_notifiers.py index df688047..b681917f 100644 --- a/tests/test_notifiers.py +++ b/tests/test_notifiers.py @@ -1,118 +1,123 @@ import json import platform -from importlib import reload from time import sleep +from unittest.mock import MagicMock import pytest import responses +from pytest_mock.plugin import MockerFixture -import tgtg_scanner.models.config -from tgtg_scanner.models.item import Item +from tgtg_scanner.models import Config, Cron, Favorites, Item, Reservations from tgtg_scanner.notifiers.apprise import Apprise from tgtg_scanner.notifiers.console import Console from tgtg_scanner.notifiers.ifttt import IFTTT from tgtg_scanner.notifiers.ntfy import Ntfy from tgtg_scanner.notifiers.script import Script +from tgtg_scanner.notifiers.smtp import SMTP +from tgtg_scanner.notifiers.telegram import Telegram from tgtg_scanner.notifiers.webhook import WebHook SYS_PLATFORM = platform.system() -IS_WINDOWS = SYS_PLATFORM.lower() in ('windows', 'cygwin') +IS_WINDOWS = SYS_PLATFORM.lower() in {"windows", "cygwin"} + + +@pytest.fixture +def reservations() -> Reservations: + return MagicMock() + + +@pytest.fixture +def favorites() -> Favorites: + return MagicMock() @responses.activate -def test_webhook_json(test_item: Item): - reload(tgtg_scanner.models.config) - config = tgtg_scanner.models.config.Config("") - config._setattr("webhook.enabled", True) - config._setattr("webhook.method", "POST") - config._setattr("webhook.url", "https://api.example.com") - config._setattr("webhook.type", "application/json") - config._setattr("webhook.headers", {"Accept": "json"}) - config._setattr("webhook.body", - '{"content": "${{items_available}} panier(s) ' - 'disponible(s) à ${{price}} € \nÀ récupérer ' - '${{pickupdate}}\n' - 'https://toogoodtogo.com/item/${{item_id}}"' - ', "username": "${{display_name}}"}') - responses.add( - responses.POST, - "https://api.example.com", - status=200 +def test_webhook_json(test_item: Item, reservations: Reservations, favorites: Favorites): + config = Config() + config.webhook.enabled = True + config.webhook.method = "POST" + config.webhook.url = "https://api.example.com" + config.webhook.type = "application/json" + config.webhook.headers = {"Accept": "json"} + config.webhook.cron = Cron() + config.webhook.body = ( + '{"content": "${{items_available}} panier(s) disponible(s) à ${{price}} € \nÀ récupérer ${{pickupdate}}' + '\nhttps://toogoodtogo.com/item/${{item_id}}", "username": "${{display_name}}"}' ) + responses.add(responses.POST, "https://api.example.com", status=200) - webhook = WebHook(config) + webhook = WebHook(config, reservations, favorites) + webhook.start() webhook.send(test_item) + webhook.stop() request = responses.calls[0].request body = json.loads(request.body) assert request.headers.get("Accept") == "json" - assert request.headers.get( - "Content-Type") == "application/json" + assert request.headers.get("Content-Type") == "application/json" assert body == { - "content": (f"{test_item.items_available} panier(s) disponible(s) à " - f"{test_item.price} € \nÀ récupérer {test_item.pickupdate}" - f"\nhttps://toogoodtogo.com/item/{test_item.item_id}"), - "username": f"{test_item.display_name}"} + "content": ( + f"{test_item.items_available} panier(s) disponible(s) à {test_item.price} € \nÀ récupérer {test_item.pickupdate}" + f"\nhttps://toogoodtogo.com/item/{test_item.item_id}" + ), + "username": f"{test_item.display_name}", + } @responses.activate -def test_webhook_text(test_item: Item): - reload(tgtg_scanner.models.config) - config = tgtg_scanner.models.config.Config("") - config._setattr("webhook.enabled", True) - config._setattr("webhook.method", "POST") - config._setattr("webhook.url", "https://api.example.com") - config._setattr("webhook.type", "text/plain") - config._setattr("webhook.headers", {"Accept": "json"}) - config._setattr("webhook.body", - '${{items_available}} panier(s) ' - 'disponible(s) à ${{price}} € \nÀ récupérer ' - '${{pickupdate}}\n' - 'https://toogoodtogo.com/item/${{item_id}}') - responses.add( - responses.POST, - "https://api.example.com", - status=200 +def test_webhook_text(test_item: Item, reservations: Reservations, favorites: Favorites): + config = Config() + config.webhook.enabled = True + config.webhook.method = "POST" + config.webhook.url = "https://api.example.com" + config.webhook.type = "text/plain" + config.webhook.headers = {"Accept": "json"} + config.webhook.cron = Cron() + config.webhook.body = ( + "${{items_available}} panier(s) disponible(s) à ${{price}} € \n" + "À récupérer ${{pickupdate}}\nhttps://toogoodtogo.com/item/${{item_id}}" ) + responses.add(responses.POST, "https://api.example.com", status=200) - webhook = WebHook(config) + webhook = WebHook(config, reservations, favorites) + webhook.start() webhook.send(test_item) + webhook.stop() request = responses.calls[0].request assert request.headers.get("Accept") == "json" assert request.headers.get("Content-Type") == "text/plain" - assert request.body.decode('utf-8') == ( - f"{test_item.items_available} panier(s) disponible(s) à " - f"{test_item.price} € \nÀ récupérer {test_item.pickupdate}" - f"\nhttps://toogoodtogo.com/item/{test_item.item_id}") + assert request.body.decode("utf-8") == ( + f"{test_item.items_available} panier(s) disponible(s) à {test_item.price} € \n" + f"À récupérer {test_item.pickupdate}\nhttps://toogoodtogo.com/item/{test_item.item_id}" + ) @responses.activate -def test_ifttt(test_item: Item): - reload(tgtg_scanner.models.config) - config = tgtg_scanner.models.config.Config("") - config._setattr("ifttt.enabled", True) - config._setattr("ifttt.event", "tgtg_notification") - config._setattr("ifttt.key", "secret_key") - config._setattr("ifttt.body", - '{"value1": "${{display_name}}", ' - '"value2": ${{items_available}}, ' - '"value3": "https://share.toogoodtogo.com/' - 'item/${{item_id}}"}') +def test_ifttt(test_item: Item, reservations: Reservations, favorites: Favorites): + config = Config() + config.ifttt.enabled = True + config.ifttt.event = "tgtg_notification" + config.ifttt.key = "secret_key" + config.ifttt.cron = Cron() + config.ifttt.body = ( + '{"value1": "${{display_name}}", "value2": ${{items_available}}, ' + '"value3": "https://share.toogoodtogo.com/item/${{item_id}}"}' + ) responses.add( responses.POST, - f"https://maker.ifttt.com/trigger/" - f"{config.ifttt.get('event')}" - f"/with/key/{config.ifttt.get('key')}", + f"https://maker.ifttt.com/trigger/{config.ifttt.event}/with/key/{config.ifttt.key}", body="Congratulations! You've fired the tgtg_notification event", content_type="text/plain", - status=200 + status=200, ) - ifttt = IFTTT(config) + ifttt = IFTTT(config, reservations, favorites) + ifttt.start() ifttt.send(test_item) + ifttt.stop() request = responses.calls[0].request body = json.loads(request.body) @@ -121,96 +126,200 @@ def test_ifttt(test_item: Item): assert body == { "value1": test_item.display_name, "value2": test_item.items_available, - "value3": f"https://share.toogoodtogo.com/item/{test_item.item_id}"} + "value3": f"https://share.toogoodtogo.com/item/{test_item.item_id}", + } @responses.activate -def test_ntfy(test_item: Item): - reload(tgtg_scanner.models.config) - config = tgtg_scanner.models.config.Config("") - config._setattr("ntfy.enabled", True) - config._setattr("ntfy.server", "https://ntfy.sh") - config._setattr("ntfy.topic", "tgtg_test") - config._setattr("ntfy.title", "New Items - ${{display_name}}") - config._setattr("ntfy.body", - '${{display_name}} - New Amount: ${{items_available}} - ' - 'https://share.toogoodtogo.com/item/${{item_id}}') +def test_ntfy(test_item: Item, reservations: Reservations, favorites: Favorites): + config = Config() + config.ntfy.enabled = True + config.ntfy.server = "https://ntfy.sh" + config.ntfy.topic = "tgtg_test" + config.ntfy.title = "New Items - ${{display_name}}" + config.ntfy.cron = Cron() + config.ntfy.body = "${{display_name}} - New Amount: ${{items_available}} - https://share.toogoodtogo.com/item/${{item_id}}" responses.add( responses.POST, - f"{config.ntfy.get('server')}/{config.ntfy.get('topic')}", - status=200 + f"{config.ntfy.server}/{config.ntfy.topic}", + status=200, ) - ntfy = Ntfy(config) + ntfy = Ntfy(config, reservations, favorites) + ntfy.start() ntfy.send(test_item) + ntfy.stop() request = responses.calls[0].request - assert request.url == ( - f"{config.ntfy.get('server')}/" - f"{config.ntfy.get('topic')}") - assert request.headers.get('X-Message').decode('utf-8') == ( - f'{test_item.display_name} - New Amount: {test_item.items_available} ' - f'- https://share.toogoodtogo.com/item/{test_item.item_id}') - assert request.headers.get('X-Title').decode('utf-8') == ( - f'New Items - {test_item.display_name}') + assert request.url == (f"{config.ntfy.server}/" f"{config.ntfy.topic}") + assert request.headers.get("X-Message").decode("utf-8") == ( + f"{test_item.display_name} - New Amount: {test_item.items_available} - " + f"https://share.toogoodtogo.com/item/{test_item.item_id}" + ) + assert request.headers.get("X-Title").decode("utf-8") == (f"New Items - {test_item.display_name}") @responses.activate -def test_apprise(test_item: Item): - reload(tgtg_scanner.models.config) - config = tgtg_scanner.models.config.Config("") - config._setattr("apprise.enabled", True) - config._setattr("apprise.url", "ntfy://tgtg_test") - config._setattr("apprise.title", "New Items - ${{display_name}}") - config._setattr("apprise.body", - '${{display_name}} - New Amount: ${{items_available}} - ' - 'https://share.toogoodtogo.com/item/${{item_id}}') - responses.add( - responses.POST, - "https://ntfy.sh/", - status=200 - ) - - apprise = Apprise(config) +def test_apprise(test_item: Item, reservations: Reservations, favorites: Favorites): + config = Config() + config.apprise.enabled = True + config.apprise.url = "ntfy://tgtg_test" + config.apprise.title = "New Items - ${{display_name}}" + config.apprise.cron = Cron() + config.apprise.body = "${{display_name}} - New Amount: ${{items_available}} - https://share.toogoodtogo.com/item/${{item_id}}" + responses.add(responses.POST, "https://ntfy.sh/", status=200) + + apprise = Apprise(config, reservations, favorites) + apprise.start() apprise.send(test_item) + apprise.stop() request = responses.calls[0].request body = json.loads(request.body) assert request.url == "https://ntfy.sh/" - assert body.get('topic') == "tgtg_test" - assert body.get('message') == ( - f'{test_item.display_name} - New Amount: {test_item.items_available} ' - f'- https://share.toogoodtogo.com/item/{test_item.item_id}') + assert body.get("topic") == "tgtg_test" + assert body.get("message") == ( + f"{test_item.display_name} - New Amount: {test_item.items_available} - " + f"https://share.toogoodtogo.com/item/{test_item.item_id}" + ) -def test_console(test_item: Item, capsys: pytest.CaptureFixture): - reload(tgtg_scanner.models.config) - config = tgtg_scanner.models.config.Config("") - config._setattr("console.enabled", True) - config._setattr("console.body", "${{display_name}} - " - "new amount: ${{items_available}}") +def test_console( + test_item: Item, + reservations: Reservations, + favorites: Favorites, + capsys: pytest.CaptureFixture, +): + config = Config() + config.console.enabled = True + config.console.cron = Cron() + config.console.body = "${{display_name}} - new amount: ${{items_available}}" - console = Console(config) + console = Console(config, reservations, favorites) + console.start() console.send(test_item) + sleep(0.5) captured = capsys.readouterr() + console.stop() - assert captured.out.rstrip() == ( - f"{test_item.display_name} - " - f"new amount: {test_item.items_available}") + assert captured.out.rstrip() == f"{test_item.display_name} - new amount: {test_item.items_available}" -def test_script(test_item: Item, capfdbinary: pytest.CaptureFixture): - reload(tgtg_scanner.models.config) - config = tgtg_scanner.models.config.Config("") - config._setattr("script.enabled", True) - config._setattr("script.command", "echo ${{display_name}}") +def test_script( + test_item: Item, + reservations: Reservations, + favorites: Favorites, + capfdbinary: pytest.CaptureFixture, +): + config = Config() + config.script.enabled = True + config.script.cron = Cron() + config.script.command = "echo ${{display_name}}" - script = Script(config) + script = Script(config, reservations, favorites) + script.start() script.send(test_item) - sleep(0.1) + sleep(0.5) captured = capfdbinary.readouterr() + script.stop() encoding = "cp1252" if IS_WINDOWS else "utf-8" assert captured.out.decode(encoding).rstrip() == test_item.display_name + + +def test_smtp(test_item: Item, reservations: Reservations, favorites: Favorites, mocker: MockerFixture): + mock_SMTP = mocker.MagicMock(name="tgtg_scanner.notifiers.smtp.smtplib.SMTP") + mocker.patch("tgtg_scanner.notifiers.smtp.smtplib.SMTP", new=mock_SMTP) + mock_SMTP.return_value.noop.return_value = (250, "OK") + + config = Config() + config.smtp.enabled = True + config.smtp.cron = Cron() + config.smtp.host = "localhost" + config.smtp.port = 25 + config.smtp.use_tls = False + config.smtp.use_ssl = False + config.smtp.sender = "user@example.com" + config.smtp.recipients = ["user@example.com"] + config.smtp.subject = "New Magic Bags" + config.smtp.body = "Á ê
Amount: ${{items_available}}" + + smtp = SMTP(config, reservations, favorites) + smtp.start() + smtp.send(test_item) + smtp.stop() + + assert mock_SMTP.call_count == 1 + assert mock_SMTP.return_value.sendmail.call_count == 1 + call_args = mock_SMTP.return_value.sendmail.call_args_list[0][0] + assert call_args[0] == "user@example.com" + assert call_args[1] == ["user@example.com"] + body = call_args[2].split("\n") + assert body[0].startswith("Content-Type: multipart/alternative;") + assert body[2] == "From: user@example.com" + assert body[3] == "To: user@example.com" + assert body[4] == "Subject: New Magic Bags" + assert body[7] == 'Content-Type: text/html; charset="utf-8"' + assert body[11] == f"=C3=81 =C3=AA
Amount: {test_item.items_available}" + + +@pytest.fixture +def mocked_telegram(mocker: MockerFixture): + mocker.patch( + "telegram.Bot.get_me", + return_value=MagicMock(username="test_bot"), + ) + mocker.patch( + "telegram.ext.Application.initialize", + return_value=None, + ) + mocker.patch( + "telegram.ext.Updater.start_polling", + return_value=None, + ) + mocker.patch( + "telegram.ext.ExtBot.set_my_commands", + return_value=True, + ) + mocker.patch( + "telegram.ext.Application.start", + return_value=None, + ) + mocker.patch( + "telegram.ext.Updater.stop", + return_value=None, + ) + mocker.patch( + "telegram.ext.Application.stop", + return_value=None, + ) + mocker.patch( + "telegram.ext.Application.shutdown", + return_value=None, + ) + mocker.patch( + "telegram.Bot.send_message", + return_value=None, + ) + return mocker + + +def test_telegram(test_item: Item, reservations: Reservations, favorites: Favorites, mocked_telegram): + config = Config() + config.telegram.enabled = True + config.telegram.token = "1234567890:ABCDEF" + config.telegram.cron = Cron() + config.telegram.chat_ids = ["123456"] + config.telegram.disable_commands = False + config.telegram.body = "New Magic Bags: ${{items_available}}" + config.telegram.image = None + + telegram = Telegram(config, reservations, favorites) + telegram.start() + telegram.send(test_item) + sleep(0.5) + assert telegram.thread.is_alive() + telegram.stop() + assert not telegram.thread.is_alive() diff --git a/tests/test_reservations.py b/tests/test_reservations.py index 3fc8fc74..4841feae 100644 --- a/tests/test_reservations.py +++ b/tests/test_reservations.py @@ -23,29 +23,28 @@ def test_make_orders(reservations: Reservations, tgtg_item: dict): reservations.make_orders({"123": Item(tgtg_item)}, callback_mock) assert len(reservations.active_orders) == 1 assert len(reservations.reservation_query) == 0 - callback_mock.assert_called_once_with( - Reservation("123", 1, "Test Item")) + callback_mock.assert_called_once_with(Reservation("123", 1, "Test Item")) def test_update_active_orders(reservations: Reservations): order = Order("1", "123", 1, "Test Item") - reservations.client.get_order_status.return_value = {"state": "RESERVED"} + reservations.client.get_order_status.return_value = {"state": "RESERVED"} # type: ignore[attr-defined] reservations.active_orders = {order.id: order} reservations.update_active_orders() assert len(reservations.active_orders) == 1 - reservations.client.get_order_status.return_value = {"state": "CANELLED"} + reservations.client.get_order_status.return_value = {"state": "CANELLED"} # type: ignore[attr-defined] reservations.update_active_orders() assert len(reservations.active_orders) == 0 def test_cancel_order(reservations: Reservations): order = Order("1", "123", 1, "Test Item") - reservations.active_orders = [order] - reservations.cancel_order(order) + reservations.active_orders = {order.id: order} + reservations.cancel_order(order.id) def test_cancel_all_orders(reservations: Reservations): order1 = Order("1", "123", 1, "Test Item 1") order2 = Order("2", "123", 2, "Test Item 2") - reservations.active_orders = [order1, order2] + reservations.active_orders = {order1.id: order1, order2.id: order2} reservations.cancel_all_orders() diff --git a/tests/test_tgtg.py b/tests/test_tgtg.py index b966cc24..5d217d56 100644 --- a/tests/test_tgtg.py +++ b/tests/test_tgtg.py @@ -1,14 +1,13 @@ import json import pathlib import re -from importlib import reload from os import environ import pytest import responses from pytest_mock.plugin import MockerFixture -import tgtg_scanner.models.config +from tgtg_scanner.models import Config from tgtg_scanner.tgtg.tgtg_client import USER_AGENTS, TgtgClient @@ -21,11 +20,10 @@ def test_get_latest_apk_version(): def test_get_user_agent(mocker: MockerFixture): apk_version = "22.11.11" mocker.patch( - 'tgtg_scanner.tgtg.tgtg_client.TgtgClient.get_latest_apk_version', - return_value=apk_version) - client = TgtgClient( - email='test@example.com' + "tgtg_scanner.tgtg.tgtg_client.TgtgClient.get_latest_apk_version", + return_value=apk_version, ) + client = TgtgClient(email="test@example.com") user_agent = client._get_user_agent() assert user_agent in [agent.format(apk_version) for agent in USER_AGENTS] @@ -33,71 +31,71 @@ def test_get_user_agent(mocker: MockerFixture): @responses.activate def test_tgtg_login_with_mail(mocker: MockerFixture): mocker.patch( - 'tgtg_scanner.tgtg.tgtg_client.TgtgClient.get_latest_apk_version', - return_value="22.11.11") - client = TgtgClient( - email='test@example.com', - polling_wait_time=1 + "tgtg_scanner.tgtg.tgtg_client.TgtgClient.get_latest_apk_version", + return_value="22.11.11", ) + client = TgtgClient(email="test@example.com", polling_wait_time=1) auth_response_data = { - "state": "WAIT", "polling_id": "009350f7-650a-43d0-ab2c-2ff0676c9626" + "state": "WAIT", + "polling_id": "009350f7-650a-43d0-ab2c-2ff0676c9626", } poll_response_data = { "access_token": "new_access_token", "access_token_ttl_seconds": 172800, "refresh_token": "new_refresh_token", - "startup_data": {"user": {"user_id": "123456789"}} + "startup_data": {"user": {"user_id": "123456789"}}, } responses.add( responses.POST, "https://apptoogoodtogo.com/api/auth/v3/authByEmail", json.dumps(auth_response_data), - status=200 + status=200, ) responses.add( responses.POST, "https://apptoogoodtogo.com/api/auth/v3/authByRequestPollingId", - status=202 + status=202, ) responses.add( responses.POST, "https://apptoogoodtogo.com/api/auth/v3/authByRequestPollingId", json.dumps(poll_response_data), - status=200 + status=200, ) client.login() assert client.access_token == poll_response_data.get("access_token") assert client.refresh_token == poll_response_data.get("refresh_token") - assert client.user_id == poll_response_data.get( - "startup_data", {}).get("user", {}).get("user_id") + user_id = poll_response_data.get("startup_data", {}).get("user", {}).get("user_id") # type: ignore[attr-defined] + assert client.user_id == user_id assert json.loads(responses.calls[1].request.body) == { "device_type": client.device_type, "email": client.email, - "request_polling_id": auth_response_data.get("polling_id") + "request_polling_id": auth_response_data.get("polling_id"), } @responses.activate def test_tgtg_login_with_token(mocker: MockerFixture): mocker.patch( - 'tgtg_scanner.tgtg.tgtg_client.TgtgClient.get_latest_apk_version', - return_value="22.11.11") + "tgtg_scanner.tgtg.tgtg_client.TgtgClient.get_latest_apk_version", + return_value="22.11.11", + ) client = TgtgClient( - email='test@example.com', - access_token='old_access_token', - refresh_token='old_refresh_token', - user_id='old_user_id' + email="test@example.com", + access_token="old_access_token", + refresh_token="old_refresh_token", + user_id="old_user_id", ) response_data = { "access_token": "new_access_token", "access_token_ttl_seconds": 172800, - "refresh_token": "new_refresh_token" + "refresh_token": "new_refresh_token", } responses.add( responses.POST, "https://apptoogoodtogo.com/api/auth/v3/token/refresh", json.dumps(response_data), - status=200 + status=200, ) client.login() assert client.access_token == response_data.get("access_token") @@ -107,21 +105,21 @@ def test_tgtg_login_with_token(mocker: MockerFixture): @responses.activate def test_tgtg_get_items(mocker: MockerFixture, tgtg_item: dict): mocker.patch( - 'tgtg_scanner.tgtg.tgtg_client.TgtgClient.get_latest_apk_version', - return_value="22.11.11") - mocker.patch('tgtg_scanner.tgtg.tgtg_client.TgtgClient.login', - return_value=None) + "tgtg_scanner.tgtg.tgtg_client.TgtgClient.get_latest_apk_version", + return_value="22.11.11", + ) + mocker.patch("tgtg_scanner.tgtg.tgtg_client.TgtgClient.login", return_value=None) responses.add( responses.POST, "https://apptoogoodtogo.com/api/item/v8/", json.dumps({"items": [tgtg_item]}), - status=200 + status=200, ) client = TgtgClient( - email='test@example.com', - access_token='access_token', - refresh_token='refresh_token', - user_id='user_id' + email="test@example.com", + access_token="access_token", + refresh_token="refresh_token", + user_id="user_id", ) response = client.get_items(favorites_only=True) assert response == [tgtg_item] @@ -130,22 +128,22 @@ def test_tgtg_get_items(mocker: MockerFixture, tgtg_item: dict): @responses.activate def test_tgtg_get_item(mocker: MockerFixture, tgtg_item: dict): mocker.patch( - 'tgtg_scanner.tgtg.tgtg_client.TgtgClient.get_latest_apk_version', - return_value="22.11.11") - mocker.patch('tgtg_scanner.tgtg.tgtg_client.TgtgClient.login', - return_value=None) - item_id = tgtg_item.get('item', {}).get('item_id') + "tgtg_scanner.tgtg.tgtg_client.TgtgClient.get_latest_apk_version", + return_value="22.11.11", + ) + mocker.patch("tgtg_scanner.tgtg.tgtg_client.TgtgClient.login", return_value=None) + item_id = tgtg_item.get("item", {}).get("item_id") responses.add( responses.POST, f"https://apptoogoodtogo.com/api/item/v8/{item_id}", json.dumps(tgtg_item), - status=200 + status=200, ) client = TgtgClient( - email='test@example.com', - access_token='access_token', - refresh_token='refresh_token', - user_id='user_id' + email="test@example.com", + access_token="access_token", + refresh_token="refresh_token", + user_id="user_id", ) response = client.get_item(item_id) assert response == tgtg_item @@ -154,22 +152,22 @@ def test_tgtg_get_item(mocker: MockerFixture, tgtg_item: dict): @responses.activate def test_tgtg_set_favorite(mocker: MockerFixture): mocker.patch( - 'tgtg_scanner.tgtg.tgtg_client.TgtgClient.get_latest_apk_version', - return_value="22.11.11") - mocker.patch('tgtg_scanner.tgtg.tgtg_client.TgtgClient.login', - return_value=None) + "tgtg_scanner.tgtg.tgtg_client.TgtgClient.get_latest_apk_version", + return_value="22.11.11", + ) + mocker.patch("tgtg_scanner.tgtg.tgtg_client.TgtgClient.login", return_value=None) item_id = "12345" responses.add( responses.POST, f"https://apptoogoodtogo.com/api/item/v8/{item_id}/setFavorite", json.dumps({"is_favorite": True}), - status=200 + status=200, ) client = TgtgClient( - email='test@example.com', - access_token='access_token', - refresh_token='refresh_token', - user_id='user_id' + email="test@example.com", + access_token="access_token", + refresh_token="refresh_token", + user_id="user_id", ) client.set_favorite(item_id, True) assert json.loads(responses.calls[0].request.body) == {"is_favorite": True} @@ -177,24 +175,23 @@ def test_tgtg_set_favorite(mocker: MockerFixture): @pytest.mark.tgtg_api def test_tgtg_api(item_properties: dict): - reload(tgtg_scanner.models.config) - if pathlib.Path('config.ini').is_file(): - config = tgtg_scanner.models.config.Config('config.ini') + if pathlib.Path("config.ini").is_file(): + config = Config("config.ini") else: - config = tgtg_scanner.models.config.Config() + config = Config() env_file = environ.get("GITHUB_ENV", None) client = TgtgClient( - email=config.tgtg.get("username"), - timeout=config.tgtg.get("timeout"), - access_token_lifetime=config.tgtg.get("access_token_lifetime"), - max_polling_tries=config.tgtg.get("max_polling_tries"), - polling_wait_time=config.tgtg.get("polling_wait_time"), - access_token=config.tgtg.get("access_token"), - refresh_token=config.tgtg.get("refresh_token"), - user_id=config.tgtg.get("user_id"), - datadome_cookie=config.tgtg.get("datadome") + email=config.tgtg.username, + timeout=config.tgtg.timeout, + access_token_lifetime=config.tgtg.access_token_lifetime, + max_polling_tries=config.tgtg.max_polling_tries, + polling_wait_time=config.tgtg.polling_wait_time, + access_token=config.tgtg.access_token, + refresh_token=config.tgtg.refresh_token, + user_id=config.tgtg.user_id, + datadome_cookie=config.tgtg.datadome, ) # get credentials and safe tokens to GITHUB_ENV file diff --git a/tests/tgtg_server.py b/tests/tgtg_server.py new file mode 100644 index 00000000..2efedffc --- /dev/null +++ b/tests/tgtg_server.py @@ -0,0 +1,78 @@ +import argparse +import json +import logging +import random +from http.server import BaseHTTPRequestHandler, HTTPServer +from urllib.parse import urljoin + +import requests + +from tgtg_scanner.tgtg.tgtg_client import API_ITEM_ENDPOINT, BASE_URL + + +class RequestHandler(BaseHTTPRequestHandler): + def do_GET(self): + logging.info("GET request,\nPath: %s\nHeaders:\n%s\n", str(self.path), str(self.headers)) + self.send_response(200) + self.send_header("Content-type", "text/html; charset=utf-8") + self.end_headers() + self.wfile.write("

TGTG API test server

".encode("utf-8")) + self.wfile.write(f"GET request for {self.path}".encode("utf-8")) + + def do_POST(self): + content_length = int(self.headers["Content-Length"]) + post_data = self.rfile.read(content_length) + logging.info( + "POST request,\nPath: %s\nHeaders:\n%s\nBody:\n%s\n", + str(self.path), + str(self.headers), + post_data.decode("utf-8"), + ) + + path = self.path[1:] + headers = self.headers + headers["Host"] = "apptoogoodtogo.com" + url = urljoin(BASE_URL, path) + response = requests.post(url, post_data, headers=headers) + + response_data = response.content + if path.startswith(API_ITEM_ENDPOINT): + response_json = response.json() + for i in range(len(response_json.get("items"))): + new_avail = random.randint(0, 3) + response_json["items"][i]["items_available"] = new_avail + response_data = json.dumps(response_json).encode("utf-8") + + logging.debug("POST response,\nBody:\n%s\n", response_data) + self.send_response(response.status_code) + self.send_header( + "Set-Cookie", + f"datadome={response.cookies.get('datadome')}; Domain=.local; Path=/", + ) + self.end_headers() + self.wfile.write(response_data) + + +def run_server(port: int = 8080): + server_address = ("", port) + httpd = HTTPServer(server_address, RequestHandler) + logging.info("Starting httpd...") + try: + httpd.serve_forever() + except KeyboardInterrupt: + pass + httpd.server_close() + logging.info("Stopping httpd...") + + +def main(): + parser = argparse.ArgumentParser(description="TGTG API test server") + parser.add_argument("-d", "--debug", action="store_true", help="activate debugging mode") + parser.add_argument("-p", "--port", type=int, default=8080) + args = parser.parse_args() + logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO) + run_server(args.port) + + +if __name__ == "__main__": + main() diff --git a/tgtg_scanner/__main__.py b/tgtg_scanner/__main__.py index 47a23ccb..b1918552 100644 --- a/tgtg_scanner/__main__.py +++ b/tgtg_scanner/__main__.py @@ -14,33 +14,33 @@ from packaging import version from requests.exceptions import RequestException -from tgtg_scanner._version import (__author__, __description__, __url__, - __version__) +from tgtg_scanner._version import __author__, __description__, __url__, __version__ +from tgtg_scanner.errors import ConfigurationError, TgtgAPIError from tgtg_scanner.models import Config -from tgtg_scanner.models.errors import ConfigurationError, TgtgAPIError from tgtg_scanner.scanner import Scanner VERSION_URL = "https://api.github.com/repos/Der-Henning/tgtg/releases/latest" HEADER = ( - " ____ ___ ____ ___ ____ ___ __ __ _ __ _ ____ ____ ", # noqa: W605,E501 - " (_ _)/ __)(_ _)/ __) / ___) / __) / _\ ( ( \( ( \( __)( _ \ ", # noqa: W605,E501 - " )( ( (_ \ )( ( (_ \ \___ \( (__ / \/ // / ) _) ) / ", # noqa: W605,E501 - " (__) \___/ (__) \___/ (____/ \___)\_/\_/\_)__)\_)__)(____)(__\_) ") # noqa: W605,E501 + r" ____ ___ ____ ___ ____ ___ __ __ _ __ _ ____ ____ ", + r" (_ _)/ __)(_ _)/ __) / ___) / __) / _\ ( ( \( ( \( __)( _ \ ", + r" )( ( (_ \ )( ( (_ \ \___ \( (__ / \/ // / ) _) ) / ", + r" (__) \___/ (__) \___/ (____/ \___)\_/\_/\_)__)\_)__)(____)(__\_) ", +) # set to 1 to debug http headers http_client.HTTPConnection.debuglevel = 0 SYS_PLATFORM = platform.system() -IS_WINDOWS = SYS_PLATFORM.lower() in ('windows', 'cygwin') +IS_WINDOWS = SYS_PLATFORM.lower() in {"windows", "cygwin"} IS_EXECUTABLE = getattr(sys, "_MEIPASS", False) PROG_PATH = Path(sys.executable).parent if IS_EXECUTABLE else Path(os.getcwd()) -IS_DOCKER = os.environ.get("DOCKER", "False").lower() in ('true', '1', 't') +IS_DOCKER = os.environ.get("DOCKER", "False").lower() in {"true", "1", "t", "y", "yes"} LOGS_PATH = os.environ.get("LOGS_PATH", PROG_PATH) -def main() -> NoReturn: +def main(): """Wrapper for Scanner and Helper functions.""" _register_signals() @@ -48,61 +48,57 @@ def main() -> NoReturn: log_file = Path(LOGS_PATH, "scanner.log") parser = argparse.ArgumentParser(description=__description__) + parser.add_argument("-v", "--version", action="version", version=f"v{__version__}") + parser.add_argument("-d", "--debug", action="store_true", help="activate debugging mode") parser.add_argument( - "-v", "--version", - action="version", - version=f"v{__version__}") - parser.add_argument( - "-d", "--debug", - action="store_true", - help="activate debugging mode") - parser.add_argument( - "-c", "--config", + "-c", + "--config", metavar="config_file", type=Path, - help="path to config file (default: config.ini)") + default=config_file, + help="path to config file (default: config.ini)", + ) parser.add_argument( - "-l", "--log_file", + "-l", + "--log_file", metavar="log_file", type=Path, default=log_file, - help="path to log file (default: scanner.log)") + help="path to log file (default: scanner.log)", + ) helper_group = parser.add_mutually_exclusive_group(required=False) helper_group.add_argument( - "-t", "--tokens", - action="store_true", - help="display your current access tokens and exit",) - helper_group.add_argument( - "-f", "--favorites", + "-t", + "--tokens", action="store_true", - help="display your favorites and exit") + help="display your current access tokens and exit", + ) + helper_group.add_argument("-f", "--favorites", action="store_true", help="display your favorites and exit") helper_group.add_argument( - "-F", "--favorite_ids", + "-F", + "--favorite_ids", action="store_true", - help="display the item ids of your favorites and exit",) + help="display the item ids of your favorites and exit", + ) helper_group.add_argument( - "-a", "--add", + "-a", + "--add", nargs="+", metavar="item_id", - help="add item ids to favorites and exit",) + help="add item ids to favorites and exit", + ) helper_group.add_argument( - "-r", "--remove", + "-r", + "--remove", nargs="+", metavar="item_id", - help="remove item ids from favorites and exit",) - helper_group.add_argument( - "-R", "--remove_all", - action="store_true", - help="remove all favorites and exit") + help="remove item ids from favorites and exit", + ) + helper_group.add_argument("-R", "--remove_all", action="store_true", help="remove all favorites and exit") json_group = parser.add_mutually_exclusive_group(required=False) - json_group.add_argument( - "-j", "--json", - action="store_true", - help="output as plain json") - json_group.add_argument( - "-J", "--json_pretty", - action="store_true", - help="output as pretty json") + json_group.add_argument("-j", "--json", action="store_true", help="output as plain json") + json_group.add_argument("-J", "--json_pretty", action="store_true", help="output as pretty json") + parser.add_argument("--base_url", default=None, help="Overwrite TGTG API URL for testing") args = parser.parse_args() # Disable logging for json output @@ -119,28 +115,26 @@ def main() -> NoReturn: # Define stream formatter and handler stream_formatter = colorlog.ColoredFormatter( - fmt=("%(cyan)s%(asctime)s%(reset)s " - "%(log_color)s%(levelname)-8s%(reset)s " - "%(message)s"), + fmt=("%(cyan)s%(asctime)s%(reset)s %(log_color)s%(levelname)-8s%(reset)s %(message)s"), datefmt="%Y-%m-%d %H:%M:%S", log_colors={ "DEBUG": "purple", "INFO": "green", "WARNING": "yellow", "ERROR": "red", - "CRITICAL": "red"}) + "CRITICAL": "red", + }, + ) stream_handler = logging.StreamHandler() stream_handler.setFormatter(stream_formatter) logging.root.addHandler(stream_handler) # Define file formatter and handler - file_handler = logging.FileHandler( - args.log_file, mode="w", encoding='utf-8') + file_handler = logging.FileHandler(args.log_file, mode="w", encoding="utf-8") file_formatter = logging.Formatter( - fmt=("[%(asctime)s][%(name)s]" - "[%(filename)s:%(funcName)s:%(lineno)d]" - "[%(levelname)s] %(message)s"), - datefmt="%Y-%m-%d %H:%M:%S") + fmt=("[%(asctime)s][%(name)s][%(filename)s:%(funcName)s:%(lineno)d][%(levelname)s] %(message)s"), + datefmt="%Y-%m-%d %H:%M:%S", + ) file_handler.setFormatter(file_formatter) logging.root.addHandler(file_handler) @@ -148,16 +142,9 @@ def main() -> NoReturn: log = logging.getLogger("tgtg") log.setLevel(logging.INFO) - # Set config file from args - if args.config: - if not args.config.is_file(): - log.error("Config file %s not found!", args.config) - sys.exit(1) - config_file = args.config - try: # Load config - config = Config(config_file) + config = Config(args.config) config.docker = IS_DOCKER # Activate debugging mode @@ -168,6 +155,9 @@ def main() -> NoReturn: logging.getLogger(logger_name).setLevel(logging.DEBUG) log.info("Debugging mode enabled") + if args.base_url is not None: + config.tgtg.base_url = args.base_url + scanner = Scanner(config) if args.tokens: credentials = scanner.get_credentials() @@ -197,8 +187,7 @@ def main() -> NoReturn: print("") elif args.favorite_ids: favorites = scanner.get_favorites() - item_ids = [fav.get("item", {}).get("item_id") - for fav in favorites] + item_ids = [fav.get("item", {}).get("item_id") for fav in favorites] if args.json: print(json.dumps(item_ids, sort_keys=True)) elif args.json_pretty: @@ -217,8 +206,7 @@ def main() -> NoReturn: scanner.unset_favorite(item_id) print("done.") elif args.remove_all: - if query_yes_no("Remove all favorites from your account?", - default='no'): + if query_yes_no("Remove all favorites from your account?", default="no"): scanner.unset_all_favorites() print("done.") else: @@ -231,26 +219,33 @@ def main() -> NoReturn: sys.exit(1) except KeyboardInterrupt: log.info("Shutting down scanner ...") + scanner.stop() sys.exit(0) except SystemExit: sys.exit(1) def _get_config_file() -> Union[Path, None]: + # Default: config.ini in current working directory or next to executable config_file = Path(PROG_PATH, "config.ini") if config_file.is_file(): return config_file - config_file = Path(PROG_PATH, "tgtg_scanner", "config.ini") + # config.ini in project folder (same place as config.sample.ini) + config_file = Path(Path(__file__).parents[1], "config.ini") if config_file.is_file(): return config_file + # legacy: config.ini in src folder + config_file = Path(Path(__file__).parents[1], "src", "config.ini") + if config_file.is_file(): + return config_file + return None def _get_version_info() -> str: lastest_release = _get_new_version() if lastest_release is None: return __version__ - return (f"{__version__} - Update available! " - f"See {lastest_release.get('html_url')}") + return f"{__version__} - Update available! See {lastest_release.get('html_url')}" def _run_scanner(scanner: Scanner) -> NoReturn: @@ -268,8 +263,7 @@ def _get_new_version() -> Union[dict, None]: res = requests.get(VERSION_URL, timeout=60) res.raise_for_status() lastest_release = res.json() - if version.parse(__version__) < version.parse( - lastest_release.get("tag_name")): + if version.parse(__version__) < version.parse(lastest_release.get("tag_name")): return lastest_release except (RequestException, version.InvalidVersion, ValueError) as err: log.warning("Failed getting latest version! - %s", err) @@ -281,9 +275,7 @@ def _print_version_check() -> None: try: lastest_release = _get_new_version() if lastest_release is not None: - log.info( - "New Version %s available!", version.parse( - lastest_release.get("tag_name"))) + log.info("New Version %s available!", version.parse(lastest_release.get("tag_name"))) log.info("Please visit %s", lastest_release.get("html_url")) log.info("") except (version.InvalidVersion, ValueError) as err: @@ -308,14 +300,14 @@ def _register_signals() -> None: if hasattr(signal, "SIGBREAK"): signal.signal(getattr(signal, "SIGBREAK"), _handle_exit_signal) if not IS_WINDOWS: - signal.signal(signal.SIGHUP, _handle_exit_signal) + signal.signal(signal.SIGHUP, _handle_exit_signal) # type: ignore[attr-defined] # TODO: SIGQUIT is ideally meant to terminate with core dumps - signal.signal(signal.SIGQUIT, _handle_exit_signal) + signal.signal(signal.SIGQUIT, _handle_exit_signal) # type: ignore[attr-defined] def _handle_exit_signal(signum: int, _frame: Any) -> None: log = logging.getLogger("tgtg") - log.debug('Received signal %d' % signum) + log.debug("Received signal %d" % signum) raise KeyboardInterrupt diff --git a/tgtg_scanner/_version.py b/tgtg_scanner/_version.py index 0508e4b2..8ea001d8 100644 --- a/tgtg_scanner/_version.py +++ b/tgtg_scanner/_version.py @@ -4,10 +4,10 @@ metadata = importlib.metadata.metadata(PAKAGE_NAME) -__title__ = metadata.get("Name") -__description__ = metadata.get("Summary") +__title__ = metadata["Name"] +__description__ = metadata["Summary"] __version__ = importlib.metadata.version(PAKAGE_NAME) -__author__ = metadata.get("Author") -__author_email__ = metadata.get("Author-email") -__license__ = metadata.get("License") -__url__ = metadata.get("Project-URL").split(", ")[1] +__author__ = metadata["Author"] +__author_email__ = metadata["Author-email"] +__license__ = metadata["License"] +__url__ = metadata["Project-URL"].split(", ")[1] diff --git a/tgtg_scanner/models/errors.py b/tgtg_scanner/errors.py similarity index 96% rename from tgtg_scanner/models/errors.py rename to tgtg_scanner/errors.py index 4f3b47b8..f3ead56c 100644 --- a/tgtg_scanner/models/errors.py +++ b/tgtg_scanner/errors.py @@ -25,8 +25,7 @@ class ConfigurationError(Error): class MaskConfigurationError(ConfigurationError): def __init__(self, variable): self.message = ( - f"Unrecognized variable {variable}. For details see " - f"https://github.com/Der-Henning/tgtg/wiki/Configuration#variables" + f"Unrecognized variable {variable}. For details see https://github.com/Der-Henning/tgtg/wiki/Configuration#variables" ) super().__init__(self.message) diff --git a/tgtg_scanner/models/__init__.py b/tgtg_scanner/models/__init__.py index 398afe74..003b40d9 100644 --- a/tgtg_scanner/models/__init__.py +++ b/tgtg_scanner/models/__init__.py @@ -1,3 +1,5 @@ +# flake8: noqa + from tgtg_scanner.models.config import Config from tgtg_scanner.models.cron import Cron from tgtg_scanner.models.favorites import Favorites @@ -5,6 +7,3 @@ from tgtg_scanner.models.location import Location from tgtg_scanner.models.metrics import Metrics from tgtg_scanner.models.reservations import Reservations - -__all__ = ['Config', 'Cron', 'Item', 'Metrics', - 'Location', 'Reservations', 'Favorites'] diff --git a/tgtg_scanner/models/config.py b/tgtg_scanner/models/config.py index 82641075..5901e561 100644 --- a/tgtg_scanner/models/config.py +++ b/tgtg_scanner/models/config.py @@ -1,595 +1,656 @@ +from __future__ import annotations + import codecs import configparser import json import logging -from io import TextIOWrapper +from abc import ABC, abstractmethod +from dataclasses import dataclass, field from os import environ from pathlib import Path -from typing import Any +from typing import IO, Any, Union import humanize +from tgtg_scanner.errors import ConfigurationError from tgtg_scanner.models.cron import Cron -from tgtg_scanner.models.errors import ConfigurationError +from tgtg_scanner.tgtg.tgtg_client import BASE_URL log = logging.getLogger("tgtg") +CONFIG_FILE_HEADER = """## TGTG Scanner Configuration +## -------------------------- +## This is the configuration file for the TGTG Scanner. +## You can find more information about the configuration on the project page: +## https://github.com/Der-Henning/tgtg/wiki/Configuration -DEFAULT_CONFIG = { - 'item_ids': [], - 'sleep_time': 60, - 'schedule_cron': Cron('* * * * *'), - 'debug': False, - 'locale': "en_US", - 'metrics': False, - 'metrics_port': 8000, - 'disable_tests': False, - 'quiet': False, - 'docker': False, - 'activity': True, - 'tgtg': { - 'username': None, - 'access_token': None, - 'refresh_token': None, - 'user_id': None, - 'datadome': None, - 'timeout': 60, - 'access_token_lifetime': 14400, - 'max_polling_tries': 24, - 'polling_wait_time': 5 - }, - 'location': { - 'enabled': False, - 'Google_Maps_API_Key': '', - 'Origin_Address': '', - }, - 'apprise': { - 'enabled': False, - 'url': '', - 'cron': Cron('* * * * *'), - 'title': 'New Magic Bags', - 'body': '${{display_name}} - new amount: ' - '${{items_available}} - ${{link}}' - }, - 'console': { - 'enabled': False, - 'body': '${{scanned_on}} ${{display_name}} - ' - 'new amount: ${{items_available}}', - 'cron': Cron('* * * * *') - }, - 'push_safer': { - 'enabled': False, - 'key': '', - 'deviceId': '', - 'cron': Cron('* * * * *') - }, - 'smtp': { - 'enabled': False, - 'host': 'smtp.gmail.com', - 'port': 587, - 'tls': True, - 'ssl': False, - 'username': '', - 'password': '', - 'sender': '', - 'recipient': [], - 'cron': Cron('* * * * *'), - 'subject': 'New Magic Bags', - 'body': '${{display_name}}
' - 'New Amount: ${{items_available}}' - }, - 'ifttt': { - 'enabled': False, - 'event': 'tgtg_notification', - 'key': '', - 'body': '{"value1": "${{display_name}}", ' - '"value2": ${{items_available}}, ' - '"value3": "${{link}}"}', - 'timeout': 60, - 'cron': Cron('* * * * *') - }, - 'ntfy': { - 'enabled': False, - 'server': 'https://ntfy.sh', - 'topic': None, - 'title': 'New TGTG items', - 'message': '${{display_name}} - New Amount: ' - '${{items_available}} - ${{link}}', - 'body': None, - 'priority': 'default', - 'tags': 'shopping,tgtg', - 'click': '${{link}}', - 'username': None, - 'password': None, - 'timeout': 60, - 'cron': Cron('* * * * *'), - }, - 'webhook': { - 'enabled': False, - 'url': '', - 'method': 'POST', - 'body': '', - 'type': 'text/plain', - 'headers': {}, - 'username': None, - 'password': None, - 'timeout': 60, - 'cron': Cron('* * * * *') - }, - 'telegram': { - 'enabled': False, - 'token': '', - 'chat_ids': [], - 'disable_commands': False, - 'timeout': 60, - 'cron': Cron('* * * * *'), - 'body': '*${{display_name}}*\n' - '*Available*: ${{items_available}}\n' - '*Price*: ${{price}} ${{currency}}\n' - '*Pickup*: ${{pickupdate}}', - 'image': None - }, - 'script': { - 'enabled': False, - 'command': '', - 'cron': Cron('* * * * *') - } -} - - -class Config(): - """ - Reads and provides configuration.\n - If file is provided the config is read from the file.\n - Else the config is read from environment variables. - """ - - item_ids: list - sleep_time: int - schedule_cron: Cron - debug: bool - docker: bool - activity: bool - locale: str - metrics: bool - metrics_port: int - disable_tests: bool - quiet: bool - tgtg: dict - apprise: dict - console: dict - push_safer: dict - smtp: dict - ifttt: dict - ntfy: dict - webhook: dict - telegram: dict - script: dict - location: dict - - def __init__(self, file: str = None): - self.file = Path(file) if file is not None else None - for key in DEFAULT_CONFIG: - setattr(self, key, DEFAULT_CONFIG[key]) +""" - if self.file is not None: - if not self.file.exists(): - raise ConfigurationError( - f"Configuration file '{self.file.absolute()}' " - "does not exist!") - self._read_ini() - log.info("Loaded config from %s", self.file.absolute()) - else: - self._read_env() - log.info("Loaded config from environment variables") +DEPRECIATION_WARNING = "{} is deprecated and will be removed in a future release. Please use {} instead." - self.token_path = environ.get("TGTG_TOKEN_PATH", None) - self._load_tokens() - if (self.locale and not self.locale.startswith('en')): - humanize.i18n.activate(self.locale) - - def _open(self, file: str, mode: str) -> TextIOWrapper: - return open(Path(self.token_path, file), mode, encoding='utf-8') - def _load_tokens(self) -> None: - """ - Reads tokens from token files - """ - if self.token_path is not None: - try: - with self._open('accessToken', 'r') as file: - self.tgtg["access_token"] = file.read() - with self._open('refreshToken', 'r') as file: - self.tgtg["refresh_token"] = file.read() - with self._open('userID', 'r') as file: - self.tgtg["user_id"] = file.read() - with self._open('datadome', 'r') as file: - self.tgtg["datadome"] = file.read() - except FileNotFoundError: - log.warning("No token files in token path.") - except EnvironmentError as err: - log.error("Error loading Tokens - %s", err) +@dataclass +class BaseConfig(ABC): + """Base configuration""" - def _getattr(self, attr: str) -> None: - if '.' in attr: - _attr, _key = attr.split(".") - return self.__dict__[_attr][_key] - return getattr(self, attr) + @abstractmethod + def _read_ini(self, parser: configparser.ConfigParser): + pass - def _setattr(self, attr: str, value: Any) -> None: - if '.' in attr: - _attr, _key = attr.split(".") - self.__dict__[_attr][_key] = value - else: - setattr(self, attr, value) + @abstractmethod + def _read_env(self): + pass @staticmethod def _decode(value: str) -> str: - return codecs.escape_decode(bytes(value, "utf-8"))[0].decode("utf-8") - - def _ini_get(self, config: configparser.ConfigParser, - section: str, key: str, attr: str) -> None: - if section in config: - value = config[section].get(key, None) - if value is not None: - value = self._decode(value) - self._setattr(attr, value) - - def _ini_get_boolean(self, config: configparser.ConfigParser, - section: str, key: str, attr: str) -> None: - if section in config: - self._setattr(attr, config[section].getboolean( - key, self._getattr(attr))) - - def _ini_get_int(self, config: configparser.ConfigParser, - section: str, key: str, attr: str) -> None: - if section in config: - self._setattr(attr, config[section].getint( - key, self._getattr(attr))) - - def _ini_get_float(self, config: configparser.ConfigParser, - section: str, key: str, attr: str) -> None: - if section in config: - self._setattr(attr, config[section].getfloat( - key, self._getattr(attr))) - - def _ini_get_array(self, config: configparser.ConfigParser, - section: str, key: str, attr: str) -> None: - if section in config: - value = config[section].get(key, None) - if value: - arr = [self._decode(val.strip()) for val in value.split(',')] - self._setattr(attr, arr) - - def _ini_get_dict(self, config: configparser.ConfigParser, - section: str, key: str, attr: str) -> None: - if section in config: - value = config[section].get(key, None) - if value: - dic = json.loads(value) - self._setattr(attr, dic) - - def _ini_get_cron(self, config: configparser.ConfigParser, - section: str, key: str, attr: str) -> None: - if section in config: - value = config[section].get(key, None) - if value is not None: - self._setattr(attr, Cron(value)) - - def _read_ini(self) -> None: + return codecs.escape_decode(bytes(value, "utf-8"))[0].decode("utf-8") # type: ignore[attr-defined] + + def _ini_get(self, parser: configparser.ConfigParser, section: str, key: str, attr: str): + value = parser.get(section, key, fallback=None) + if value is not None: + setattr(self, attr, self._decode(value)) + + def _ini_get_boolean(self, parser: configparser.ConfigParser, section: str, key: str, attr: str): try: - config = configparser.ConfigParser() - config.read(self.file, encoding='utf-8') - - self._ini_get_boolean(config, "MAIN", "debug", "debug") - self._ini_get_array(config, "MAIN", "ItemIDs", "item_ids") - self._ini_get_int(config, "MAIN", "SleepTime", "sleep_time") - self._ini_get_cron(config, "MAIN", "ScheduleCron", "schedule_cron") - self._ini_get_boolean(config, "MAIN", "Metrics", "metrics") - self._ini_get_int(config, "MAIN", "MetricsPort", "metrics_port") - self._ini_get_boolean(config, "MAIN", "DisableTests", - "disable_tests") - self._ini_get_boolean(config, "MAIN", "quiet", "quiet") - self._ini_get(config, "MAIN", "locale", "locale") - self._ini_get_boolean(config, "MAIN", "Activity", "activity") - - self._ini_get(config, "TGTG", "Username", "tgtg.username") - self._ini_get(config, "TGTG", "AccessToken", "tgtg.access_token") - self._ini_get(config, "TGTG", "RefreshToken", "tgtg.refresh_token") - self._ini_get(config, "TGTG", "UserId", "tgtg.user_id") - self._ini_get(config, "TGTG", "Datadome", "tgtg.datadome") - self._ini_get_int(config, "TGTG", "Timeout", "tgtg.timeout") - self._ini_get_int(config, "TGTG", "AccessTokenLifetime", - "tgtg.access_token_lifetime") - self._ini_get_int(config, "TGTG", "MaxPollingTries", - "tgtg.max_polling_tries") - self._ini_get_int(config, "TGTG", "PollingWaitTime", - "tgtg.polling_wait_time") - - self._ini_get_boolean(config, "APPRISE", - "enabled", "apprise.enabled") - self._ini_get(config, "APPRISE", "URL", "apprise.url") - self._ini_get_cron(config, "APPRISE", "cron", "apprise.cron") - self._ini_get(config, "APPRISE", "title", "apprise.title") - self._ini_get(config, "APPRISE", "body", "apprise.body") - - self._ini_get_boolean(config, "CONSOLE", - "enabled", "console.enabled") - self._ini_get(config, "CONSOLE", "Body", "console.body") - self._ini_get_cron(config, "CONSOLE", "cron", "console.cron") - - self._ini_get_boolean(config, "PUSHSAFER", - "enabled", "push_safer.enabled") - self._ini_get(config, "PUSHSAFER", "Key", "push_safer.key") - self._ini_get(config, "PUSHSAFER", "DeviceID", - "push_safer.deviceId") - self._ini_get_cron(config, "PUSHSAFER", "cron", "push_safer.cron") - - self._ini_get_boolean(config, "SMTP", "enabled", "smtp.enabled") - self._ini_get(config, "SMTP", "Host", "smtp.host") - self._ini_get_int(config, "SMTP", "Port", "smtp.port") - self._ini_get_boolean(config, "SMTP", "TLS", "smtp.tls") - self._ini_get_boolean(config, "SMTP", "SSL", "smtp.ssl") - self._ini_get(config, "SMTP", "Username", "smtp.username") - self._ini_get(config, "SMTP", "Password", "smtp.password") - self._ini_get_cron(config, "SMTP", "cron", "smtp.cron") - self._ini_get(config, "SMTP", "Sender", "smtp.sender") - self._ini_get_array(config, "SMTP", "Recipient", "smtp.recipient") - self._ini_get(config, "SMTP", "Subject", "smtp.subject") - self._ini_get(config, "SMTP", "Body", "smtp.body") - - self._ini_get_boolean(config, "IFTTT", "enabled", "ifttt.enabled") - self._ini_get(config, "IFTTT", "Event", "ifttt.event") - self._ini_get(config, "IFTTT", "Key", "ifttt.key") - self._ini_get(config, "IFTTT", "Body", "ifttt.body") - self._ini_get_int(config, "IFTTT", "Timeout", "ifttt.timeout") - self._ini_get_cron(config, "IFTTT", "cron", "ifttt.cron") - - self._ini_get_boolean(config, "NTFY", "enabled", "ntfy.enabled") - self._ini_get(config, "NTFY", "Server", "ntfy.server") - self._ini_get(config, "NTFY", "Topic", "ntfy.topic") - self._ini_get(config, "NTFY", "Title", "ntfy.title") - self._ini_get(config, "NTFY", "Message", "ntfy.message") - self._ini_get(config, "NTFY", "Body", "ntfy.body") - self._ini_get(config, "NTFY", "Priority", "ntfy.priority") - self._ini_get(config, "NTFY", "Tags", "ntfy.tags") - self._ini_get(config, "NTFY", "Click", "ntfy.click") - self._ini_get(config, "NTFY", "Username", "ntfy.username") - self._ini_get(config, "NTFY", "Password", "ntfy.password") - self._ini_get_int(config, "NTFY", "Timeout", "ntfy.timeout") - self._ini_get_cron(config, "NTFY", "cron", "ntfy.cron") - - self._ini_get_boolean(config, "WEBHOOK", "enabled", - "webhook.enabled") - self._ini_get(config, "WEBHOOK", "URL", "webhook.url") - self._ini_get(config, "WEBHOOK", "Method", "webhook.method") - self._ini_get(config, "WEBHOOK", "body", "webhook.body") - self._ini_get(config, "WEBHOOK", "type", "webhook.type") - self._ini_get_dict(config, "WEBHOOK", "headers", "webhook.headers") - self._ini_get(config, "WEBHOOK", "Username", "webhook.username") - self._ini_get(config, "WEBHOOK", "Password", "webhook.password") - self._ini_get_int(config, "WEBHOOK", "timeout", "webhook.timeout") - self._ini_get_cron(config, "WEBHOOK", "cron", "webhook.cron") - - self._ini_get_boolean(config, "TELEGRAM", - "enabled", "telegram.enabled") - self._ini_get(config, "TELEGRAM", "token", "telegram.token") - self._ini_get_array(config, "TELEGRAM", - "chat_ids", "telegram.chat_ids") - self._ini_get_int(config, "TELEGRAM", - "timeout", "telegram.timeout") - self._ini_get_boolean(config, "TELEGRAM", "disableCommands", - "telegram.disable_commands") - self._ini_get_cron(config, "TELEGRAM", "cron", "telegram.cron") - self._ini_get(config, "TELEGRAM", "body", "telegram.body") - self._ini_get(config, "TELEGRAM", "image", "telegram.image") - - self._ini_get_boolean(config, "SCRIPT", - "enabled", "script.enabled") - self._ini_get(config, "SCRIPT", "Command", "script.command") - self._ini_get_cron(config, "SCRIPT", "cron", "script.cron") - - self._ini_get_boolean(config, "LOCATION", - "enabled", "location.enabled") - self._ini_get(config, "LOCATION", "Address", - "location.origin_address") - self._ini_get( - config, "LOCATION", - "Google_Maps_API_Key", "location.gmaps_api_key") + value = parser.getboolean(section, key, fallback=None) + except ValueError as err: + raise ConfigurationError(f"Invalid boolean value for {section}.{key} - {err}") from err + if value is not None: + setattr(self, attr, value) + def _ini_get_int(self, parser: configparser.ConfigParser, section: str, key: str, attr: str): + try: + value = parser.getint(section, key, fallback=None) except ValueError as err: - raise ConfigurationError(err) from err + raise ConfigurationError(f"Invalid integer value for {section}.{key} - {err}") from err + if value is not None: + setattr(self, attr, value) - def _env_get(self, key: str, attr: str) -> None: - value = environ.get(key, None) + def _ini_get_list(self, parser: configparser.ConfigParser, section: str, key: str, attr: str): + value = parser.get(section, key, fallback=None) if value is not None: - value = self._decode(value) - self._setattr(attr, value) + setattr(self, attr, [self._decode(val.strip()) for val in value.split(",")]) + + def _ini_get_dict(self, parser: configparser.ConfigParser, section: str, key: str, attr: str): + value = parser.get(section, key, fallback=None) + if value is not None: + try: + setattr(self, attr, json.loads(value)) + except json.JSONDecodeError as err: + raise ConfigurationError(f"Invalid JSON value for {section}.{key} - {err}") from err - def _env_get_boolean(self, key: str, attr: str) -> None: + def _ini_get_cron(self, parser: configparser.ConfigParser, section: str, key: str, attr: str): + value = parser.get(section, key, fallback=None) + if value is not None: + try: + setattr(self, attr, Cron(value)) + except ValueError as err: + raise ConfigurationError(f"Invalid cron value for {section}.{key} - {err}") from err + + def _env_get(self, key: str, attr: str): value = environ.get(key, None) if value is not None: - self._setattr(attr, value.lower() in ('true', '1', 't')) + setattr(self, attr, self._decode(value)) - def _env_get_int(self, key: str, attr: str) -> None: - self._setattr(attr, int(environ.get(key, self._getattr(attr)))) + def _env_get_boolean(self, key: str, attr: str): + value = environ.get(key, None) + if value is not None: + setattr(self, attr, value.lower() in {"true", "1", "t", "y", "yes"}) - def _env_get_float(self, key: str, attr: str) -> None: - self._setattr(attr, float(environ.get(key, self._getattr(attr)))) + def _env_get_int(self, key: str, attr: str): + value = environ.get(key, None) + if value is not None: + try: + setattr(self, attr, int(value)) + except ValueError as err: + raise ConfigurationError(f"Invalid integer value for {key} - {err}") from err - def _env_get_array(self, key: str, attr: str) -> None: + def _env_get_list(self, key: str, attr: str): value = environ.get(key, None) - if value: - arr = [self._decode(val.strip()) for val in value.split(',')] - self._setattr(attr, arr) + if value is not None: + setattr(self, attr, [self._decode(val.strip()) for val in value.split(",")]) - def _env_get_dict(self, key: str, attr: str) -> None: + def _env_get_dict(self, key: str, attr: str): value = environ.get(key, None) - if value: - dic = json.loads(value) - self._setattr(attr, dic) + if value is not None: + try: + setattr(self, attr, json.loads(value)) + except json.JSONDecodeError as err: + raise ConfigurationError(f"Invalid JSON value for {key} - {err}") from err - def _env_get_cron(self, key: str, attr: str) -> None: + def _env_get_cron(self, key: str, attr: str): value = environ.get(key, None) if value is not None: - self._setattr(attr, Cron(value)) + try: + setattr(self, attr, Cron(value)) + except ValueError as err: + raise ConfigurationError(f"Invalid cron value for {key} - {err}") from err + + +@dataclass +class NotifierConfig(BaseConfig): + """Base Notifier configuration""" + + enabled: bool = False + cron: Cron = field(default_factory=Cron) + + +@dataclass +class AppriseConfig(NotifierConfig): + """Apprise Notifier configuration""" + + url: Union[str, None] = None + title: str = "New Magic Bags" + body: str = "${{display_name}} - new amount: ${{items_available}} - ${{link}}" + + def _read_ini(self, parser: configparser.ConfigParser): + self._ini_get_boolean(parser, "APPRISE", "Enabled", "enabled") + self._ini_get_cron(parser, "APPRISE", "Cron", "cron") + self._ini_get(parser, "APPRISE", "URL", "url") + self._ini_get(parser, "APPRISE", "Title", "title") + self._ini_get(parser, "APPRISE", "Body", "body") + + def _read_env(self): + self._env_get_boolean("APPRISE", "enabled") + self._env_get_cron("APPRISE_CRON", "cron") + self._env_get("APPRISE_URL", "url") + self._env_get("APPRISE_TITLE", "title") + self._env_get("APPRISE_BODY", "body") + + +@dataclass +class TelegramConfig(NotifierConfig): + """Telegram Notifier configuration""" + + token: Union[str, None] = None + chat_ids: list[str] = field(default_factory=list) + disable_commands: bool = False + timeout: int = 60 + body: str = ( + "*${{display_name}}*\n*Available*: ${{items_available}}\n*Price*: ${{price}} ${{currency}}\n*Pickup*: ${{pickupdate}}" + ) + image: Union[str, None] = None + + def _read_ini(self, parser: configparser.ConfigParser): + self._ini_get_boolean(parser, "TELEGRAM", "Enabled", "enabled") + self._ini_get_cron(parser, "TELEGRAM", "Cron", "cron") + self._ini_get(parser, "TELEGRAM", "Token", "token") + if parser.has_option("TELEGRAM", "chat_ids"): + log.warning(DEPRECIATION_WARNING.format("[TELEGRAM] chat_ids", "ChatIDs")) + self._ini_get_list(parser, "TELEGRAM", "chat_ids", "chat_ids") # legacy support + self._ini_get_list(parser, "TELEGRAM", "ChatIDs", "chat_ids") + self._ini_get_boolean(parser, "TELEGRAM", "DisableCommands", "disable_commands") + self._ini_get_int(parser, "TELEGRAM", "Timeout", "timeout") + self._ini_get(parser, "TELEGRAM", "Body", "body") + self._ini_get(parser, "TELEGRAM", "Image", "image") + + def _read_env(self): + self._env_get_boolean("TELEGRAM", "enabled") + self._env_get_cron("TELEGRAM_CRON", "cron") + self._env_get("TELEGRAM_TOKEN", "token") + self._env_get_list("TELEGRAM_CHAT_IDS", "chat_ids") + self._env_get_boolean("TELEGRAM_DISABLE_COMMANDS", "disable_commands") + self._env_get_int("TELEGRAM_TIMEOUT", "timeout") + self._env_get("TELEGRAM_BODY", "body") + self._env_get("TELEGRAM_IMAGE", "image") + + +@dataclass +class PushSaferConfig(NotifierConfig): + """PushSafer Notifier configuration""" + + key: Union[str, None] = None + device_id: Union[str, None] = None + + def _read_ini(self, parser: configparser.ConfigParser): + self._ini_get_boolean(parser, "PUSHSAFER", "Enabled", "enabled") + self._ini_get_cron(parser, "PUSHSAFER", "Cron", "cron") + self._ini_get(parser, "PUSHSAFER", "Key", "key") + self._ini_get(parser, "PUSHSAFER", "DeviceID", "device_id") + + def _read_env(self): + self._env_get_boolean("PUSHSAFER", "enabled") + self._env_get_cron("PUSHSAFER_CRON", "cron") + self._env_get("PUSHSAFER_KEY", "key") + self._env_get("PUSHSAFER_DEVICE_ID", "device_id") + + +@dataclass +class ConsoleConfig(NotifierConfig): + """Console Notifier configuration""" + + body: str = "${{display_name}} - new amount: ${{items_available}} - ${{link}}" + + def _read_ini(self, parser: configparser.ConfigParser): + self._ini_get_boolean(parser, "CONSOLE", "Enabled", "enabled") + self._ini_get_cron(parser, "CONSOLE", "Cron", "cron") + self._ini_get(parser, "CONSOLE", "Body", "body") + + def _read_env(self): + self._env_get_boolean("CONSOLE", "enabled") + self._env_get_cron("CONSOLE_CRON", "cron") + self._env_get("CONSOLE_BODY", "body") + + +@dataclass +class SMTPConfig(NotifierConfig): + """SMTP Notifier configuration""" + + host: Union[str, None] = None + port: Union[int, None] = None + username: Union[str, None] = None + password: Union[str, None] = None + use_tls: bool = False + use_ssl: bool = False + sender: Union[str, None] = None + recipients: list[str] = field(default_factory=list) + subject: str = "New Magic Bags" + body: str = "${{display_name}}
New Amount: ${{items_available}}" + + def _read_ini(self, parser: configparser.ConfigParser): + self._ini_get_boolean(parser, "SMTP", "Enabled", "enabled") + self._ini_get_cron(parser, "SMTP", "Cron", "cron") + self._ini_get(parser, "SMTP", "Host", "host") + self._ini_get_int(parser, "SMTP", "Port", "port") + self._ini_get(parser, "SMTP", "Username", "username") + self._ini_get(parser, "SMTP", "Password", "password") + self._ini_get_boolean(parser, "SMTP", "TLS", "use_tls") + self._ini_get_boolean(parser, "SMTP", "SSL", "use_ssl") + self._ini_get(parser, "SMTP", "Sender", "sender") + if parser.has_option("SMTP", "Recipient"): + log.warning(DEPRECIATION_WARNING.format("[SMTP] Recipient", "Recipients")) + self._ini_get_list(parser, "SMTP", "Recipient", "recipients") # legacy support + self._ini_get_list(parser, "SMTP", "Recipients", "recipients") + self._ini_get(parser, "SMTP", "Subject", "subject") + self._ini_get(parser, "SMTP", "Body", "body") + + def _read_env(self): + self._env_get_boolean("SMTP", "enabled") + self._env_get_cron("SMTP_CRON", "cron") + self._env_get("SMTP_HOST", "host") + self._env_get_int("SMTP_PORT", "port") + self._env_get("SMTP_USERNAME", "username") + self._env_get("SMTP_PASSWORD", "password") + self._env_get_boolean("SMTP_TLS", "use_tls") + self._env_get_boolean("SMTP_SSL", "use_ssl") + self._env_get("SMTP_SENDER", "sender") + if environ.get("SMTP_RECIPIENT", None): + log.warning(DEPRECIATION_WARNING.format("SMTP_RECIPIENT", "SMTP_RECIPIENTS")) + self._env_get_list("SMTP_RECIPIENT", "recipients") # legacy support + self._env_get_list("SMTP_RECIPIENTS", "recipients") + self._env_get("SMTP_SUBJECT", "subject") + self._env_get("SMTP_BODY", "body") + + +@dataclass +class IFTTTConfig(NotifierConfig): + """IFTTT Notifier configuration""" + + event: str = "tgtg_notification" + key: Union[str, None] = None + body: str = '{"value1": "${{display_name}}", "value2": ${{items_available}}, "value3": "${{link}}"}' + timeout: int = 60 + + def _read_ini(self, parser: configparser.ConfigParser): + self._ini_get_boolean(parser, "IFTTT", "Enabled", "enabled") + self._ini_get_cron(parser, "IFTTT", "Cron", "cron") + self._ini_get(parser, "IFTTT", "Event", "event") + self._ini_get(parser, "IFTTT", "Key", "key") + self._ini_get(parser, "IFTTT", "Body", "body") + self._ini_get_int(parser, "IFTTT", "Timeout", "timeout") + + def _read_env(self): + self._env_get_boolean("IFTTT", "enabled") + self._env_get_cron("IFTTT_CRON", "cron") + self._env_get("IFTTT_EVENT", "event") + self._env_get("IFTTT_KEY", "key") + self._env_get("IFTTT_BODY", "body") + self._env_get_int("IFTTT_TIMEOUT", "timeout") + + +@dataclass +class NtfyConfig(NotifierConfig): + """Ntfy Notifier configuration""" + + server: str = "https://ntfy.sh" + topic: Union[str, None] = None + title: str = "New Magic Bags" + message: str = "${{display_name}} - New Amount: ${{items_available}} - ${{link}}" + body: Union[str, None] = None + priority: str = "default" + tags: str = "shopping,tgtg" + click: str = "${{link}}" + username: Union[str, None] = None + password: Union[str, None] = None + timeout: int = 60 + + def _read_ini(self, parser: configparser.ConfigParser): + self._ini_get_boolean(parser, "NTFY", "Enabled", "enabled") + self._ini_get_cron(parser, "NTFY", "Cron", "cron") + self._ini_get(parser, "NTFY", "Server", "server") + self._ini_get(parser, "NTFY", "Topic", "topic") + self._ini_get(parser, "NTFY", "Title", "title") + self._ini_get(parser, "NTFY", "Message", "message") + self._ini_get(parser, "NTFY", "Body", "body") + self._ini_get(parser, "NTFY", "Priority", "priority") + self._ini_get(parser, "NTFY", "Tags", "tags") + self._ini_get(parser, "NTFY", "Click", "click") + self._ini_get(parser, "NTFY", "Username", "username") + self._ini_get(parser, "NTFY", "Password", "password") + self._ini_get_int(parser, "NTFY", "Timeout", "timeout") + + def _read_env(self): + self._env_get_boolean("NTFY", "enabled") + self._env_get_cron("NTFY_CRON", "cron") + self._env_get("NTFY_SERVER", "server") + self._env_get("NTFY_TOPIC", "topic") + self._env_get("NTFY_TITLE", "title") + self._env_get("NTFY_MESSAGE", "message") + self._env_get("NTFY_BODY", "body") + self._env_get("NTFY_PRIORITY", "priority") + self._env_get("NTFY_TAGS", "tags") + self._env_get("NTFY_CLICK", "click") + self._env_get("NTFY_USERNAME", "username") + self._env_get("NTFY_PASSWORD", "password") + self._env_get_int("NTFY_TIMEOUT", "timeout") + + +@dataclass +class WebhookConfig(NotifierConfig): + """Webhook Notifier configuration""" + + url: Union[str, None] = None + method: str = "POST" + headers: dict[str, str | bytes] = field(default_factory=dict) + body: str = "" + type: str = "text/plain" + timeout: int = 60 + username: Union[str, None] = None + password: Union[str, None] = None + + def _read_ini(self, parser: configparser.ConfigParser): + self._ini_get_boolean(parser, "WEBHOOK", "Enabled", "enabled") + self._ini_get_cron(parser, "WEBHOOK", "Cron", "cron") + self._ini_get(parser, "WEBHOOK", "URL", "url") + self._ini_get(parser, "WEBHOOK", "Method", "method") + self._ini_get_dict(parser, "WEBHOOK", "Headers", "headers") + self._ini_get(parser, "WEBHOOK", "Body", "body") + self._ini_get(parser, "WEBHOOK", "Type", "type") + self._ini_get(parser, "WEBHOOK", "Username", "username") + self._ini_get(parser, "WEBHOOK", "Password", "password") + self._ini_get_int(parser, "WEBHOOK", "Timeout", "timeout") + + def _read_env(self): + self._env_get_boolean("WEBHOOK", "enabled") + self._env_get_cron("WEBHOOK_CRON", "cron") + self._env_get("WEBHOOK_URL", "url") + self._env_get("WEBHOOK_METHOD", "method") + self._env_get_dict("WEBHOOK_HEADERS", "headers") + self._env_get("WEBHOOK_BODY", "body") + self._env_get("WEBHOOK_TYPE", "type") + self._env_get("WEBHOOK_USERNAME", "username") + self._env_get("WEBHOOK_PASSWORD", "password") + self._env_get_int("WEBHOOK_TIMEOUT", "timeout") + + +@dataclass +class ScriptConfig(NotifierConfig): + """Script Notifier configuration""" + + command: Union[str, None] = None + + def _read_ini(self, parser: configparser.ConfigParser): + self._ini_get_boolean(parser, "SCRIPT", "Enabled", "enabled") + self._ini_get_cron(parser, "SCRIPT", "Cron", "cron") + self._ini_get(parser, "SCRIPT", "Command", "command") + + def _read_env(self): + self._env_get_boolean("SCRIPT", "enabled") + self._env_get_cron("SCRIPT_CRON", "cron") + self._env_get("SCRIPT_COMMAND", "command") + + +@dataclass +class TgtgConfig(BaseConfig): + """Tgtg configuration""" + + username: Union[str, None] = None + access_token: Union[str, None] = None + refresh_token: Union[str, None] = None + user_id: Union[str, None] = None + datadome: Union[str, None] = None + timeout: int = 60 + access_token_lifetime: int = 14400 + max_polling_tries: int = 24 + polling_wait_time: int = 5 + base_url: str = BASE_URL + + def _read_ini(self, parser: configparser.ConfigParser): + self._ini_get(parser, "TGTG", "Username", "username") + self._ini_get(parser, "TGTG", "AccessToken", "access_token") + self._ini_get(parser, "TGTG", "RefreshToken", "refresh_token") + self._ini_get(parser, "TGTG", "UserID", "user_id") + self._ini_get(parser, "TGTG", "Datadome", "datadome") + self._ini_get_int(parser, "TGTG", "Timeout", "timeout") + self._ini_get_int(parser, "TGTG", "AccessTokenLifetime", "access_token_lifetime") + self._ini_get_int(parser, "TGTG", "MaxPollingTries", "max_polling_tries") + self._ini_get_int(parser, "TGTG", "PollingWaitTime", "polling_wait_time") + + def _read_env(self): + self._env_get("TGTG_USERNAME", "username") + self._env_get("TGTG_ACCESS_TOKEN", "access_token") + self._env_get("TGTG_REFRESH_TOKEN", "refresh_token") + self._env_get("TGTG_USER_ID", "user_id") + self._env_get("TGTG_DATADOME", "datadome") + self._env_get_int("TGTG_TIMEOUT", "timeout") + self._env_get_int("TGTG_ACCESS_TOKEN_LIFETIME", "access_token_lifetime") + self._env_get_int("TGTG_MAX_POLLING_TRIES", "max_polling_tries") + self._env_get_int("TGTG_POLLING_WAIT_TIME", "polling_wait_time") + + +@dataclass +class LocationConfig(BaseConfig): + """Location configuration""" + + enabled: bool = False + google_maps_api_key: Union[str, None] = None + origin_address: Union[str, None] = None + + def _read_ini(self, parser: configparser.ConfigParser): + self._ini_get_boolean(parser, "LOCATION", "Enabled", "enabled") + if parser.has_option("LOCATION", "Google_Maps_API_Key"): + log.warning(DEPRECIATION_WARNING.format("[LOCATION] Google_Maps_API_Key", "GoogleMapsAPIKey")) + self._ini_get(parser, "LOCATION", "Google_Maps_API_Key", "google_maps_api_key") # legacy support + self._ini_get(parser, "LOCATION", "GoogleMapsAPIKey", "google_maps_api_key") + if parser.has_option("LOCATION", "Address"): + log.warning(DEPRECIATION_WARNING.format("[LOCATION] Address", "OriginAddress")) + self._ini_get(parser, "LOCATION", "Address", "origin_address") # legacy support + self._ini_get(parser, "LOCATION", "OriginAddress", "origin_address") + + def _read_env(self): + self._env_get_boolean("LOCATION", "enabled") + self._env_get("LOCATION_GOOGLE_MAPS_API_KEY", "google_maps_api_key") + if environ.get("LOCATION_ADDRESS", None): + log.warning(DEPRECIATION_WARNING.format("LOCATION_ADDRESS", "LOCATION_ORIGIN_ADDRESS")) + self._env_get("LOCATION_ADDRESS", "origin_address") # legacy support + self._env_get("LOCATION_ORIGIN_ADDRESS", "origin_address") + + +@dataclass +class Config(BaseConfig): + """Main configuration""" + + file: Union[str, None] = None + item_ids: list[str] = field(default_factory=list) + sleep_time: int = 60 + schedule_cron: Cron = field(default_factory=Cron) + debug: bool = False + locale: str = "en_US" + metrics: bool = False + metrics_port: int = 8000 + disable_tests: bool = False + quiet: bool = False + docker: bool = False + activity: bool = True + tgtg: TgtgConfig = field(default_factory=TgtgConfig) + location: LocationConfig = field(default_factory=LocationConfig) + token_path: Union[str, None] = None + apprise: AppriseConfig = field(default_factory=AppriseConfig) + telegram: TelegramConfig = field(default_factory=TelegramConfig) + pushsafer: PushSaferConfig = field(default_factory=PushSaferConfig) + console: ConsoleConfig = field(default_factory=ConsoleConfig) + smtp: SMTPConfig = field(default_factory=SMTPConfig) + ifttt: IFTTTConfig = field(default_factory=IFTTTConfig) + ntfy: NtfyConfig = field(default_factory=NtfyConfig) + webhook: WebhookConfig = field(default_factory=WebhookConfig) + script: ScriptConfig = field(default_factory=ScriptConfig) + + def __post_init__(self): + if self.file: + config_file = Path(self.file) + if not config_file.is_file(): + raise ConfigurationError(f"Configuration file '{config_file.absolute()}' is not a file!") + config_file = Path(self.file) + parser = configparser.ConfigParser() + parser.read(config_file, encoding="utf-8") + self._read_ini(parser) + self.tgtg._read_ini(parser) + self.location._read_ini(parser) + self.apprise._read_ini(parser) + self.telegram._read_ini(parser) + self.pushsafer._read_ini(parser) + self.console._read_ini(parser) + self.smtp._read_ini(parser) + self.ifttt._read_ini(parser) + self.ntfy._read_ini(parser) + self.webhook._read_ini(parser) + self.script._read_ini(parser) + + log.info("Loaded config from %s", config_file.absolute()) + else: + self._read_env() + self.tgtg._read_env() + self.location._read_env() + self.apprise._read_env() + self.telegram._read_env() + self.pushsafer._read_env() + self.console._read_env() + self.smtp._read_env() + self.ifttt._read_env() + self.ntfy._read_env() + self.webhook._read_env() + self.script._read_env() - def _read_env(self) -> None: - try: - self._env_get_boolean("DEBUG", "debug") - self._env_get_array("ITEM_IDS", "item_ids") - self._env_get_int("SLEEP_TIME", "sleep_time") - self._env_get_cron("SCHEDULE_CRON", "schedule_cron") - self._env_get_boolean("METRICS", "metrics") - self._env_get_int("METRICS_PORT", "metrics_port") - self._env_get_boolean("DISABLE_TESTS", "disable_tests") - self._env_get_boolean("QUIET", "quiet") - self._env_get("LOCALE", "locale") - self._env_get_boolean("ACTIVITY", "activity") - - self._env_get("TGTG_USERNAME", "tgtg.username") - self._env_get("TGTG_ACCESS_TOKEN", "tgtg.access_token") - self._env_get("TGTG_REFRESH_TOKEN", "tgtg.refresh_token") - self._env_get("TGTG_USER_ID", "tgtg.user_id") - self._env_get("TGTG_DATADOME", "tgtg.datadome") - self._env_get_int("TGTG_TIMEOUT", "tgtg.timeout") - self._env_get_int("TGTG_ACCESS_TOKEN_LIFETIME", - "tgtg.access_token_lifetime") - self._env_get_int("TGTG_MAX_POLLING_TRIES", - "tgtg.max_polling_tries") - self._env_get_int("TGTG_POLLING_WAIT_TIME", - "tgtg.polling_wait_time") - - self._env_get_boolean("APPRISE", "apprise.enabled") - self._env_get("APPRISE_URL", "apprise.url") - self._env_get_cron("APPRISE_CRON", "apprise.cron") - self._env_get("APPRISE_TITLE", "apprise.title") - self._env_get("APPRISE_BODY", "apprise.body") - - self._env_get_boolean("CONSOLE", "console.enabled") - self._env_get("CONSOLE_BODY", "console.body") - self._env_get_cron("CONSOLE_CRON", "console.cron") - - self._env_get_boolean("PUSH_SAFER", "push_safer.enabled") - self._env_get("PUSH_SAFER_KEY", "push_safer.key") - self._env_get("PUSH_SAFER_DEVICE_ID", "push_safer.deviceId") - self._env_get_cron("PUSH_SAFER_CRON", "push_safer.cron") - - self._env_get_boolean("SMTP", "smtp.enabled") - self._env_get("SMTP_HOST", "smtp.host") - self._env_get_int("SMTP_PORT", "smtp.port") - self._env_get_boolean("SMTP_TLS", "smtp.tls") - self._env_get_boolean("SMTP_SSL", "smtp.ssl") - self._env_get("SMTP_USERNAME", "smtp.username") - self._env_get("SMTP_PASSWORD", "smtp.password") - self._env_get("SMTP_SENDER", "smtp.sender") - self._env_get_array("SMTP_RECIPIENT", "smtp.recipient") - self._env_get_cron("SMTP_CRON", "smtp.cron") - self._env_get("SMTP_SUBJECT", "smtp.subject") - self._env_get("SMTP_BODY", "smtp.body") - - self._env_get_boolean("IFTTT", "ifttt.enabled") - self._env_get("IFTTT_EVENT", "ifttt.event") - self._env_get("IFTTT_KEY", "ifttt.key") - self._env_get("IFTTT_BODY", "ifttt.body") - self._env_get_int("IFTTT_TIMEOUT", "ifttt.timeout") - self._env_get_cron("IFTTT_CRON", "ifttt.cron") - - self._env_get_boolean("NTFY", "ntfy.enabled") - self._env_get("NTFY_SERVER", "ntfy.server") - self._env_get("NTFY_TOPIC", "ntfy.topic") - self._env_get("NTFY_TITLE", "ntfy.title") - self._env_get("NTFY_MESSAGE", "ntfy.message") - self._env_get("NTFY_BODY", "ntfy.body") - self._env_get("NTFY_PRIORITY", "ntfy.priority") - self._env_get("NTFY_TAGS", "ntfy.tags") - self._env_get("NTFY_CLICK", "ntfy.click") - self._env_get("NTFY_USERNAME", "ntfy.username") - self._env_get("NTFY_PASSWORD", "ntfy.password") - self._env_get_int("NTFY_TIMEOUT", "ntfy.timeout") - self._env_get_cron("NTFY_CRON", "ntfy.cron") - - self._env_get_boolean("WEBHOOK", "webhook.enabled") - self._env_get("WEBHOOK_URL", "webhook.url") - self._env_get("WEBHOOK_METHOD", "webhook.method") - self._env_get("WEBHOOK_BODY", "webhook.body") - self._env_get("WEBHOOK_TYPE", "webhook.type") - self._env_get_dict("WEBHOOK_HEADERS", "webhook.headers") - self._env_get("NTFY_USERNAME", "webhook.username") - self._env_get("NTFY_PASSWORD", "webhook.password") - self._env_get_int("WEBHOOK_TIMEOUT", "webhook.timeout") - self._env_get_cron("WEBHOOK_CRON", "webhook.cron") - - self._env_get_boolean("TELEGRAM", "telegram.enabled") - self._env_get("TELEGRAM_TOKEN", "telegram.token") - self._env_get_array("TELEGRAM_CHAT_IDS", "telegram.chat_ids") - self._env_get_int("TELEGRAM_TIMEOUT", "telegram.timeout") - self._env_get_boolean("TELEGRAM_DISABLE_COMMANDS", - "telegram.disable_commands") - self._env_get_cron("TELEGRAM_CRON", "telegram.cron") - self._env_get("TELEGRAM_BODY", "telegram.body") - self._env_get("TELEGRAM_IMAGE", "telegram.image") - - self._env_get_boolean("SCRIPT", "script.enabled") - self._env_get("SCRIPT_COMMAND", "script.command") - self._env_get_cron("SCRIPT_CRON", "script.cron") - - self._env_get_boolean("LOCATION", "location.enabled") - self._env_get("LOCATION_GOOGLE_MAPS_API_KEY", - "location.gmaps_api_key") - self._env_get("LOCATION_ADDRESS", "location.origin_address") - except ValueError as err: - raise ConfigurationError(err) from err + log.info("Loaded config from environment variables") + + self.token_path = environ.get("TGTG_TOKEN_PATH", None) + self._load_tokens() + if self.locale and not self.locale.startswith("en"): + try: + humanize.i18n.activate(self.locale) + except FileNotFoundError as err: + raise ConfigurationError(f"Invalid locale '{self.locale}' - {err}") from err + + def _read_ini(self, parser: configparser.ConfigParser): + self._ini_get_list(parser, "MAIN", "ItemIDs", "item_ids") + self._ini_get_int(parser, "MAIN", "SleepTime", "sleep_time") + self._ini_get_cron(parser, "MAIN", "ScheduleCron", "schedule_cron") + self._ini_get_boolean(parser, "MAIN", "Debug", "debug") + self._ini_get(parser, "MAIN", "Locale", "locale") + self._ini_get_boolean(parser, "MAIN", "Metrics", "metrics") + self._ini_get_int(parser, "MAIN", "MetricsPort", "metrics_port") + self._ini_get_boolean(parser, "MAIN", "DisableTests", "disable_tests") + self._ini_get_boolean(parser, "MAIN", "Quiet", "quiet") + self._ini_get_boolean(parser, "MAIN", "Docker", "docker") + self._ini_get_boolean(parser, "MAIN", "Activity", "activity") + + def _read_env(self): + self._env_get_list("ITEM_IDS", "item_ids") + self._env_get_int("SLEEP_TIME", "sleep_time") + self._env_get_cron("SCHEDULE_CRON", "schedule_cron") + self._env_get_boolean("DEBUG", "debug") + self._env_get("LOCALE", "locale") + self._env_get_boolean("METRICS", "metrics") + self._env_get_int("METRICS_PORT", "metrics_port") + self._env_get_boolean("DISABLE_TESTS", "disable_tests") + self._env_get_boolean("QUIET", "quiet") + self._env_get_boolean("DOCKER", "docker") + self._env_get_boolean("ACTIVITY", "activity") + + def _open(self, file: str, mode: str) -> IO[Any]: + if self.token_path is None: + raise ConfigurationError("Token path is not set.") + return open(Path(self.token_path, file), mode, encoding="utf-8") - def set(self, section: str, option: str, value: Any) -> bool: + def _load_tokens(self) -> None: """ - Sets an option in config.ini if provided. + Reads tokens from token files """ - if self.file is not None: + if self.token_path is not None: try: - config = configparser.ConfigParser() - config.optionxform = str - config.read(self.file, encoding='utf-8') - if section not in config.sections(): - config.add_section(section) - config.set(section, option, str(value)) - with open(self.file, 'w', encoding='utf-8') as configfile: - config.write(configfile) - return True + with self._open("accessToken", "r") as file: + self.tgtg.access_token = file.read() + with self._open("refreshToken", "r") as file: + self.tgtg.refresh_token = file.read() + with self._open("userID", "r") as file: + self.tgtg.user_id = file.read() + with self._open("datadome", "r") as file: + self.tgtg.datadome = file.read() + except FileNotFoundError: + log.warning("No token files in token path.") except EnvironmentError as err: - log.error("error writing config.ini! - %s", err) - return False + log.error("Error loading Tokens - %s", err) - def save_tokens(self, access_token: str, refresh_token: str, - user_id: str, datadome: str) -> None: + def save_tokens(self, access_token: str, refresh_token: str, user_id: str, datadome: str) -> None: """ Saves TGTG Access Tokens to config.ini if provided or as files to token_path. """ if self.file is not None: try: + config_file = Path(self.file) config = configparser.ConfigParser() - config.optionxform = str - config.read(self.file, encoding='utf-8') + config.optionxform = str # type: ignore + config.read(config_file, encoding="utf-8") if "TGTG" not in config.sections(): config.add_section("TGTG") config.set("TGTG", "AccessToken", access_token) config.set("TGTG", "RefreshToken", refresh_token) config.set("TGTG", "UserId", user_id) config.set("TGTG", "Datadome", datadome) - with open(self.file, 'w', encoding='utf-8') as configfile: + with open(config_file, "w", encoding="utf-8") as configfile: + configfile.write(CONFIG_FILE_HEADER) config.write(configfile) except EnvironmentError as err: log.error("error saving credentials to config.ini! - %s", err) if self.token_path is not None: try: - with self._open('accessToken', 'w') as file: + with self._open("accessToken", "w") as file: file.write(access_token) - with self._open('refreshToken', 'w') as file: + with self._open("refreshToken", "w") as file: file.write(refresh_token) - with self._open('userID', 'w') as file: + with self._open("userID", "w") as file: file.write(user_id) - with self._open('datadome', 'w') as file: + with self._open("datadome", "w") as file: file.write(datadome) except EnvironmentError as err: log.error("error saving credentials! - %s", err) + + def set(self, section: str, option: str, value: str) -> bool: + """ + Sets an option in config.ini if provided. + """ + if self.file is not None: + try: + config = configparser.ConfigParser() + config.optionxform = str # type: ignore + config.read(self.file, encoding="utf-8") + if section not in config.sections(): + config.add_section(section) + config.set(section, option, str(value)) + with open(self.file, "w", encoding="utf-8") as configfile: + config.write(configfile) + return True + except EnvironmentError as err: + log.error("error writing config.ini! - %s", err) + return False diff --git a/tgtg_scanner/models/cron.py b/tgtg_scanner/models/cron.py index 7717a5f4..9b92ee40 100644 --- a/tgtg_scanner/models/cron.py +++ b/tgtg_scanner/models/cron.py @@ -1,43 +1,36 @@ import logging +from typing import Union import pycron from cron_descriptor import Options, get_description -from tgtg_scanner.models.errors import ConfigurationError +log = logging.getLogger("tgtg") -log = logging.getLogger('tgtg') - -class Cron(): - def __init__(self, cron_str: str = None) -> None: - self.crons = ( - list(dict.fromkeys([cron.strip() for cron in cron_str.split(';')])) - if cron_str else ['* * * * *']) +class Cron: + def __init__(self, cron_str: Union[str, None] = None) -> None: + self.crons = list(dict.fromkeys([cron.strip() for cron in cron_str.split(";")])) if cron_str else ["* * * * *"] self.options = Options() self.options.use_24hour_time_format = True self.options.day_of_week_start_index_zero = True try: self.is_now except ValueError as err: - raise ConfigurationError( - f"Cron expression parsing error - {err}") from err + raise ValueError(f"Cron expression parsing error - {err}") from err for cron in self.crons: _, _, _, _, dow = cron.split() if any(int(day) > 6 for day in dow.split("-") if day.isdigit()): - raise ConfigurationError( - "Cron expression parsing error - " - "day of week must be between 0 and 6 (Sunday=0)") + raise ValueError("Cron expression parsing error - day of week must be between 0 and 6 (Sunday=0)") @property def is_now(self) -> bool: - """ Returns True if the cron expression matches the current time """ + """Returns True if the cron expression matches the current time""" return any(pycron.is_now(cron) for cron in self.crons) def get_description(self, locale: str = "en") -> str: - """ Returns a human-readable description of the cron expression """ + """Returns a human-readable description of the cron expression""" self.options.locale_code = locale - return "; ".join(get_description(cron, options=self.options) - for cron in self.crons) + return "; ".join(get_description(cron, options=self.options) for cron in self.crons) def __eq__(self, __o: object) -> bool: return getattr(__o, "crons") == self.crons diff --git a/tgtg_scanner/models/favorites.py b/tgtg_scanner/models/favorites.py index 1ecf55f9..97e7cd62 100644 --- a/tgtg_scanner/models/favorites.py +++ b/tgtg_scanner/models/favorites.py @@ -2,7 +2,7 @@ from dataclasses import dataclass from typing import List -from tgtg_scanner.models.errors import TgtgAPIError +from tgtg_scanner.errors import TgtgAPIError from tgtg_scanner.models.item import Item from tgtg_scanner.tgtg import TgtgClient @@ -10,20 +10,20 @@ @dataclass -class AddFavoriteRequest(): +class AddFavoriteRequest: item_id: str item_display_name: str proceed: bool @dataclass -class RemoveFavoriteRequest(): +class RemoveFavoriteRequest: item_id: str item_display_name: str proceed: bool -class Favorites(): +class Favorites: def __init__(self, client: TgtgClient) -> None: self.client = client @@ -35,9 +35,7 @@ def is_item_favorite(self, item_id: str) -> bool: Returns: bool: true, if the provided item ID is in the favorites """ - return any(item for item - in self.client.get_favorites() - if Item(item).item_id == item_id) + return any(item for item in self.client.get_favorites() if Item(item).item_id == item_id) def get_item_by_id(self, item_id: str) -> Item: """Gets an item by the Item ID diff --git a/tgtg_scanner/models/item.py b/tgtg_scanner/models/item.py index 6e3db200..5f8dd876 100644 --- a/tgtg_scanner/models/item.py +++ b/tgtg_scanner/models/item.py @@ -7,42 +7,61 @@ import humanize import requests -from tgtg_scanner.models.errors import MaskConfigurationError +from tgtg_scanner.errors import MaskConfigurationError from tgtg_scanner.models.location import DistanceTime, Location -ATTRS = ["item_id", "items_available", "display_name", "description", - "price", "currency", "pickupdate", "favorite", "rating", - "buffet", "item_category", "item_name", "packaging_option", - "pickup_location", "store_name", "item_logo", "item_cover", - "scanned_on", "item_logo_bytes", "item_cover_bytes", "link", - "distance_walking", "distance_driving", "distance_transit", - "distance_biking", "duration_walking", "duration_driving", - "duration_transit", "duration_biking"] - -log = logging.getLogger('tgtg') - - -class Item(): +ATTRS = [ + "item_id", + "items_available", + "display_name", + "description", + "price", + "currency", + "pickupdate", + "favorite", + "rating", + "buffet", + "item_category", + "item_name", + "packaging_option", + "pickup_location", + "store_name", + "item_logo", + "item_cover", + "scanned_on", + "item_logo_bytes", + "item_cover_bytes", + "link", + "distance_walking", + "distance_driving", + "distance_transit", + "distance_biking", + "duration_walking", + "duration_driving", + "duration_transit", + "duration_biking", +] + +log = logging.getLogger("tgtg") + + +class Item: """ Takes the raw data from the TGTG API and returns well formated data for notifications. """ - def __init__(self, data: dict, location: Location = None): + def __init__(self, data: dict, location: Union[Location, None] = None): self.items_available = data.get("items_available", 0) self.display_name = data.get("display_name", "-") self.favorite = "Yes" if data.get("favorite", False) else "No" - self.pickup_interval_start = data.get( - "pickup_interval", {}).get("start", None) - self.pickup_interval_end = data.get( - "pickup_interval", {}).get("end", None) - self.pickup_location = data.get("pickup_location", {}).get( - "address", {}).get("address_line", "-") + self.pickup_interval_start = data.get("pickup_interval", {}).get("start", None) + self.pickup_interval_end = data.get("pickup_interval", {}).get("end", None) + self.pickup_location = data.get("pickup_location", {}).get("address", {}).get("address_line", "-") item = data.get("item", {}) self.item_id = item.get("item_id") - self.rating = item.get("average_overall_rating", {}).get( - "average_overall_rating", None) + self.rating = item.get("average_overall_rating", {}).get("average_overall_rating", None) self.rating = "-" if not self.rating else f"{self.rating:.1f}" self.packaging_option = item.get("packaging_option", "-") self.item_name = item.get("name", "-") @@ -50,17 +69,17 @@ def __init__(self, data: dict, location: Location = None): self.item_category = item.get("item_category", "-") self.description = item.get("description", "-") price_including_taxes = item.get("price_including_taxes", {}) - self.price = (price_including_taxes.get("minor_units", 0) / - 10**price_including_taxes.get("decimals", 0)) + self.price = price_including_taxes.get("minor_units", 0) / 10 ** price_including_taxes.get("decimals", 0) self.price = f"{self.price:.2f}" self.currency = item.get("price_including_taxes", {}).get("code", "-") self.item_logo = item.get("logo_picture", {}).get( "current_url", - "https://tgtg-mkt-cms-prod.s3.eu-west-1.amazonaws.com/" - "13512/TGTG_Icon_White_Cirle_1988x1988px_RGB.png") + "https://tgtg-mkt-cms-prod.s3.eu-west-1.amazonaws.com/13512/TGTG_Icon_White_Cirle_1988x1988px_RGB.png", + ) self.item_cover = item.get("cover_picture", {}).get( "current_url", - "https://images.tgtg.ninja/standard_images/GENERAL/other1.jpg") + "https://images.tgtg.ninja/standard_images/GENERAL/other1.jpg", + ) store = data.get("store", {}) self.store_name = store.get("name", "-") @@ -89,21 +108,19 @@ def check_mask(text: str) -> None: raise MaskConfigurationError(match.group(0)) @staticmethod - def get_image(url: str) -> bytes: + def get_image(url: str) -> Union[bytes, None]: response = requests.get(url) if not response.status_code == HTTPStatus.OK: - log.warning("Get Image Error: %s - %s", - response.status_code, - response.content) + log.warning("Get Image Error: %s - %s", response.status_code, response.content) return None return response.content @property - def item_logo_bytes(self) -> bytes: + def item_logo_bytes(self) -> Union[bytes, None]: return self.get_image(self.item_logo) @property - def item_cover_bytes(self) -> bytes: + def item_cover_bytes(self) -> Union[bytes, None]: return self.get_image(self.item_cover) @property @@ -138,8 +155,7 @@ def pickupdate(self) -> str: now = datetime.datetime.now() pfr = self._datetimeparse(self.pickup_interval_start) pto = self._datetimeparse(self.pickup_interval_end) - prange = (f"{pfr.hour:02d}:{pfr.minute:02d} - " - f"{pto.hour:02d}:{pto.minute:02d}") + prange = f"{pfr.hour:02d}:{pfr.minute:02d} - {pto.hour:02d}:{pto.minute:02d}" tommorow = now + datetime.timedelta(days=1) if now.date() == pfr.date(): return f"{humanize.naturalday(now)}, {prange}" @@ -148,26 +164,26 @@ def pickupdate(self) -> str: return f"{pfr.day}/{pfr.month}, {prange}" return "-" - def _get_distance_time(self, travel_mode: str - ) -> Union[DistanceTime, None]: + def _get_distance_time(self, travel_mode: str) -> Union[DistanceTime, None]: if self.location is None: return None - return self.location.calculate_distance_time( - self.pickup_location, travel_mode) + return self.location.calculate_distance_time(self.pickup_location, travel_mode) def _get_distance(self, travel_mode: str) -> str: distance_time = self._get_distance_time(travel_mode) if distance_time is None: - return 'n/a' + return "n/a" return f"{distance_time.distance / 1000:.1f} km" def _get_duration(self, travel_mode: str) -> str: distance_time = self._get_distance_time(travel_mode) if distance_time is None: - return 'n/a' + return "n/a" return humanize.precisedelta( datetime.timedelta(seconds=distance_time.duration), - minimum_unit="minutes", format="%0.0f") + minimum_unit="minutes", + format="%0.0f", + ) def __getattribute__(self, __name: str) -> Any: try: diff --git a/tgtg_scanner/models/location.py b/tgtg_scanner/models/location.py index ccd8f0a3..47fd3f40 100644 --- a/tgtg_scanner/models/location.py +++ b/tgtg_scanner/models/location.py @@ -4,7 +4,7 @@ import googlemaps -from tgtg_scanner.models.errors import LocationConfigurationError +from tgtg_scanner.errors import LocationConfigurationError log = logging.getLogger("tgtg") @@ -14,8 +14,9 @@ class DistanceTime: """ Dataclass for distance and time. """ - distance: str - duration: str + + distance: float + duration: float travel_mode: str @@ -25,7 +26,7 @@ class Location: PUBLIC_TRANSPORT_MODE = "transit" BIKING_MODE = "bicycling" - def __init__(self, enabled: bool, api_key: str, origin: str): + def __init__(self, enabled: bool = False, api_key: Union[str, None] = None, origin: Union[str, None] = None) -> None: """ Initializes Location class. First run flag important only for validating origin address. @@ -33,21 +34,19 @@ def __init__(self, enabled: bool, api_key: str, origin: str): self.enabled = enabled self.origin = origin if enabled: - if not api_key or not origin: - raise LocationConfigurationError( - "Location enabled but no API key or origin address given") + if api_key is None or self.origin is None: + raise LocationConfigurationError("Location enabled but no API key or origin address given") try: self.gmaps = googlemaps.Client(key=api_key) - except ValueError as exc: + if not self._is_address_valid(self.origin): + raise LocationConfigurationError("Invalid origin address") + except (ValueError, googlemaps.exceptions.ApiError) as exc: raise LocationConfigurationError(exc) from exc - if not self._is_address_valid(self.origin): - raise LocationConfigurationError("Invalid origin address") # cached DistanceTime object for each item_id+mode self.distancetime_dict: dict[str, DistanceTime] = {} - def calculate_distance_time(self, destination: str, travel_mode: str - ) -> Union[DistanceTime, None]: + def calculate_distance_time(self, destination: str, travel_mode: str) -> Union[DistanceTime, None]: """ Calculates the distance and time taken to travel from origin to destination using the given mode of transportation. @@ -60,22 +59,21 @@ def calculate_distance_time(self, destination: str, travel_mode: str if not self._is_address_valid(destination): return None - key = f'{destination}_{travel_mode}' + key = f"{destination}_{travel_mode}" # use cached value if available if key in self.distancetime_dict: return self.distancetime_dict[key] - log.debug(f"Sending Google Maps API request: " - f"{destination} using {travel_mode} mode") + log.debug(f"Sending Google Maps API request: {destination} using {travel_mode} mode") # calculate distance and time - directions = self.gmaps.directions(self.origin, destination, - mode=travel_mode) + directions = self.gmaps.directions(self.origin, destination, mode=travel_mode) distance_time = DistanceTime( - directions[0]["legs"][0]["distance"]["value"], - directions[0]["legs"][0]["duration"]["value"], - travel_mode) + float(directions[0]["legs"][0]["distance"]["value"]), + float(directions[0]["legs"][0]["duration"]["value"]), + travel_mode, + ) # cache value self.distancetime_dict[key] = distance_time diff --git a/tgtg_scanner/models/metrics.py b/tgtg_scanner/models/metrics.py index dd47b524..9a67c899 100644 --- a/tgtg_scanner/models/metrics.py +++ b/tgtg_scanner/models/metrics.py @@ -2,26 +2,26 @@ from prometheus_client import Counter, Gauge, start_http_server -log = logging.getLogger('tgtg') +log = logging.getLogger("tgtg") -class Metrics(): +class Metrics: """ Provides a prometheus metrics client. """ def __init__(self, port: int = 8000): self.port = port - self.item_count = Gauge("tgtg_item_count", - "Currently available bags", - ['item_id', 'display_name']) + self.item_count = Gauge("tgtg_item_count", "Currently available bags", ["item_id", "display_name"]) self.get_favorites_errors = Counter( "tgtg_get_favorites_errors", - "Count of request errors fetching tgtg favorites") + "Count of request errors fetching tgtg favorites", + ) self.send_notifications = Counter( "tgtg_send_notifications", "Count of send notifications", - ['item_id', 'display_name']) + ["item_id", "display_name"], + ) def enable_metrics(self) -> None: """ diff --git a/tgtg_scanner/models/reservations.py b/tgtg_scanner/models/reservations.py index 9034f5f5..f94b4890 100644 --- a/tgtg_scanner/models/reservations.py +++ b/tgtg_scanner/models/reservations.py @@ -9,7 +9,7 @@ @dataclass -class Order(): +class Order: id: str item_id: str amount: int @@ -17,21 +17,19 @@ class Order(): @dataclass -class Reservation(): +class Reservation: item_id: str amount: int display_name: str -class Reservations(): +class Reservations: def __init__(self, client: TgtgClient) -> None: self.client = client self.reservation_query: List[Reservation] = [] self.active_orders: Dict[str, Order] = {} - def reserve(self, item_id: str, - display_name: str, - amount: int = 1) -> None: + def reserve(self, item_id: str, display_name: str, amount: int = 1) -> None: """Create a new reservation Args: @@ -39,11 +37,9 @@ def reserve(self, item_id: str, display_name (str): Item display name amount (int, optional): Amount. Defaults to 1. """ - self.reservation_query.append( - Reservation(item_id, amount, display_name)) + self.reservation_query.append(Reservation(item_id, amount, display_name)) - def make_orders(self, state: Dict[str, Item], - callback: Callable[[Reservation], None]) -> None: + def make_orders(self, state: Dict[str, Item], callback: Callable[[Reservation], None]) -> None: """Create orders for reservations Args: @@ -51,40 +47,39 @@ def make_orders(self, state: Dict[str, Item], callback (Callable[[Reservation], None]): Callback for each order """ for reservation in self.reservation_query: - if state.get(reservation.item_id).items_available > 0: + item = state.get(reservation.item_id) + if item and item.items_available > 0: try: self._create_order(reservation) self.reservation_query.remove(reservation) callback(reservation) except Exception as exc: - log.error("Create Order Error: %s", exc) + log.warning("Order failed: %s", exc) def update_active_orders(self) -> None: - """Remove orders that are not active anymore - """ + """Remove orders that are not active anymore""" for order_id in list(self.active_orders): res = self.client.get_order_status(order_id) if res.get("state") != "RESERVED": del self.active_orders[order_id] def cancel_order(self, order_id: str) -> None: - """Cancel an order - """ + """Cancel an order""" self.client.abort_order(order_id) def cancel_all_orders(self) -> None: - """Cancel all active orders - """ + """Cancel all active orders""" for order_id in list(self.active_orders): self.cancel_order(order_id) def _create_order(self, reservation: Reservation) -> None: - res = self.client.create_order( - reservation.item_id, reservation.amount) + res = self.client.create_order(reservation.item_id, reservation.amount) order_id = res.get("id") if order_id: - order = Order(order_id, - reservation.item_id, - reservation.amount, - reservation.display_name) + order = Order( + order_id, + reservation.item_id, + reservation.amount, + reservation.display_name, + ) self.active_orders[order_id] = order diff --git a/tgtg_scanner/notifiers/__init__.py b/tgtg_scanner/notifiers/__init__.py index 66f6e696..9f60877a 100644 --- a/tgtg_scanner/notifiers/__init__.py +++ b/tgtg_scanner/notifiers/__init__.py @@ -1,4 +1,4 @@ +# flake8: noqa + from tgtg_scanner.notifiers.base import Notifier from tgtg_scanner.notifiers.notifiers import Notifiers - -__all__ = ['Notifier', 'Notifiers'] diff --git a/tgtg_scanner/notifiers/apprise.py b/tgtg_scanner/notifiers/apprise.py index c09a3a9e..9bbefd26 100644 --- a/tgtg_scanner/notifiers/apprise.py +++ b/tgtg_scanner/notifiers/apprise.py @@ -1,13 +1,14 @@ import logging +from typing import Union import apprise -from tgtg_scanner.models import Config, Item -from tgtg_scanner.models.errors import (AppriseConfigurationError, - MaskConfigurationError) +from tgtg_scanner.errors import AppriseConfigurationError, MaskConfigurationError +from tgtg_scanner.models import Config, Favorites, Item, Reservations +from tgtg_scanner.models.reservations import Reservation from tgtg_scanner.notifiers.base import Notifier -log = logging.getLogger('tgtg') +log = logging.getLogger("tgtg") class Apprise(Notifier): @@ -17,15 +18,16 @@ class Apprise(Notifier): https://github.com/caronc/apprise """ - def __init__(self, config: Config): - self.enabled = config.apprise.get("enabled", False) - self.title = config.apprise.get("title") - self.body = config.apprise.get("body") - self.url = config.apprise.get("url") - self.cron = config.apprise.get("cron") - if self.enabled and (not self.url or not self.body): - raise AppriseConfigurationError() + def __init__(self, config: Config, reservations: Reservations, favorites: Favorites): + super().__init__(config, reservations, favorites) + self.enabled = config.apprise.enabled + self.title = config.apprise.title + self.body = config.apprise.body + self.url = config.apprise.url + self.cron = config.apprise.cron if self.enabled: + if self.url is None or self.body is None or self.title is None: + raise AppriseConfigurationError() try: Item.check_mask(self.title) Item.check_mask(self.body) @@ -33,20 +35,23 @@ def __init__(self, config: Config): except MaskConfigurationError as exc: raise AppriseConfigurationError(exc.message) from exc - def _send(self, item: Item) -> None: + def _send(self, item: Union[Item, Reservation]) -> None: """Sends item information via configured Apprise URL""" - url = item.unmask(self.url) - title = item.unmask(self.title) - body = item.unmask(self.body) - - log.debug("Apprise url: %s", url) - log.debug("Apprise title: %s", title) - log.debug("Apprise body: %s", body) - - apobj = apprise.Apprise() - apobj.add(self.url) - apobj.notify(title=title, body=body) - apobj.clear() + if isinstance(item, Item): + if self.url is None or self.body is None or self.title is None: + raise AppriseConfigurationError() + url = item.unmask(self.url) + title = item.unmask(self.title) + body = item.unmask(self.body) + + log.debug("Apprise url: %s", url) + log.debug("Apprise title: %s", title) + log.debug("Apprise body: %s", body) + + apobj = apprise.Apprise() + apobj.add(self.url) + apobj.notify(title=title, body=body) + apobj.clear() def __repr__(self) -> str: return f"Apprise: {self.url}" diff --git a/tgtg_scanner/notifiers/base.py b/tgtg_scanner/notifiers/base.py index 7801efba..c750ba60 100644 --- a/tgtg_scanner/notifiers/base.py +++ b/tgtg_scanner/notifiers/base.py @@ -1,45 +1,77 @@ import logging +import threading from abc import ABC, abstractmethod +from queue import Queue +from typing import Union -from tgtg_scanner.models import Config, Cron, Item +from tgtg_scanner.models import Config, Cron, Favorites, Item, Reservations from tgtg_scanner.models.reservations import Reservation -log = logging.getLogger('tgtg') +log = logging.getLogger("tgtg") class Notifier(ABC): + """Base Notifier""" + @abstractmethod - def __init__(self, config: Config): + def __init__(self, config: Config, reservations: Reservations, favorites: Favorites): self.enabled = False + self.reservations = reservations + self.favorites = favorites self.cron = Cron() + self.thread = threading.Thread(target=self._run) + self.queue: Queue[Union[Item, Reservation, None]] = Queue() @property def name(self): """Get notifier name""" return self.__class__.__name__ - def send(self, item: Item) -> None: - """Send notification for new item""" - if self.enabled and self.cron.is_now: - log.debug("Sending %s Notification", self.name) - self._send(item) - - def send_reservation(self, reservation: Reservation) -> None: - """Send notification for new reservation + def _run(self) -> None: + """Run notifier""" + while True: + try: + item = self.queue.get() + if item is None: + break + log.debug("Sending %s Notification", self.name) + self._send(item) + except KeyboardInterrupt: + pass + except Exception as exc: + log.error("Failed sending %s: %s", self.name, exc) - Args: - reservation (Reservation): Reservation to send - """ + def start(self) -> None: + """Run notifier in thread""" if self.enabled: - log.debug("Sending %s new Reservation", self.name) - self._send_reservation(reservation) + log.debug("Starting %s Notifier thread", self.name) + self.thread.start() + + def send(self, item: Union[Item, Reservation]) -> None: + """Send notification""" + if not isinstance(item, (Item, Reservation)): + log.error("Invalid item type: %s", type(item)) + return + if self.enabled and self.cron.is_now: + self.queue.put(item) + if not self.thread.is_alive(): + log.debug("%s Notifier thread is dead. Restarting", self.name) + self.thread = threading.Thread(target=self._run) + self.start() @abstractmethod - def _send(self, item: Item) -> None: + def _send(self, item: Union[Item, Reservation]) -> None: """Send Item information""" - - def _send_reservation(self, reservation: Reservation) -> None: pass def stop(self) -> None: """Stop notifier""" + if self.thread.is_alive(): + log.debug("Stopping %s Notifier thread", self.name) + self.queue.put(None) + self.thread.join() + log.debug("%s Notifier thread stopped", self.name) + + @abstractmethod + def __repr__(self) -> str: + pass diff --git a/tgtg_scanner/notifiers/console.py b/tgtg_scanner/notifiers/console.py index 2b7d66e7..5deb486a 100644 --- a/tgtg_scanner/notifiers/console.py +++ b/tgtg_scanner/notifiers/console.py @@ -1,20 +1,22 @@ import logging +from typing import Union -from tgtg_scanner.models import Config, Item -from tgtg_scanner.models.errors import (ConsoleConfigurationError, - MaskConfigurationError) +from tgtg_scanner.errors import ConsoleConfigurationError, MaskConfigurationError +from tgtg_scanner.models import Config, Favorites, Item, Reservations +from tgtg_scanner.models.reservations import Reservation from tgtg_scanner.notifiers.base import Notifier -log = logging.getLogger('tgtg') +log = logging.getLogger("tgtg") class Console(Notifier): """Notifier for the console output""" - def __init__(self, config: Config): - self.enabled = config.console.get("enabled", False) - self.body = config.console.get("body") - self.cron = config.console.get("cron") + def __init__(self, config: Config, reservations: Reservations, favorites: Favorites): + super().__init__(config, reservations, favorites) + self.enabled = config.console.enabled + self.body = config.console.body + self.cron = config.console.cron if self.enabled: try: @@ -22,9 +24,10 @@ def __init__(self, config: Config): except MaskConfigurationError as exc: raise ConsoleConfigurationError(exc.message) from exc - def _send(self, item: Item) -> None: - message = item.unmask(self.body) - print(message) + def _send(self, item: Union[Item, Reservation]) -> None: + if isinstance(item, Item): + message = item.unmask(self.body) + print(message) def __repr__(self) -> str: return "Console stdout" diff --git a/tgtg_scanner/notifiers/ifttt.py b/tgtg_scanner/notifiers/ifttt.py index b87ee62d..b1bae356 100644 --- a/tgtg_scanner/notifiers/ifttt.py +++ b/tgtg_scanner/notifiers/ifttt.py @@ -1,11 +1,10 @@ import logging -from tgtg_scanner.models import Config, Item -from tgtg_scanner.models.errors import (IFTTTConfigurationError, - MaskConfigurationError) +from tgtg_scanner.errors import IFTTTConfigurationError, MaskConfigurationError +from tgtg_scanner.models import Config, Favorites, Item, Reservations from tgtg_scanner.notifiers.webhook import WebHook -log = logging.getLogger('tgtg') +log = logging.getLogger("tgtg") class IFTTT(WebHook): @@ -15,17 +14,17 @@ class IFTTT(WebHook): https://ifttt.com/maker_webhooks """ - def __init__(self, config: Config): - self.enabled = config.ifttt.get("enabled", False) - self.event = config.ifttt.get("event") - self.key = config.ifttt.get("key") - self.body = config.ifttt.get("body") - self.cron = config.ifttt.get("cron") - self.timeout = config.ifttt.get("timeout") + def __init__(self, config: Config, reservations: Reservations, favorites: Favorites): + super(WebHook, self).__init__(config, reservations, favorites) + self.enabled = config.ifttt.enabled + self.event = config.ifttt.event + self.key = config.ifttt.key + self.body = config.ifttt.body + self.cron = config.ifttt.cron + self.timeout = config.ifttt.timeout self.headers = {} self.method = "POST" - self.url = (f"https://maker.ifttt.com/trigger/" - f"{self.event}/with/key/{self.key}") + self.url = f"https://maker.ifttt.com/trigger/{self.event}/with/key/{self.key}" self.type = "application/json" self.auth = None diff --git a/tgtg_scanner/notifiers/notifiers.py b/tgtg_scanner/notifiers/notifiers.py index bf6cde6c..7a4c2339 100644 --- a/tgtg_scanner/notifiers/notifiers.py +++ b/tgtg_scanner/notifiers/notifiers.py @@ -1,5 +1,5 @@ import logging -from typing import List +from typing import Type, Union from tgtg_scanner.models import Config, Cron, Favorites, Item, Reservations from tgtg_scanner.models.reservations import Reservation @@ -16,32 +16,24 @@ log = logging.getLogger("tgtg") +NOTIFIERS: list[Type[Notifier]] = [Apprise, Console, PushSafer, SMTP, IFTTT, Ntfy, WebHook, Telegram, Script] + class Notifiers: - def __init__(self, config: Config, reservations: Reservations, - favorites: Favorites): - self._notifiers: List[Notifier] = [ - Apprise(config), - Console(config), - PushSafer(config), - SMTP(config), - IFTTT(config), - Ntfy(config), - WebHook(config), - Telegram(config, reservations, favorites), - Script(config), - ] + """Notifier Manager""" + + def __init__(self, config: Config, reservations: Reservations, favorites: Favorites): + self._notifiers: list[Notifier] = [NotifierCls(config, reservations, favorites) for NotifierCls in NOTIFIERS] log.info("Activated notifiers:") if self.notifier_count == 0: log.warning("No notifiers configured!") for notifier in self._enabled_notifiers: log.info("- %s", notifier) if notifier.cron != Cron("* * * * *"): - log.info(" Schedule: %s", - notifier.cron.get_description(config.locale)) + log.info(" Schedule: %s", notifier.cron.get_description(config.locale)) @property - def _enabled_notifiers(self) -> List[Notifier]: + def _enabled_notifiers(self) -> list[Notifier]: return [notifier for notifier in self._notifiers if notifier.enabled] @property @@ -53,29 +45,22 @@ def notifier_count(self) -> int: """ return len(self._enabled_notifiers) - def send(self, item: Item) -> None: + def send(self, item: Union[Item, Reservation]) -> None: """Send notifications on all enabled notifiers. Args: - item (Item): Item information to send + item (Item, Reservation): Item information to send """ - for notifier in self._enabled_notifiers: - try: - notifier.send(item) - except Exception as exc: - log.error("Failed sending %s: %s", notifier, exc) - - def send_reservation(self, reservation: Reservation) -> None: - """Send notification for new reservation + for notifier in self._notifiers: + notifier.send(item) - Args: - reservation (Reservation): New reservation - """ - for notifier in self._enabled_notifiers: + def start(self) -> None: + """Start all notifiers""" + for notifier in self._notifiers: try: - notifier.send_reservation(reservation) + notifier.start() except Exception as exc: - log.error("Failed sending %s: %s", notifier, exc) + log.warning("Error starting %s - %s", notifier, exc) def stop(self) -> None: """Stop all notifiers""" diff --git a/tgtg_scanner/notifiers/ntfy.py b/tgtg_scanner/notifiers/ntfy.py index 8a998dfe..c764acf8 100644 --- a/tgtg_scanner/notifiers/ntfy.py +++ b/tgtg_scanner/notifiers/ntfy.py @@ -1,33 +1,35 @@ import logging +from typing import Union from requests.auth import HTTPBasicAuth -from tgtg_scanner.models import Config, Item -from tgtg_scanner.models.errors import (MaskConfigurationError, - NtfyConfigurationError) +from tgtg_scanner.errors import MaskConfigurationError, NtfyConfigurationError +from tgtg_scanner.models import Config, Favorites, Item, Reservations +from tgtg_scanner.models.reservations import Reservation from tgtg_scanner.notifiers.webhook import WebHook -log = logging.getLogger('tgtg') +log = logging.getLogger("tgtg") class Ntfy(WebHook): """Notifier for Ntfy""" - def __init__(self, config: Config): - self.enabled = config.ntfy.get("enabled", False) - self.server = config.ntfy.get("server", "https://ntfy.sh") - self.topic = config.ntfy.get("topic") - self.title = config.ntfy.get("title", "tgtg") - self.message = config.ntfy.get("message") - self.body = config.ntfy.get("body") - self.priority = config.ntfy.get("priority", "default") - self.tags = config.ntfy.get("tags", "tgtg") - self.click = config.ntfy.get("click") - self.username = config.ntfy.get("username") - self.password = config.ntfy.get("password") - self.timeout = config.ntfy.get("timeout", 60) - self.cron = config.ntfy.get("cron") - self.headers = None + def __init__(self, config: Config, reservations: Reservations, favorites: Favorites): + super(WebHook, self).__init__(config, reservations, favorites) + self.enabled = config.ntfy.enabled + self.server = config.ntfy.server + self.topic = config.ntfy.topic + self.title = config.ntfy.title + self.message = config.ntfy.message + self.body = config.ntfy.body + self.priority = config.ntfy.priority + self.tags = config.ntfy.tags + self.click = config.ntfy.click + self.username = config.ntfy.username + self.password = config.ntfy.password + self.timeout = config.ntfy.timeout + self.cron = config.ntfy.cron + self.headers = dict() self.auth = None self.method = "POST" self.type = None @@ -37,13 +39,11 @@ def __init__(self, config: Config): raise NtfyConfigurationError() self.url = f"{self.server}/{self.topic}" log.debug("Ntfy url: %s", self.url) - if (self.username and self.password) is not None: + if self.username is not None and self.password is not None: self.auth = HTTPBasicAuth(self.username, self.password) - log.debug("Using basic auth with user '%s' for Ntfy", - self.username) + log.debug("Using basic auth with user '%s' for Ntfy", self.username) elif (self.username or self.password) is not None: - log.warning("Username or Password missing for Ntfy " - "authentication, defaulting to no auth") + log.warning("Username or Password missing for Ntfy authentication, defaulting to no auth") try: Item.check_mask(self.title) Item.check_mask(self.message) @@ -52,20 +52,21 @@ def __init__(self, config: Config): except MaskConfigurationError as exc: raise NtfyConfigurationError(exc.message) from exc - def _send(self, item: Item) -> None: + def _send(self, item: Union[Item, Reservation]) -> None: """Sends item information via configured Ntfy endpoint""" - title = item.unmask(self.title).encode("utf-8") - message = item.unmask(self.message).encode("utf-8") - tags = item.unmask(self.tags).encode("utf-8") - click = item.unmask(self.click).encode("utf-8") - self.headers = { - "X-Title": title, - "X-Message": message, - "X-Priority": self.priority, - "X-Tags": tags, - "X-Click": click - } - super()._send(item) + if isinstance(item, Item): + title = item.unmask(self.title).encode("utf-8") + message = item.unmask(self.message).encode("utf-8") + tags = item.unmask(self.tags).encode("utf-8") + click = item.unmask(self.click).encode("utf-8") + self.headers = { + "X-Title": title, + "X-Message": message, + "X-Priority": self.priority, + "X-Tags": tags, + "X-Click": click, + } + super()._send(item) def __repr__(self) -> str: return f"Ntfy: {self.server}/{self.topic}" diff --git a/tgtg_scanner/notifiers/push_safer.py b/tgtg_scanner/notifiers/push_safer.py index e97fa692..3ed0f7ca 100644 --- a/tgtg_scanner/notifiers/push_safer.py +++ b/tgtg_scanner/notifiers/push_safer.py @@ -1,12 +1,14 @@ import logging +from typing import Union from pushsafer import Client -from tgtg_scanner.models import Config, Item -from tgtg_scanner.models.errors import PushSaferConfigurationError +from tgtg_scanner.errors import PushSaferConfigurationError +from tgtg_scanner.models import Config, Favorites, Item, Reservations +from tgtg_scanner.models.reservations import Reservation from tgtg_scanner.notifiers.base import Notifier -log = logging.getLogger('tgtg') +log = logging.getLogger("tgtg") class PushSafer(Notifier): @@ -16,23 +18,22 @@ class PushSafer(Notifier): https://www.pushsafer.com/ """ - def __init__(self, config: Config): - self.enabled = config.push_safer.get("enabled", False) - self.key = config.push_safer.get("key") - self.device_id = config.push_safer.get("deviceId") - self.cron = config.push_safer.get("cron") - if self.enabled and (not self.key or not self.device_id): - raise PushSaferConfigurationError() + def __init__(self, config: Config, reservations: Reservations, favorites: Favorites): + super().__init__(config, reservations, favorites) + self.enabled = config.pushsafer.enabled + self.key = config.pushsafer.key + self.device_id = config.pushsafer.device_id + self.cron = config.pushsafer.cron if self.enabled: + if self.key is None or self.device_id is None: + raise PushSaferConfigurationError() self.client = Client(self.key) - def _send(self, item: Item) -> None: - """ - Sends item information to the Pushsafer endpoint. - """ - message = f"New Amount: {item.items_available}" - self.client.send_message(message, item.display_name, - self.device_id) + def _send(self, item: Union[Item, Reservation]) -> None: + """Sends item information to the Pushsafer endpoint""" + if isinstance(item, Item): + message = f"New Amount: {item.items_available}" + self.client.send_message(message, item.display_name, self.device_id) def __repr__(self) -> str: return f"PushSafer: {self.key}" diff --git a/tgtg_scanner/notifiers/script.py b/tgtg_scanner/notifiers/script.py index 2df2ae02..e0f528df 100644 --- a/tgtg_scanner/notifiers/script.py +++ b/tgtg_scanner/notifiers/script.py @@ -1,33 +1,39 @@ import logging import subprocess +from typing import Union -from tgtg_scanner.models import Config, Item -from tgtg_scanner.models.errors import (MaskConfigurationError, - ScriptConfigurationError) +from tgtg_scanner.errors import MaskConfigurationError, ScriptConfigurationError +from tgtg_scanner.models import Config, Favorites, Item, Reservations +from tgtg_scanner.models.reservations import Reservation from tgtg_scanner.notifiers import Notifier -log = logging.getLogger('tgtg') +log = logging.getLogger("tgtg") class Script(Notifier): """Notifier for the script output""" - def __init__(self, config: Config): - self.enabled = config.script.get("enabled", False) - self.command = config.script.get("command") - self.cron = config.script.get("cron") + def __init__(self, config: Config, reservations: Reservations, favorites: Favorites): + super().__init__(config, reservations, favorites) + self.enabled = config.script.enabled + self.command = config.script.command + self.cron = config.script.cron - if self.enabled and (not self.command): - raise ScriptConfigurationError() if self.enabled: - try: - Item.check_mask(self.command) - except MaskConfigurationError as exc: - raise ScriptConfigurationError(exc.message) from exc - - def _send(self, item: Item) -> None: - args = [item.unmask(arg) for arg in self.command.split()] - subprocess.Popen(args) + if self.command is None: + raise ScriptConfigurationError() + else: + try: + Item.check_mask(self.command) + except MaskConfigurationError as exc: + raise ScriptConfigurationError(exc.message) from exc + + def _send(self, item: Union[Item, Reservation]) -> None: + if self.command is None: + raise ScriptConfigurationError() + if isinstance(item, Item): + args = [item.unmask(arg) for arg in self.command.split()] + subprocess.Popen(args) def __repr__(self) -> str: return f"Shell script: {self.command}" diff --git a/tgtg_scanner/notifiers/smtp.py b/tgtg_scanner/notifiers/smtp.py index 7dc9f7c2..1da75e2e 100644 --- a/tgtg_scanner/notifiers/smtp.py +++ b/tgtg_scanner/notifiers/smtp.py @@ -3,40 +3,38 @@ from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from smtplib import SMTPException, SMTPServerDisconnected +from typing import Union -from tgtg_scanner.models import Config, Item -from tgtg_scanner.models.errors import (MaskConfigurationError, - SMTPConfigurationError) +from tgtg_scanner.errors import MaskConfigurationError, SMTPConfigurationError +from tgtg_scanner.models import Config, Favorites, Item, Reservations +from tgtg_scanner.models.reservations import Reservation from tgtg_scanner.notifiers.base import Notifier -log = logging.getLogger('tgtg') +log = logging.getLogger("tgtg") class SMTP(Notifier): - """ - Notifier for SMTP. - """ + """Notifier for SMTP""" - def __init__(self, config: Config): - self.server = None + def __init__(self, config: Config, reservations: Reservations, favorites: Favorites): + super().__init__(config, reservations, favorites) + self.server: Union[smtplib.SMTP, None] = None self.debug = config.debug - self.enabled = config.smtp.get("enabled", False) - self.host = config.smtp.get("host") - self.port = config.smtp.get("port", 25) - self.tls = config.smtp.get("tls", False) - self.ssl = config.smtp.get("ssl", False) - self.username = config.smtp.get("username") - self.password = config.smtp.get("password") - self.sender = config.smtp.get("sender") - self.recipient = config.smtp.get("recipient") - self.subject = config.smtp.get("subject") - self.body = config.smtp.get("body") - self.cron = config.smtp.get("cron") - if self.enabled and (not self.host or - not self.port or - not self.recipient): - raise SMTPConfigurationError() + self.enabled = config.smtp.enabled + self.host = config.smtp.host + self.port = config.smtp.port + self.use_tls = config.smtp.use_tls + self.use_ssl = config.smtp.use_ssl + self.username = config.smtp.username + self.password = config.smtp.password + self.sender = config.smtp.sender + self.recipients = config.smtp.recipients + self.subject = config.smtp.subject + self.body = config.smtp.body + self.cron = config.smtp.cron if self.enabled: + if self.host is None or self.port is None or self.recipients is None: + raise SMTPConfigurationError() try: Item.check_mask(self.subject) Item.check_mask(self.body) @@ -57,47 +55,53 @@ def __del__(self): def _connect(self) -> None: """Connect to SMTP Server""" - if self.ssl: + if self.host is None or self.port is None: + raise SMTPConfigurationError() + if self.use_ssl: self.server = smtplib.SMTP_SSL(self.host, self.port) else: self.server = smtplib.SMTP(self.host, self.port) self.server.set_debuglevel(self.debug) - if self.tls: + if self.use_tls: self.server.starttls() self.server.ehlo() - if self.username and self.password: + if self.username is not None and self.password is not None: self.server.login(self.username, self.password) def _stay_connected(self) -> None: """Refresh server connection if connection is lost""" - try: - status = self.server.noop()[0] - except SMTPServerDisconnected: - status = -1 + status = -1 + if self.server is not None: + try: + status = self.server.noop()[0] + except SMTPServerDisconnected: + pass if status != 250: self._connect() def _send_mail(self, subject: str, html: str) -> None: """Sends mail with html body""" - message = MIMEMultipart('alternative') - message['From'] = self.sender - message['To'] = ", ".join(self.recipient) - message['Subject'] = subject - message.attach(MIMEText(html, 'html')) + if self.server is None: + self._connect() + if self.sender is None or self.recipients is None or self.server is None: + raise SMTPConfigurationError() + message = MIMEMultipart("alternative") + message["From"] = self.sender + message["To"] = ", ".join(self.recipients) + message["Subject"] = subject + message.attach(MIMEText(html, "html", "utf-8")) body = message.as_string() self._stay_connected() try: - self.server.sendmail(self.sender, self.recipient, body) + self.server.sendmail(self.sender, self.recipients, body) except SMTPException: self._connect() - self.server.sendmail(self.sender, self.recipient, body) + self.server.sendmail(self.sender, self.recipients, body) - def _send(self, item: Item) -> None: + def _send(self, item: Union[Item, Reservation]) -> None: """Sends item information via Mail.""" - self._send_mail( - item.unmask(self.subject), - item.unmask(self.body) - ) + if isinstance(item, Item): + self._send_mail(item.unmask(self.subject), item.unmask(self.body)) def __repr__(self) -> str: - return f"SMTP: {self.recipient}" + return f"SMTP: {self.recipients}" diff --git a/tgtg_scanner/notifiers/telegram.py b/tgtg_scanner/notifiers/telegram.py index 181c9d0a..4fba4a00 100644 --- a/tgtg_scanner/notifiers/telegram.py +++ b/tgtg_scanner/notifiers/telegram.py @@ -1,108 +1,177 @@ +import asyncio import datetime import logging import random +import warnings +from queue import Empty from time import sleep +from typing import Union -from telegram import (InlineKeyboardButton, InlineKeyboardMarkup, ParseMode, - Update) -from telegram.bot import BotCommand -from telegram.error import BadRequest, NetworkError, TelegramError, TimedOut -from telegram.ext import (CallbackContext, CallbackQueryHandler, - CommandHandler, Filters, MessageHandler, Updater) -from telegram.utils.helpers import escape_markdown +from telegram import BotCommand, InlineKeyboardButton, InlineKeyboardMarkup, Update +from telegram.constants import ParseMode +from telegram.error import ( + BadRequest, + InvalidToken, + NetworkError, + TelegramError, + TimedOut, +) +from telegram.ext import ( + Application, + ApplicationBuilder, + CallbackContext, + CallbackQueryHandler, + CommandHandler, + MessageHandler, + filters, +) +from telegram.helpers import escape_markdown +from telegram.warnings import PTBUserWarning +from tgtg_scanner.errors import MaskConfigurationError, TelegramConfigurationError from tgtg_scanner.models import Config, Favorites, Item, Reservations -from tgtg_scanner.models.errors import (MaskConfigurationError, - TelegramConfigurationError) -from tgtg_scanner.models.favorites import (AddFavoriteRequest, - RemoveFavoriteRequest) +from tgtg_scanner.models.favorites import AddFavoriteRequest, RemoveFavoriteRequest from tgtg_scanner.models.reservations import Order, Reservation from tgtg_scanner.notifiers.base import Notifier -log = logging.getLogger('tgtg') +log = logging.getLogger("tgtg") class Telegram(Notifier): - """ - Notifier for Telegram. - """ + """Notifier for Telegram""" + MAX_RETRIES = 10 - def __init__(self, config: Config, reservations: Reservations, - favorites: Favorites): - self.updater = None + def __init__(self, config: Config, reservations: Reservations, favorites: Favorites): + super().__init__(config, reservations, favorites) + self.application: Application = None self.config = config - self.enabled = config.telegram.get("enabled", False) - self.token = config.telegram.get("token") - self.body = config.telegram.get("body") - self.image = config.telegram.get("image") - self.chat_ids = config.telegram.get("chat_ids") - self.timeout = config.telegram.get("timeout", 60) - self.disable_commands = config.telegram.get( - "disable_commands", False) - self.cron = config.telegram.get("cron") - self.mute = None + self.enabled = config.telegram.enabled + self.token = config.telegram.token + self.body = config.telegram.body + self.image = config.telegram.image + self.chat_ids = config.telegram.chat_ids + self.timeout = config.telegram.timeout + self.disable_commands = config.telegram.disable_commands + self.cron = config.telegram.cron + self.mute: Union[datetime.datetime, None] = None self.retries = 0 - self.reservations = reservations - self.favorites = favorites - if self.enabled and (not self.token or not self.body): - raise TelegramConfigurationError() if self.enabled: - if self.image not in [None, "", "${{item_logo_bytes}}", - "${{item_cover_bytes}}"]: + if not self.token or not self.body: raise TelegramConfigurationError() + if self.image not in [ + None, + "", + "${{item_logo_bytes}}", + "${{item_cover_bytes}}", + ]: + raise TelegramConfigurationError() + # Suppress Telegram Warnings + warnings.filterwarnings("ignore", category=PTBUserWarning, module="telegram") try: Item.check_mask(self.body) - self.updater = Updater(token=self.token, - arbitrary_callback_data=True) - self.updater.bot.get_me(timeout=self.timeout) except MaskConfigurationError as err: raise TelegramConfigurationError(err.message) from err + try: + # Setting event loop explicitly for python 3.9 compatibility + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + application = ApplicationBuilder().token(self.token).arbitrary_callback_data(True).build() + application.add_error_handler(self._error) + asyncio.run(application.bot.get_me()) + except InvalidToken as err: + raise TelegramConfigurationError("Invalid Telegram Bot Token") from err except TelegramError as err: - raise TelegramConfigurationError() from err - if not self.chat_ids: - self._get_chat_id() - handlers = [ - CommandHandler("mute", self._mute), - CommandHandler("unmute", self._unmute), - CommandHandler("reserve", self._reserve_item_menu), - CommandHandler("reservations", self._cancel_reservations_menu), - CommandHandler("orders", self._cancel_orders_menu), - CommandHandler("cancelall", self._cancel_all_orders), - CommandHandler("listfavorites", self._list_favorites), - CommandHandler("listfavoriteids", self._list_favorite_ids), - CommandHandler("addfavorites", self._add_favorites), - CommandHandler("removefavorites", self._remove_favorites), - MessageHandler(Filters.regex( - r'^https:\/\/share\.toogoodtogo\.com\/item\/(\d+)\/?' - ), self._url_handler), - CallbackQueryHandler(self._callback_query_handler) + raise TelegramConfigurationError(err.message) from err + + @property + def _handlers(self): + return [ + CommandHandler("mute", self._mute), + CommandHandler("unmute", self._unmute), + CommandHandler("reserve", self._reserve_item_menu), + CommandHandler("reservations", self._cancel_reservations_menu), + CommandHandler("orders", self._cancel_orders_menu), + CommandHandler("cancelall", self._cancel_all_orders), + CommandHandler("listfavorites", self._list_favorites), + CommandHandler("listfavoriteids", self._list_favorite_ids), + CommandHandler("addfavorites", self._add_favorites), + CommandHandler("removefavorites", self._remove_favorites), + MessageHandler( + filters.Regex(r"^https:\/\/share\.toogoodtogo\.com\/item\/(\d+)\/?"), + self._url_handler, + ), + CallbackQueryHandler(self._callback_query_handler), + ] + + async def _start_polling(self): + log.debug("Telegram: Starting polling") + for handler in self._handlers: + self.application.add_handler(handler) + await self.application.initialize() + await self.application.updater.start_polling(allowed_updates=Update.ALL_TYPES, timeout=self.timeout, poll_interval=0.1) + await self.application.bot.set_my_commands( + [ + BotCommand("mute", "Deactivate Telegram Notifications for 1 or x days"), + BotCommand("unmute", "Reactivate Telegram Notifications"), + BotCommand("reserve", "Reserve the next available Magic Bag"), + BotCommand("reservations", "List and cancel Reservations"), + BotCommand("orders", "List and cancel active Orders"), + BotCommand("cancelall", "Cancels all active orders"), + BotCommand("listfavorites", "List all favorites"), + BotCommand("listfavoriteids", "List all item ids from favorites"), + BotCommand("addfavorites", "Add item ids to favorites"), + BotCommand("removefavorites", "Remove Item ids from favorites"), ] - for handler in handlers: - self.updater.dispatcher.add_handler(handler) - self.updater.dispatcher.add_error_handler(self._error) - self.updater.bot.set_my_commands([ - BotCommand('mute', - 'Deactivate Telegram Notifications for ' - '1 or x days'), - BotCommand('unmute', 'Reactivate Telegram Notifications'), - BotCommand('reserve', 'Reserve the next available Magic Bag'), - BotCommand('reservations', 'List and cancel Reservations'), - BotCommand('orders', 'List and cancel active Orders'), - BotCommand('cancelall', 'Cancels all active orders'), - BotCommand('listfavorites', 'List all favorites'), - BotCommand('listfavoriteids', - 'List all item ids from favorites'), - BotCommand('addfavorites', 'Add item ids to favorites'), - BotCommand('removefavorites', 'Remove Item ids from favorites') - ]) + ) + await self.application.start() + + async def _stop_polling(self): + log.debug("Telegram: stopping polling") + await self.application.updater.stop() + await self.application.stop() + await self.application.shutdown() + + def start(self) -> None: + if not self.chat_ids: + asyncio.run(self._get_chat_ids()) + super().start() + + def _run(self) -> None: + async def _listen_for_items() -> None: + # Setting event loop explicitly for python 3.9 compatibility + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + self.application = ApplicationBuilder().token(self.token).arbitrary_callback_data(True).build() + self.application.add_error_handler(self._error) + if not self.disable_commands: + try: + await self._start_polling() + except Exception as exc: + log.error("Telegram failed starting polling: %s", exc) + return + while True: + try: + item = self.queue.get(block=False) + if item is None: + break + log.debug("Sending %s Notification", self.name) + await self._send(item) + except Empty: + pass + except Exception as exc: + log.error("Failed sending %s: %s", self.name, exc) + finally: + await asyncio.sleep(0.1) if not self.disable_commands: - self.updater.start_polling() + try: + await self._stop_polling() + except Exception as exc: + log.warning("Telegram failed stopping polling: %s", exc) + + asyncio.run(_listen_for_items()) def _unmask(self, text: str, item: Item) -> str: - if text in ["${{item_logo_bytes}}", "${{item_cover_bytes}}"]: - matches = item._get_variables(text) - return getattr(item, matches[0].group(1)) for match in item._get_variables(text): if hasattr(item, match.group(1)): val = str(getattr(item, match.group(1))) @@ -110,331 +179,281 @@ def _unmask(self, text: str, item: Item) -> str: text = text.replace(match.group(0), val) return text - def _send(self, item: Item) -> None: + def _unmask_image(self, text: str, item: Item) -> Union[bytes, None]: + if text in ["${{item_logo_bytes}}", "${{item_cover_bytes}}"]: + matches = item._get_variables(text) + return bytes(getattr(item, matches[0].group(1))) + return None + + async def _send(self, item: Union[Item, Reservation]) -> None: # type: ignore[override] """Send item information as Telegram message""" if self.mute and self.mute > datetime.datetime.now(): return if self.mute: log.info("Reactivated Telegram Notifications") self.mute = None - message = self._unmask(self.body, item) image = None - if self.image: - image = self._unmask(self.image, item) - self._send_message(message, image) - - def _send_reservation(self, reservation: Reservation) -> None: - message = escape_markdown( - f"{reservation.display_name} is reserved for 5 minutes", - version=2) - self._send_message(message) + if isinstance(item, Item): + message = self._unmask(self.body, item) + if self.image: + image = self._unmask_image(self.image, item) + elif isinstance(item, Reservation): + message = escape_markdown(f"{item.display_name} is reserved for 5 minutes", version=2) + await self._send_message(message, image) - def _send_message(self, message: str, image: bytes = None) -> None: + async def _send_message(self, message: str, image: Union[bytes, None] = None) -> None: log.debug("%s message: %s", self.name, message) fmt = ParseMode.MARKDOWN_V2 for chat_id in self.chat_ids: try: if image: - self.updater.bot.send_photo( - chat_id=chat_id, - photo=image, - caption=message, - parse_mode=fmt, - timeout=self.timeout) + await self.application.bot.send_photo(chat_id=chat_id, photo=image, caption=message, parse_mode=fmt) else: - self.updater.bot.send_message( + await self.application.bot.send_message( chat_id=chat_id, text=message, parse_mode=fmt, - timeout=self.timeout, - disable_web_page_preview=True) + disable_web_page_preview=True, + ) self.retries = 0 except BadRequest as err: err_message = err.message if err_message.startswith("Can't parse entities:"): - err_message += ". For details see https://github.com/" - err_message += "Der-Henning/tgtg/wiki/Configuration" - err_message += "#note-on-markdown-v2" - log.error('Telegram Error: %s', err_message) + err_message += ". For details see https://github.com/Der-Henning/tgtg/wiki/Configuration#note-on-markdown-v2" + log.error("Telegram Error: %s", err_message) except (NetworkError, TimedOut) as err: - log.warning('Telegram Error: %s', err) + log.warning("Telegram Error: %s", err) self.retries += 1 if self.retries > Telegram.MAX_RETRIES: raise err - self.updater.stop() - self.updater.start_polling() - self._send_message(message) + await self._send_message(message) except TelegramError as err: - log.error('Telegram Error: %s', err) + log.error("Telegram Error: %s", err) - def _mute(self, update: Update, context: CallbackContext) -> None: + async def _mute(self, update: Update, context: CallbackContext) -> None: """Deactivates Telegram Notifications for x days""" - days = int(context.args[0]) if context.args and \ - context.args[0].isnumeric() else 1 + days = int(context.args[0]) if context.args and context.args[0].isnumeric() else 1 self.mute = datetime.datetime.now() + datetime.timedelta(days=days) - log.info('Deactivated Telegram Notifications for %s days', days) - log.info('Reactivation at %s', self.mute) - update.message.reply_text( - f"Deactivated Telegram Notifications for {days} days.\n" - f"Reactivating at {self.mute} or use /unmute.") + log.info("Deactivated Telegram Notifications for %s days", days) + log.info("Reactivation at %s", self.mute) + await update.message.reply_text( + f"Deactivated Telegram Notifications for {days} days.\nReactivating at {self.mute} or use /unmute." + ) - def _unmute(self, update: Update, context: CallbackContext) -> None: + async def _unmute(self, update: Update, _) -> None: """Reactivate Telegram Notifications""" - del context self.mute = None log.info("Reactivated Telegram Notifications") - update.message.reply_text("Reactivated Telegram Notifications") + await update.message.reply_text("Reactivated Telegram Notifications") - def _reserve_item_menu(self, - update: Update, - context: CallbackContext) -> None: - del context + async def _reserve_item_menu(self, update: Update, _) -> None: favorites = self.favorites.get_favorites() - buttons = [[ - InlineKeyboardButton( - f"{item.display_name}: {item.items_available}", - callback_data=item) - ] for item in favorites] + buttons = [ + [InlineKeyboardButton(f"{item.display_name}: {item.items_available}", callback_data=item)] for item in favorites + ] reply_markup = InlineKeyboardMarkup(buttons) - update.message.reply_text( - "Select a Bag to reserve", - reply_markup=reply_markup) - - def _cancel_reservations_menu(self, - update: Update, - context: CallbackContext) -> None: - del context - buttons = [[ - InlineKeyboardButton( - reservation.display_name, - callback_data=reservation) - ] for reservation in self.reservations.reservation_query] + await update.message.reply_text("Select a Bag to reserve", reply_markup=reply_markup) + + async def _cancel_reservations_menu(self, update: Update, _) -> None: + buttons = [ + [InlineKeyboardButton(reservation.display_name, callback_data=reservation)] + for reservation in self.reservations.reservation_query + ] if len(buttons) == 0: - update.message.reply_text("No active Reservations") + await update.message.reply_text("No active Reservations") return reply_markup = InlineKeyboardMarkup(buttons) - update.message.reply_text( - "Active Reservations. Select to cancel.", - reply_markup=reply_markup) - - def _cancel_orders_menu(self, - update: Update, - context: CallbackContext) -> None: - del context + await update.message.reply_text("Active Reservations. Select to cancel.", reply_markup=reply_markup) + + async def _cancel_orders_menu(self, update: Update, _) -> None: self.reservations.update_active_orders() - buttons = [[ - InlineKeyboardButton( - order.display_name, - callback_data=order) - ] for order in self.reservations.active_orders] + buttons = [ + [InlineKeyboardButton(order.display_name, callback_data=order)] for order in self.reservations.active_orders.values() + ] if len(buttons) == 0: - update.message.reply_text("No active Orders") + await update.message.reply_text("No active Orders") return reply_markup = InlineKeyboardMarkup(buttons) - update.message.reply_text( - "Active Orders. Select to cancel.", - reply_markup=reply_markup) - - def _cancel_all_orders(self, - update: Update, - context: CallbackContext) -> None: - del context + await update.message.reply_text("Active Orders. Select to cancel.", reply_markup=reply_markup) + + async def _cancel_all_orders(self, update: Update, _) -> None: self.reservations.cancel_all_orders() - update.message.reply_text("Cancelled all active Orders") + await update.message.reply_text("Cancelled all active Orders") log.debug("Cancelled all active Orders") - def _list_favorites(self, - update: Update, - context: CallbackContext) -> None: - del context + async def _list_favorites(self, update: Update, _) -> None: favorites = self.favorites.get_favorites() if not favorites: - update.message.reply_text( - "You currently don't have any favorites.") + await update.message.reply_text("You currently don't have any favorites.") else: - update.message.reply_text( - "\n".join([f"• {item.item_id} - {item.display_name}" - for item in favorites])) - - def _list_favorite_ids(self, - update: Update, - context: CallbackContext) -> None: - del context + await update.message.reply_text("\n".join([f"• {item.item_id} - {item.display_name}" for item in favorites])) + + async def _list_favorite_ids(self, update: Update, _) -> None: favorites = self.favorites.get_favorites() if not favorites: - update.message.reply_text( - "You currently don't have any favorites.") + await update.message.reply_text("You currently don't have any favorites.") else: - update.message.reply_text( - " ".join([item.item_id for item in favorites])) + await update.message.reply_text(" ".join([item.item_id for item in favorites])) - def _add_favorites(self, - update: Update, - context: CallbackContext) -> None: + async def _add_favorites(self, update: Update, context: CallbackContext) -> None: if not context.args: - update.message.reply_text( - "Please supply item ids in one of the following ways: " + - "'/addfavorites 12345 23456 34567' or " + - "'/addfavorites 12345,23456,34567'") + await update.message.reply_text( + "Please supply item ids in one of the following ways: " + "'/addfavorites 12345 23456 34567' or " + "'/addfavorites 12345,23456,34567'" + ) return - item_ids = list(filter(bool, map(str.strip, - [split_args for arg in context.args - for split_args in arg.split(",")] - ))) + item_ids = list( + filter( + bool, + map( + str.strip, + [split_args for arg in context.args for split_args in arg.split(",")], + ), + ) + ) self.favorites.add_favorites(item_ids) - update.message.reply_text( - f"Added the following item ids to favorites: {' '.join(item_ids)}") + await update.message.reply_text(f"Added the following item ids to favorites: {' '.join(item_ids)}") log.debug('Added the following item ids to favorites: "%s"', item_ids) - def _remove_favorites(self, - update: Update, - context: CallbackContext) -> None: + async def _remove_favorites(self, update: Update, context: CallbackContext) -> None: if not context.args: - update.message.reply_text( - "Please supply item ids in one of the following ways: " + - "'/removefavorites 12345 23456 34567' or " + - "'/removefavorites 12345,23456,34567'") + await update.message.reply_text( + "Please supply item ids in one of the following ways: " + "'/removefavorites 12345 23456 34567' or " + "'/removefavorites 12345,23456,34567'" + ) return - item_ids = list(filter(bool, map(str.strip, - [split_args for arg in context.args - for split_args in arg.split(",")] - ))) + item_ids = list( + filter( + bool, + map( + str.strip, + [split_args for arg in context.args for split_args in arg.split(",")], + ), + ) + ) self.favorites.remove_favorite(item_ids) - update.message.reply_text( - "Removed the following item ids from favorites: " - + f"{' '.join(item_ids)}") - log.debug('Removed the following item ids from favorites: ' - + '"%s"', item_ids) - - def _url_handler(self, - update: Update, - context: CallbackContext) -> None: + await update.message.reply_text(f"Removed the following item ids from favorites: {' '.join(item_ids)}") + log.debug("Removed the following item ids from favorites: '%s'", item_ids) + + async def _url_handler(self, update: Update, context: CallbackContext) -> None: item_id = context.matches[0].group(1) item_favorite = self.favorites.is_item_favorite(item_id) item = self.favorites.get_item_by_id(item_id) if item.item_id is None: - update.message.reply_text("There is no Item with this link") + await update.message.reply_text("There is no Item with this link") return if item_favorite: - update.message.reply_text( - f"{item.display_name} is in your favorites. " + - "Do you want to remove it?", - reply_markup=(InlineKeyboardMarkup([[ - InlineKeyboardButton("Yes", - callback_data=RemoveFavoriteRequest( - item_id, - item.display_name, - True - )), - InlineKeyboardButton("No", - callback_data=RemoveFavoriteRequest( - item_id, - item.display_name, - False - )) - ]]))) + await update.message.reply_text( + f"{item.display_name} is in your favorites. Do you want to remove it?", + reply_markup=( + InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + "Yes", + callback_data=RemoveFavoriteRequest(item_id, item.display_name, True), + ), + InlineKeyboardButton( + "No", + callback_data=RemoveFavoriteRequest(item_id, item.display_name, False), + ), + ] + ] + ) + ), + ) else: - update.message.reply_text( - f"{item.display_name} is not in your favorites. " + - "Do you want to add it?", - reply_markup=(InlineKeyboardMarkup([[ - InlineKeyboardButton("Yes", - callback_data=AddFavoriteRequest( - item_id, - item.display_name, - True - )), - InlineKeyboardButton("No", - callback_data=AddFavoriteRequest( - item_id, - item.display_name, - False - )) - ]]))) - - def _callback_query_handler(self, - update: Update, - context: CallbackContext) -> None: - del context + await update.message.reply_text( + f"{item.display_name} is not in your favorites. Do you want to add it?", + reply_markup=( + InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + "Yes", + callback_data=AddFavoriteRequest(item_id, item.display_name, True), + ), + InlineKeyboardButton( + "No", + callback_data=AddFavoriteRequest(item_id, item.display_name, False), + ), + ] + ] + ) + ), + ) + + async def _callback_query_handler(self, update: Update, _) -> None: data = update.callback_query.data if isinstance(data, Item): - self.reservations.reserve( - data.item_id, - data.display_name) - update.callback_query.answer( - f"Added {data.display_name} to reservation queue") + self.reservations.reserve(data.item_id, data.display_name) + await update.callback_query.answer(f"Added {data.display_name} to reservation queue") log.debug('Added "%s" to reservation queue', data.display_name) if isinstance(data, Reservation): self.reservations.reservation_query.remove(data) - update.callback_query.answer( - f"Removed {data.display_name} form reservation queue") + await update.callback_query.answer(f"Removed {data.display_name} form reservation queue") log.debug('Removed "%s" from reservation queue', data.display_name) if isinstance(data, Order): - self.reservations.cancel_order(data) - update.callback_query.answer( - f"Canceled Order for {data.display_name}") + self.reservations.cancel_order(data.id) + await update.callback_query.answer(f"Canceled Order for {data.display_name}") log.debug('Canceled order for "%s"', data.display_name) if isinstance(data, AddFavoriteRequest): if data.proceed: self.favorites.add_favorites([data.item_id]) - update.callback_query.edit_message_text( - f"Added {data.item_display_name} to favorites") + await update.callback_query.edit_message_text(f"Added {data.item_display_name} to favorites") log.debug('Added "%s" to favorites', data.item_display_name) - log.debug('Removed "%s" from favorites', - data.item_display_name) + log.debug('Removed "%s" from favorites', data.item_display_name) else: - update.callback_query.delete_message() + await update.callback_query.delete_message() if isinstance(data, RemoveFavoriteRequest): if data.proceed: self.favorites.remove_favorite([data.item_id]) - update.callback_query.edit_message_text( - f"Removed {data.item_display_name} from favorites") - log.debug('Removed "%s" from favorites', - data.item_display_name) + await update.callback_query.edit_message_text(f"Removed {data.item_display_name} from favorites") + log.debug('Removed "%s" from favorites', data.item_display_name) else: - update.callback_query.delete_message() + await update.callback_query.delete_message() - def _error(self, update: Update, context: CallbackContext) -> None: + async def _error(self, update: Update, context: CallbackContext) -> None: """Log Errors caused by Updates.""" - log.debug('Update "%s" caused error "%s"', update, context.error) + log.warning('Update "%s" caused error "%s"', update, context.error) - def _get_chat_id(self) -> None: + async def _get_chat_ids(self) -> None: """Initializes an interaction with the user to obtain the telegram chat id. \n On using the config.ini configuration the chat id will be stored in the config.ini. """ - log.warning("You enabled the Telegram notifications " - "without providing a chat id!") + log.warning("You enabled the Telegram notifications without providing a chat id!") code = random.randint(1111, 9999) log.warning("Send %s to the bot in your desired chat.", code) log.warning("Waiting for code ...") + application = ApplicationBuilder().token(self.token).arbitrary_callback_data(True).build() + application.add_error_handler(self._error) while not self.chat_ids: - updates = self.updater.bot.get_updates(timeout=self.timeout) + updates = await application.bot.get_updates(timeout=self.timeout) for update in reversed(updates): if update.message and update.message.text: - if update.message.text.isdecimal() and \ - int(update.message.text) == code: + if update.message.text.isdecimal() and int(update.message.text) == code: log.warning( "Received code from %s %s on chat id %s", update.message.from_user.first_name, update.message.from_user.last_name, - update.message.chat_id) + update.message.chat_id, + ) self.chat_ids = [str(update.message.chat_id)] sleep(1) - if self.config.set("TELEGRAM", "chat_ids", ','.join(self.chat_ids)): + if self.config.set("TELEGRAM", "ChatIDs", ",".join(self.chat_ids)): log.warning("Saved chat id in your config file") else: log.warning( "For persistence please set TELEGRAM_CHAT_IDS=%s", - ','.join(self.chat_ids)) - - def stop(self) -> None: - if self.updater is not None: - self.updater.stop() + ",".join(self.chat_ids), + ) def __repr__(self) -> str: return f"Telegram: {self.chat_ids}" diff --git a/tgtg_scanner/notifiers/webhook.py b/tgtg_scanner/notifiers/webhook.py index 90f94ff2..f28abc77 100644 --- a/tgtg_scanner/notifiers/webhook.py +++ b/tgtg_scanner/notifiers/webhook.py @@ -1,71 +1,84 @@ import json import logging +from typing import Union import requests from requests.auth import HTTPBasicAuth -from tgtg_scanner.models import Config, Item -from tgtg_scanner.models.errors import (MaskConfigurationError, - WebHookConfigurationError) +from tgtg_scanner.errors import MaskConfigurationError, WebHookConfigurationError +from tgtg_scanner.models import Config, Favorites, Item, Reservations +from tgtg_scanner.models.reservations import Reservation from tgtg_scanner.notifiers.base import Notifier -log = logging.getLogger('tgtg') +log = logging.getLogger("tgtg") class WebHook(Notifier): """Notifier for custom Webhooks""" - def __init__(self, config: Config): - self.enabled = config.webhook.get("enabled", False) - self.method = config.webhook.get("method") - self.url = config.webhook.get("url") - self.body = config.webhook.get("body") - self.type = config.webhook.get("type") - self.headers = config.webhook.get("headers", {}) + def __init__(self, config: Config, reservations: Reservations, favorites: Favorites): + super().__init__(config, reservations, favorites) + self.enabled: bool = config.webhook.enabled + self.method: str = config.webhook.method + self.url: Union[str, None] = config.webhook.url + self.body: Union[str, None] = config.webhook.body + self.type: Union[str, None] = config.webhook.type + self.headers: dict[str, Union[str, bytes]] = config.webhook.headers self.auth = None - self.username = config.webhook.get("username") - self.password = config.webhook.get("password") - self.timeout = config.webhook.get("timeout", 60) - self.cron = config.webhook.get("cron") + self.username: Union[str, None] = config.webhook.username + self.password: Union[str, None] = config.webhook.password + self.timeout: int = config.webhook.timeout + self.cron = config.webhook.cron if self.enabled: - if not self.method or not self.url: + if self.method is None or self.url is None: raise WebHookConfigurationError() - if (self.username and self.password) is not None: + if self.username is not None and self.password is not None: self.auth = HTTPBasicAuth(self.username, self.password) - log.debug("Using basic auth with user '%s' " - "for webhook", self.username) + log.debug("Using basic auth with user '%s' for webhook", self.username) try: Item.check_mask(self.body) Item.check_mask(self.url) except MaskConfigurationError as exc: raise WebHookConfigurationError(exc.message) from exc - def _send(self, item: Item) -> None: + def _send(self, item: Union[Item, Reservation]) -> None: """Sends item information via configured Webhook endpoint""" - url = item.unmask(self.url) - log.debug("%s url: %s", self.name, url) - body = None - headers = self.headers - if self.type: - headers["Content-Type"] = self.type - if self.body: - body = item.unmask(self.body) - if isinstance(body, bytes): - pass - elif self.type and 'json' in self.type: - body = json.dumps(json.loads(body.replace('\n', '\\n'))) - log.debug("%s body: %s", self.name, body) - else: - body = body.encode('utf-8') + if isinstance(item, Item): + if self.url is None: + raise WebHookConfigurationError() + url = item.unmask(self.url) + log.debug("%s url: %s", self.name, url) + body: Union[bytes, None] = None + headers = self.headers or dict() + if self.type: + headers["Content-Type"] = self.type + if self.body: + if self.type is not None and "json" in self.type: + body = json.dumps(json.loads(item.unmask(self.body).replace("\n", "\\n"))).encode("utf-8") + else: + body = item.unmask(self.body).encode("utf-8") log.debug("%s body: %s", self.name, body) - log.debug("%s headers: %s", self.name, headers) - res = requests.request(method=self.method, url=url, - timeout=self.timeout, data=body, - headers=headers, auth=self.auth) - if not res.ok: - log.error("%s Request failed with status code %s", - self.name, res.status_code) - log.debug("%s Response content: %s", self.name, res.text) + # body = item.unmask(self.body) + # if isinstance(body, bytes): + # pass + # elif self.type and "json" in self.type: + # body = json.dumps(json.loads(body.replace("\n", "\\n"))) + # log.debug("%s body: %s", self.name, body) + # else: + # body = body.encode("utf-8") + # log.debug("%s body: %s", self.name, body) + log.debug("%s headers: %s", self.name, headers) + res = requests.request( + method=self.method, + url=url, + timeout=self.timeout, + data=body, + headers=headers, + auth=self.auth, + ) + if not res.ok: + log.error("%s Request failed with status code %s", self.name, res.status_code) + log.debug("%s Response content: %s", self.name, res.text) def __repr__(self) -> str: return f"WebHook: {self.url}" diff --git a/tgtg_scanner/scanner.py b/tgtg_scanner/scanner.py index 7f345544..02d01e22 100644 --- a/tgtg_scanner/scanner.py +++ b/tgtg_scanner/scanner.py @@ -2,13 +2,20 @@ import sys from random import random from time import sleep -from typing import Dict, List, NoReturn +from typing import Dict, List, NoReturn, Union from progress.spinner import Spinner -from tgtg_scanner.models import (Config, Cron, Favorites, Item, Location, - Metrics, Reservations) -from tgtg_scanner.models.errors import TgtgAPIError +from tgtg_scanner.errors import TgtgAPIError +from tgtg_scanner.models import ( + Config, + Cron, + Favorites, + Item, + Location, + Metrics, + Reservations, +) from tgtg_scanner.notifiers import Notifiers from tgtg_scanner.tgtg import TgtgClient @@ -16,7 +23,7 @@ class Activity: - """ Activity class that creates a spinner if active is True """ + """Activity class that creates a spinner if active is True""" def __init__(self, active: bool): self.active = active @@ -25,12 +32,12 @@ def __init__(self, active: bool): self.spinner = Spinner("Scanning... ") def next(self) -> None: - """ Next function that updates the spinner """ + """Next function that updates the spinner""" if self.spinner: self.spinner.next() def flush(self) -> None: - """ Flush function that flushes the spinner """ + """Flush function that flushes the spinner""" if self.spinner: sys.stdout.write("\x1b[80D\x1b[K") sys.stdout.flush() @@ -45,19 +52,19 @@ def __init__(self, config: Config): self.item_ids = set(self.config.item_ids) self.cron = self.config.schedule_cron self.state: Dict[str, Item] = {} - self.notifiers = None - self.location = None + self.notifiers: Union[Notifiers, None] = None + self.location: Union[Location, None] = None self.tgtg_client = TgtgClient( - email=self.config.tgtg.get("username"), - timeout=self.config.tgtg.get("timeout"), - access_token_lifetime=self.config.tgtg.get( - "access_token_lifetime"), - max_polling_tries=self.config.tgtg.get("max_polling_tries"), - polling_wait_time=self.config.tgtg.get("polling_wait_time"), - access_token=self.config.tgtg.get("access_token"), - refresh_token=self.config.tgtg.get("refresh_token"), - user_id=self.config.tgtg.get("user_id"), - datadome_cookie=self.config.tgtg.get("datadome") + email=self.config.tgtg.username, + timeout=self.config.tgtg.timeout, + access_token_lifetime=self.config.tgtg.access_token_lifetime, + max_polling_tries=self.config.tgtg.max_polling_tries, + polling_wait_time=self.config.tgtg.polling_wait_time, + access_token=self.config.tgtg.access_token, + refresh_token=self.config.tgtg.refresh_token, + user_id=self.config.tgtg.user_id, + datadome_cookie=self.config.tgtg.datadome, + base_url=self.config.tgtg.base_url, ) self.reservations = Reservations(self.tgtg_client) self.favorites = Favorites(self.tgtg_client) @@ -66,20 +73,14 @@ def _get_test_item(self) -> Item: """ Returns an item for test notifications """ - items = sorted(self._get_favorites(), - key=lambda x: x.items_available, - reverse=True) + items = sorted(self._get_favorites(), key=lambda x: x.items_available, reverse=True) if items: return items[0] items = sorted( [ Item(item, self.location) - for item in self.tgtg_client.get_items( - favorites_only=False, - latitude=53.5511, - longitude=9.9937, - radius=50) + for item in self.tgtg_client.get_items(favorites_only=False, latitude=53.5511, longitude=9.9937, radius=50) ], key=lambda x: x.items_available, reverse=True, @@ -91,25 +92,24 @@ def _job(self) -> None: """ Job iterates over all monitored items """ - items = [] + if self.notifiers is None: + raise RuntimeError("Notifiers not initialized!") + + items: list[Item] = [] for item_id in self.item_ids: try: if item_id != "": - item = self.tgtg_client.get_item(item_id) - items.append(Item(item, self.location)) + item_dict = self.tgtg_client.get_item(item_id) + items.append(Item(item_dict, self.location)) except TgtgAPIError as err: log.error(err) items += self._get_favorites() for item in items: self._check_item(item) - amounts = {item_id: self.state.get(item_id).items_available - for item_id in self.state.keys() - if self.state.get(item_id) is not None} + amounts = {item_id: item.items_available for item_id, item in self.state.items() if item is not None} log.debug("new State: %s", amounts) - self.reservations.make_orders( - self.state, - self.notifiers.send_reservation) + self.reservations.make_orders(self.state, self.notifiers.send) if len(self.state) == 0: log.warning("No items in observation! Did you add any favorites?") @@ -118,7 +118,7 @@ def _job(self) -> None: self.tgtg_client.access_token, self.tgtg_client.refresh_token, self.tgtg_client.user_id, - self.tgtg_client.datadome_cookie + self.tgtg_client.datadome_cookie, ) def _get_favorites(self) -> list[Item]: @@ -144,22 +144,20 @@ def _check_item(self, item: Item) -> None: if state_item is not None: if state_item.items_available == item.items_available: return - log.info("%s - new amount: %s", - item.display_name, item.items_available) - if (state_item.items_available == 0 and item.items_available > 0): + log.info("%s - new amount: %s", item.display_name, item.items_available) + if state_item.items_available == 0 and item.items_available > 0: self._send_messages(item) - self.metrics.send_notifications.labels( - item.item_id, item.display_name - ).inc() - self.metrics.item_count.labels(item.item_id, - item.display_name - ).set(item.items_available) + self.metrics.send_notifications.labels(item.item_id, item.display_name).inc() + self.metrics.item_count.labels(item.item_id, item.display_name).set(item.items_available) self.state[item.item_id] = item def _send_messages(self, item: Item) -> None: """ Send notifications for Item """ + if self.notifiers is None: + raise RuntimeError("Notifiers not initialized!") + log.info( "Sending notifications for %s - %s bags available", item.display_name, @@ -177,31 +175,28 @@ def run(self) -> NoReturn: self.tgtg_client.access_token, self.tgtg_client.refresh_token, self.tgtg_client.user_id, - self.tgtg_client.datadome_cookie + self.tgtg_client.datadome_cookie, ) # activate location service self.location = Location( - self.config.location.get("enabled"), - self.config.location.get("gmaps_api_key"), - self.config.location.get("origin_address"), + self.config.location.enabled, + self.config.location.google_maps_api_key, + self.config.location.origin_address, ) # activate and test notifiers if self.config.metrics: self.metrics.enable_metrics() - self.notifiers = Notifiers( - self.config, self.reservations, self.favorites) - if not self.config.disable_tests and \ - self.notifiers.notifier_count > 0: + self.notifiers = Notifiers(self.config, self.reservations, self.favorites) + self.notifiers.start() + if not self.config.disable_tests and self.notifiers.notifier_count > 0: log.info("Sending test Notifications ...") self.notifiers.send(self._get_test_item()) # start scanner log.info("Scanner started ...") running = True if self.cron != Cron("* * * * *"): - log.info("Active on schedule: %s", - self.cron.get_description(self.config.locale)) - activity = Activity(self.config.activity and not - (self.config.docker or self.config.quiet)) + log.info("Active on schedule: %s", self.cron.get_description(self.config.locale)) + activity = Activity(self.config.activity and not (self.config.docker or self.config.quiet)) while True: if self.cron.is_now: if not running: @@ -212,7 +207,7 @@ def run(self) -> NoReturn: except Exception: log.error("Job Error! - %s", sys.exc_info()) finally: - sleep_time = self.config.sleep_time * (.9 + .2 * random()) + sleep_time = self.config.sleep_time * (0.9 + 0.2 * random()) for _ in range(int(sleep_time)): activity.next() sleep(sleep_time / int(sleep_time)) @@ -223,9 +218,9 @@ def run(self) -> NoReturn: else: sleep(60) - def __del__(self) -> None: + def stop(self) -> None: """ - Cleanup on shutdown + Stop scanner. """ if self.notifiers: self.notifiers.stop() @@ -283,8 +278,7 @@ def unset_favorite(self, item_id: str) -> None: def unset_all_favorites(self) -> None: """Remove all items from favorites.""" - item_ids = [item.get("item", {}).get("item_id") - for item in self.get_favorites()] + item_ids = [item.get("item", {}).get("item_id") for item in self.get_favorites()] for item_id in item_ids: self.unset_favorite(item_id) diff --git a/tgtg_scanner/tgtg/__init__.py b/tgtg_scanner/tgtg/__init__.py index af9c1bbb..f5eedc21 100644 --- a/tgtg_scanner/tgtg/__init__.py +++ b/tgtg_scanner/tgtg/__init__.py @@ -1,3 +1,3 @@ -from tgtg_scanner.tgtg.tgtg_client import TgtgClient +# flake8: noqa -__all__ = ['TgtgClient'] +from tgtg_scanner.tgtg.tgtg_client import TgtgClient diff --git a/tgtg_scanner/tgtg/tgtg_client.py b/tgtg_scanner/tgtg/tgtg_client.py index 6fe1b568..255f5a55 100644 --- a/tgtg_scanner/tgtg/tgtg_client.py +++ b/tgtg_scanner/tgtg/tgtg_client.py @@ -7,15 +7,19 @@ import time from datetime import datetime from http import HTTPStatus -from typing import List -from urllib.parse import urljoin +from typing import List, Union +from urllib.parse import urljoin, urlparse import requests from requests.adapters import HTTPAdapter from urllib3.util import Retry -from tgtg_scanner.models.errors import (TgtgAPIError, TGTGConfigurationError, - TgtgLoginError, TgtgPollingError) +from tgtg_scanner.errors import ( + TgtgAPIError, + TGTGConfigurationError, + TgtgLoginError, + TgtgPollingError, +) log = logging.getLogger("tgtg") BASE_URL = "https://apptoogoodtogo.com/api/" @@ -39,10 +43,7 @@ DEFAULT_POLLING_WAIT_TIME = 5 # Seconds DEFAULT_APK_VERSION = "22.11.11" -APK_RE_SCRIPT = re.compile( - r"AF_initDataCallback\({key:\s*'ds:5'.*?" - r"data:([\s\S]*?), sideChannel:.+<\/script" -) +APK_RE_SCRIPT = re.compile(r"AF_initDataCallback\({key:\s*'ds:5'.*?data:([\s\S]*?), sideChannel:.+<\/script") class TgtgSession(requests.Session): @@ -55,34 +56,43 @@ class TgtgSession(requests.Session): ) ) - def __init__(self, user_agent: str = None, language: str = "en-UK", - timeout: int = None, proxies: dict = None, - datadome_cookie: str = None, *args, **kwargs) -> None: + def __init__( + self, + user_agent: Union[str, None] = None, + language: str = "en-UK", + timeout: Union[int, None] = None, + proxies: Union[dict, None] = None, + datadome_cookie: Union[str, None] = None, + base_url: str = BASE_URL, + *args, + **kwargs, + ) -> None: super().__init__(*args, **kwargs) self.mount("https://", self.http_adapter) self.mount("http://", self.http_adapter) self.headers = { - "user-agent": user_agent, "accept-language": language, "accept": "application/json", "content-type": "application/json; charset=utf-8", "Accept-Encoding": "gzip", } + if user_agent: + self.headers["user-agent"] = user_agent self.timeout = timeout - self.proxies = proxies + if proxies: + self.proxies = proxies if datadome_cookie: - self.cookies.set("datadome", datadome_cookie, - domain=".apptoogoodtogo.com", - path="/", secure=True) + domain = urlparse(base_url).netloc.split(":")[0] + domain = f".{'local' if domain == 'localhost' else domain}" + self.cookies.set("datadome", datadome_cookie, domain=domain, path="/", secure=True) - def post(self, url: str, access_token: str = None, **kwargs - ) -> requests.Response: + def post(self, *args, access_token: Union[str, None] = None, **kwargs) -> requests.Response: headers = kwargs.get("headers") if headers is None and getattr(self, "headers"): kwargs["headers"] = getattr(self, "headers") if "headers" in kwargs and access_token: kwargs["headers"]["authorization"] = f"Bearer {access_token}" - return super().post(url, **kwargs) + return super().post(*args, **kwargs) def send(self, request, **kwargs): for key in ["timeout", "proxies"]: @@ -95,7 +105,7 @@ def send(self, request, **kwargs): class TgtgClient: def __init__( self, - url=BASE_URL, + base_url=BASE_URL, email=None, access_token=None, refresh_token=None, @@ -110,7 +120,10 @@ def __init__( polling_wait_time=DEFAULT_POLLING_WAIT_TIME, device_type="ANDROID", ): - self.base_url = url + if base_url != BASE_URL: + log.warn("Using custom tgtg base url: %s", base_url) + + self.base_url = base_url self.email = email self.access_token = access_token @@ -143,11 +156,14 @@ def _get_url(self, path) -> str: def _create_session(self) -> TgtgSession: if not self.user_agent: self.user_agent = self._get_user_agent() - return TgtgSession(self.user_agent, - self.language, - self.timeout, - self.proxies, - self.datadome_cookie) + return TgtgSession( + self.user_agent, + self.language, + self.timeout, + self.proxies, + self.datadome_cookie, + self.base_url, + ) def get_credentials(self) -> dict: """Returns current tgtg api credentials. @@ -161,7 +177,7 @@ def get_credentials(self) -> dict: "access_token": self.access_token, "refresh_token": self.refresh_token, "user_id": self.user_id, - "datadome_cookie": self.datadome_cookie + "datadome_cookie": self.datadome_cookie, } def _post(self, path, **kwargs) -> requests.Response: @@ -193,8 +209,7 @@ def _post(self, path, **kwargs) -> requests.Response: self.datadome_cookie = None self.session = self._create_session() elif self.captcha_error_count >= 10: - log.warning( - "Too many captcha Errors! Sleeping for 10 minutes...") + log.warning("Too many captcha Errors! Sleeping for 10 minutes...") time.sleep(10 * 60) log.info("Retrying ...") self.captcha_error_count = 0 @@ -222,11 +237,12 @@ def get_latest_apk_version() -> str: str: APK Version string """ response = requests.get( - "https://play.google.com/store/apps/" - "details?id=com.app.tgtg&hl=en&gl=US", + "https://play.google.com/store/apps/details?id=com.app.tgtg&hl=en&gl=US", timeout=30, ) match = APK_RE_SCRIPT.search(response.text) + if not match: + raise TgtgAPIError("Failed to get latest APK version from Google Play Store.") data = json.loads(match.group(1)) return data[1][2][140][0][0][0] @@ -237,26 +253,17 @@ def _already_logged(self) -> bool: def _refresh_token(self) -> None: if ( self.last_time_token_refreshed - and (datetime.now() - self.last_time_token_refreshed).seconds - <= self.access_token_lifetime + and (datetime.now() - self.last_time_token_refreshed).seconds <= self.access_token_lifetime ): return - response = self._post( - REFRESH_ENDPOINT, - json={"refresh_token": self.refresh_token} - ) + response = self._post(REFRESH_ENDPOINT, json={"refresh_token": self.refresh_token}) self.access_token = response.json().get("access_token") self.refresh_token = response.json().get("refresh_token") self.last_time_token_refreshed = datetime.now() def login(self) -> None: - if not (self.email or - self.access_token and - self.refresh_token and - self.user_id): - raise TGTGConfigurationError( - "You must provide at least email or access_token, " - "refresh_token and user_id") + if not (self.email or self.access_token and self.refresh_token and self.user_id): + raise TGTGConfigurationError("You must provide at least email or access_token, refresh_token and user_id") if self._already_logged: self._refresh_token() else: @@ -266,13 +273,12 @@ def login(self) -> None: json={ "device_type": self.device_type, "email": self.email, - } + }, ) first_login_response = response.json() if first_login_response["state"] == "TERMS": raise TgtgPollingError( - f"This email {self.email} is not linked to a tgtg " - "account. Please signup with this email first." + f"This email {self.email} is not linked to a tgtg account. Please signup with this email first." ) if first_login_response.get("state") == "WAIT": self.start_polling(first_login_response.get("polling_id")) @@ -287,13 +293,11 @@ def start_polling(self, polling_id) -> None: "device_type": self.device_type, "email": self.email, "request_polling_id": polling_id, - } + }, ) if response.status_code == HTTPStatus.ACCEPTED: log.warning( - "Check your mailbox on PC to continue... " - "(Mailbox on mobile won't work, " - "if you have installed tgtg app.)" + "Check your mailbox on PC to continue... (Mailbox on mobile won't work, if you have installed tgtg app.)" ) time.sleep(self.polling_wait_time) continue @@ -303,8 +307,7 @@ def start_polling(self, polling_id) -> None: self.access_token = login_response.get("access_token") self.refresh_token = login_response.get("refresh_token") self.last_time_token_refreshed = datetime.now() - self.user_id = login_response.get( - "startup_data", {}).get("user", {}).get("user_id") + self.user_id = login_response.get("startup_data", {}).get("user", {}).get("user_id") return raise TgtgPollingError("Max polling retries reached. Try again.") @@ -353,7 +356,8 @@ def get_item(self, item_id: str) -> dict: self.login() response = self._post( f"{API_ITEM_ENDPOINT}/{item_id}", - json={"user_id": self.user_id, "origin": None}) + json={"user_id": self.user_id, "origin": None}, + ) return response.json() def get_favorites(self) -> List[dict]: @@ -366,11 +370,7 @@ def get_favorites(self) -> List[dict]: page = 1 page_size = 100 while True: - new_items = self.get_items( - favorites_only=True, - page_size=page_size, - page=page - ) + new_items = self.get_items(favorites_only=True, page_size=page_size, page=page) items += new_items if len(new_items) < page_size: break @@ -381,18 +381,17 @@ def set_favorite(self, item_id: str, is_favorite: bool) -> None: self.login() self._post( f"{API_ITEM_ENDPOINT}/{item_id}/setFavorite", - json={"is_favorite": is_favorite}) + json={"is_favorite": is_favorite}, + ) - def create_order(self, item_id: str, item_count: int) -> dict: + def create_order(self, item_id: str, item_count: int) -> dict[str, str]: self.login() - response = self._post( - f"{CREATE_ORDER_ENDPOINT}/{item_id}", - json={"item_count": item_count}) + response = self._post(f"{CREATE_ORDER_ENDPOINT}/{item_id}", json={"item_count": item_count}) if response.json().get("state") != "SUCCESS": raise TgtgAPIError(response.status_code, response.content) return response.json().get("order", {}) - def get_order_status(self, order_id: str) -> dict: + def get_order_status(self, order_id: str) -> dict[str, str]: self.login() response = self._post(ORDER_STATUS_ENDPOINT.format(order_id)) return response.json() @@ -400,8 +399,6 @@ def get_order_status(self, order_id: str) -> dict: def abort_order(self, order_id: str) -> None: """Use this when your order is not yet paid""" self.login() - response = self._post( - ABORT_ORDER_ENDPOINT.format(order_id), - json={"cancel_reason_id": 1}) + response = self._post(ABORT_ORDER_ENDPOINT.format(order_id), json={"cancel_reason_id": 1}) if response.json().get("state") != "SUCCESS": raise TgtgAPIError(response.status_code, response.content) diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..fc90d77c --- /dev/null +++ b/tox.ini @@ -0,0 +1,9 @@ +[tox] +isolated_build = true +env_list = py{39,310,311,312} + +[testenv] +skip_install = true +allowlist_externals = make +commands_pre = poetry install --without build +commands = make test diff --git a/wiki/Configuration.md b/wiki/Configuration.md index 39736b82..ab921dc6 100644 --- a/wiki/Configuration.md +++ b/wiki/Configuration.md @@ -98,8 +98,8 @@ You can combine multiple crons as semicolon separated list. | config.ini | environment | description | default | required if enabled | variables | |------------|-------------|-------------|---------|:-------------------:|:---------:| | Enabled | LOCATION | enable location service | `false` | -| Google_Maps_API_Key | LOCATION_GOOGLE_MAPS_API_KEY | API key for google maps service | | YES | -| Address | LOCATION_ADDRESS | origin for distance calculation, e.g. your home address | | YES | +| GoogleMapsAPIKey | LOCATION_GOOGLE_MAPS_API_KEY | API key for google maps service | | YES | +| OriginAddress | LOCATION_ORIGIN_ADDRESS | origin for distance calculation, e.g. your home address | | YES | ### [CONSOLE] / Console Notifier @@ -121,7 +121,7 @@ You can combine multiple crons as semicolon separated list. | Username | SMTP_USERNAME | login username | | Password | SMTP_PASSWORD | login password | | Sender | SMTP_SENDER | email sender | -| Recipient | SMTP_RECIPIENT | email recipient | | YES | +| Recipients | SMTP_RECIPIENTS | email recipients | | YES | | Subject | SMTP_SUBJECT | email subject | `New Magic Bags` | | YES | | Body | SMTP_BODY | email html body | `${{display_name}}
New Amount: ${{items_available}}` | | YES | | Cron | SMTP_CRON | enable notification only on schedule | `* * * * *` | @@ -152,9 +152,9 @@ You can combine multiple crons as semicolon separated list. |------------|-------------|-------------|---------|:-------------------:|:---------:| | Enabled | TELEGRAM | enable Telegram notifications | `false` | | Token | TELEGRAM_TOKEN | Telegram Bot token | | YES | -| Chat_IDs | TELEGRAM_CHAT_IDS | comma-separated list of chat ids | +| ChatIDs | TELEGRAM_CHAT_IDS | comma-separated list of chat ids | | Body | TELEGRAM_BODY | message body | `*${{display_name}}* \n*Available*: ${{items_available}}\n*Price*: ${{price}} ${{currency}}\n*Pickup*: ${{pickupdate}}` | | YES -| disableCommands | TELEGRAM_DISABLE_COMMANDS | disable bot commands | `false` | +| DisableCommands | TELEGRAM_DISABLE_COMMANDS | disable bot commands | `false` | | Timeout | TELEGRAM_TIMEOUT | timeout for telegram API requests | 60 | | Cron | TELEGRAM_CRON | enable notification only on schedule | `* * * * *` |