diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index f0f52f59e..3ee2b16c5 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -20,36 +20,26 @@ jobs: runs-on: ubuntu-latest steps: - - - name: Checkout - uses: actions/checkout@v2 + - name: Checkout + uses: actions/checkout@v6 - - - name: Prepare + - name: Prepare id: prepare run: | if [[ $GITHUB_REF == refs/tags/* ]]; then TAG=${GITHUB_REF#refs/tags/} - echo ::set-output name=tag_name::${TAG} - echo ::set-output name=version::${TAG%-*} + echo "tag_name=${TAG}" >> $GITHUB_OUTPUT + echo "version=${TAG%-*}" >> $GITHUB_OUTPUT else - echo ::set-output name=version::snapshot + echo "version=snapshot" >> $GITHUB_OUTPUT fi - echo ::set-output name=build_date::$(date -u +'%Y-%m-%dT%H:%M:%SZ') - - # Work around an armv7 -> qemu bug caused during rust compilation. Yeah.. See repo. - - name: Run Docker on tmpfs - uses: JonasAlfredsson/docker-on-tmpfs@v1 - with: - tmpfs_size: 8 - swap_size: 8 - swap_location: '/mnt/swapfile' + echo "build_date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v4 # Set up the tag(s) we will build/push. The `metadata-action` action has # a bunch of rules and a little language for specifying this. This should @@ -59,32 +49,35 @@ jobs: # When What Example # =================== ============================= ============= # All runs "dev" dev - # Merge to main "latest" latest # Merge tag v1.2.3b1 # Merge semver tag 1.2.3 - # Merge semver tag . 1.2 - - - name: Set up docker tag names + # Merge semver tag . 1.2 + # Merge semver tag "latest" (non-prerelease) latest + # + # Note that "latest" follows tagged releases (via `flavor: latest=auto`), + # NOT merges to main; main merges publish only "dev". + - name: Set up docker tag names id: docker_meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v6 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + flavor: | + latest=auto tags: | type=raw,value=dev - type=raw,value=latest,enable={{is_default_branch}} type=ref,event=tag type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} - name: Build locally - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v7 with: context: . cache-from: type=gha cache-to: type=gha,mode=max push: false tags: ${{ steps.docker_meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} + labels: ${{ steps.docker_meta.outputs.labels }} build-args: | BUILD_DATE=${{ steps.prepare.outputs.build_date }} GIT_SHORT_SHA=${GITHUB_SHA::8} @@ -92,26 +85,25 @@ jobs: platforms: | linux/amd64 linux/arm64 - linux/arm/v7 - name: Log in to container registry - if: ${{ '${{ secrets.GITHUB_TOKEN }}' != '' }} - uses: docker/login-action@v2 + if: ${{ github.event_name != 'pull_request' }} + uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push to container registry - if: ${{ '${{ secrets.GITHUB_TOKEN }}' != '' }} - uses: docker/build-push-action@v3 + if: ${{ github.event_name != 'pull_request' }} + uses: docker/build-push-action@v7 with: context: . cache-from: type=gha cache-to: type=gha,mode=max push: true tags: ${{ steps.docker_meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} + labels: ${{ steps.docker_meta.outputs.labels }} build-args: | BUILD_DATE=${{ steps.prepare.outputs.build_date }} GIT_SHORT_SHA=${GITHUB_SHA::8} @@ -119,4 +111,3 @@ jobs: platforms: | linux/amd64 linux/arm64 - linux/arm/v7 diff --git a/.github/workflows/legacy-upgrade.yml b/.github/workflows/legacy-upgrade.yml new file mode 100644 index 000000000..de270bf09 --- /dev/null +++ b/.github/workflows/legacy-upgrade.yml @@ -0,0 +1,107 @@ +name: Legacy Upgrade Test + +# Restores the frozen v1.3 backup zips committed under +# testdata/legacy-backups/, runs `kegbot upgrade`, and verifies the +# result. Because the committed zips are frozen, any future migration is +# tested against a database as it existed at v1.3. +# +# The zips are built locally with bin/generate-legacy-backups.sh (docker +# compose). The steps below self-skip while no zips are committed. + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + REDIS_URL: redis://localhost:6379/0 + KEGBOT_SECRET_KEY: changeme + +jobs: + upgrade: + name: 'Upgrade from v1.3 (${{ matrix.database }})' + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + database: [mysql, postgres] + include: + - database: mysql + database_url: mysql://root:cipass@127.0.0.1/kegbot_ci + - database: postgres + database_url: postgres://postgres:cipass@127.0.0.1/kegbot_ci + + services: + mysql: + image: ${{ matrix.database == 'mysql' && 'mysql:8' || '' }} + env: + MYSQL_ROOT_PASSWORD: cipass + MYSQL_DATABASE: kegbot_ci + ports: + - 3306:3306 + options: --health-cmd "mysqladmin ping" --health-interval 10s --health-timeout 5s --health-retries 10 + + postgres: + image: ${{ matrix.database == 'postgres' && 'postgres:16' || '' }} + env: + POSTGRES_PASSWORD: cipass + POSTGRES_DB: kegbot_ci + ports: + - 5432:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 10 + + redis: + image: redis + ports: + - 6379:6379 + + env: + DATABASE_URL: ${{ matrix.database_url }} + KEGBOT_DATA_DIR: ${{ github.workspace }}/kegbot-data + + steps: + - uses: actions/checkout@v6 + + - name: Check for frozen backup + id: frozen + run: | + if [ -f "testdata/legacy-backups/kegbot-v1.3-${{ matrix.database }}.zip" ]; then + echo "present=true" >> "$GITHUB_OUTPUT" + else + echo "present=false" >> "$GITHUB_OUTPUT" + echo "No frozen backup committed yet; skipping." + fi + + - name: Install uv + if: steps.frozen.outputs.present == 'true' + uses: astral-sh/setup-uv@v7 + + - name: Set up Python 3.14 + if: steps.frozen.outputs.present == 'true' + uses: actions/setup-python@v6 + with: + python-version: '3.14' + + - name: Install system libraries + if: steps.frozen.outputs.present == 'true' + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + default-libmysqlclient-dev libpq-dev pkg-config + + - name: Install dependencies + if: steps.frozen.outputs.present == 'true' + run: uv sync --all-groups + + - name: Restore v1.3 backup + if: steps.frozen.outputs.present == 'true' + run: uv run bin/kegbot restore testdata/legacy-backups/kegbot-v1.3-${{ matrix.database }}.zip + + - name: Run upgrade + if: steps.frozen.outputs.present == 'true' + run: uv run bin/kegbot upgrade + + - name: Verify upgraded site + if: steps.frozen.outputs.present == 'true' + run: uv run python bin/check-upgraded-site.py diff --git a/.github/workflows/pybuild.yml b/.github/workflows/pybuild.yml index 148b5d70f..397a94515 100644 --- a/.github/workflows/pybuild.yml +++ b/.github/workflows/pybuild.yml @@ -2,32 +2,83 @@ name: Python Build & Test on: push: - branches: - - main + branches: [main] pull_request: - branches: - - main + branches: [main] jobs: - py_build_and_test: + test: + name: 'Build and test (${{ matrix.database }})' runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + database: [sqlite, mysql, postgres] + include: + - database: sqlite + database_url: sqlite:///test-db.sqlite + - database: mysql + database_url: mysql://root:cipass@127.0.0.1/kegbot_ci + - database: postgres + database_url: postgres://postgres:cipass@127.0.0.1/kegbot_ci + + services: + mysql: + image: ${{ matrix.database == 'mysql' && 'mysql:8' || '' }} + env: + MYSQL_ROOT_PASSWORD: cipass + MYSQL_DATABASE: kegbot_ci + ports: + - 3306:3306 + options: --health-cmd "mysqladmin ping" --health-interval 10s --health-timeout 5s --health-retries 10 + + postgres: + image: ${{ matrix.database == 'postgres' && 'postgres:16' || '' }} + env: + POSTGRES_PASSWORD: cipass + POSTGRES_DB: kegbot_ci + ports: + - 5432:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 10 + + redis: + image: redis + ports: + - 6379:6379 + + env: + DATABASE_URL: ${{ matrix.database_url }} + REDIS_URL: redis://localhost:6379/0 + KEGBOT_SECRET_KEY: changeme + steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 + + - name: Install uv + uses: astral-sh/setup-uv@v7 - - name: Setup Python - uses: actions/setup-python@v1 + - name: Set up Python 3.14 + uses: actions/setup-python@v6 with: - python-version: "3.10" + python-version: '3.14' - - name: Install dependencies + - name: Install system libraries run: | - pip install poetry docker-compose - poetry install -n + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + default-libmysqlclient-dev libpq-dev pkg-config - - name: Run mysql & redis - run: | - docker-compose -f testdata/test-docker-compose.yml up -d - sleep 10 && ./bin/wait-for-container.sh testdata_mysql_1 + - name: Install dependencies + run: uv sync --all-groups + + - name: Run migrations + run: uv run bin/kegbot migrate --noinput + + - name: Collect static files + run: uv run bin/kegbot collectstatic --noinput -v 0 + + - name: Run tests + run: uv run pytest - - name: pytest - run: DATABASE_URL=mysql://root:changeme@127.0.0.1:3306/kegbot_dev REDIS_URL=redis:// KEGBOT_SECRET_KEY=changeme poetry run pytest + - name: Run lint checks + run: uv run ruff check diff --git a/.gitignore b/.gitignore index 189d604bd..6158c700f 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,6 @@ docs/build ### pykeg build dist -test-db.sqlite \ No newline at end of file +test-db.sqlite + +/tmp diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d7caf6599..1f0fda227 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,22 +1,17 @@ default_language_version: - python: python3.10 + python: python3.14 repos: - - repo: https://github.com/floatingpurr/sync_with_poetry - rev: '0.1.0' # the revision or tag to clone at + - repo: https://github.com/tsvikas/sync-with-uv + rev: v0.4.0 hooks: - - id: sync_with_poetry - args: [] # optional args - additional_dependencies: ['poetry'] + - id: sync-with-uv - - repo: https://github.com/pycqa/isort - rev: 5.10.1 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.16.1 hooks: - - id: isort - name: isort (python) - - - repo: https://github.com/psf/black - rev: 22.6.0 - hooks: - - id: black - + # Run the linter. + - id: ruff-check + args: [--fix] + # Run the formatter. + - id: ruff-format diff --git a/.python-version b/.python-version new file mode 100644 index 000000000..6324d401a --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.14 diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 6fe48fff0..9f7893318 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -5,12 +5,16 @@ # Required version: 2 +# Install the docs toolchain with uv (from the `docs` dependency group) and +# build with sphinx. The project itself is not installed: the docs use no +# autodoc, so the heavyweight runtime deps (mysqlclient, etc.) are unneeded. build: - os: ubuntu-20.04 + os: ubuntu-24.04 tools: - python: '3.10' - jobs: - post_install: - - pip install poetry - - poetry config virtualenvs.create false - - poetry install + python: '3.13' + commands: + - asdf plugin add uv + - asdf install uv latest + - asdf global uv latest + - uv sync --only-group docs --frozen + - uv run --no-sync sphinx-build -T -b html docs/source $READTHEDOCS_OUTPUT/html diff --git a/CHANGELOG.md b/CHANGELOG.md index c6e107e93..8f45cd11a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,3 +2,35 @@ You can [view the published changelog here](https://docs.kegbot.org/projects/kegbot-server/en/latest/releases/changelog.html). +## Current version (unreleased) + +A modernization release. The runtime, framework, and toolchain were all brought +up to date. + +### Highlights + +- **Python 3.14** is now required (was 3.10). +- **Django 5.2 LTS** (was 3.2). +- Web server switched from **gunicorn/gevent** to **waitress**. +- Packaging moved from **Poetry** to **uv**; linting/formatting moved to **ruff**; + pre-flight checks run via **pre-commit**. +- protobuf upgraded to the 6.x series. +- Docker image rebuilt on `python:3.14-slim` with uv. +- **Very old backups can now be restored directly.** `kegbot restore` accepts + legacy format-1 backups (created by Kegbot v1.1.x) and upgrades their data in + one step; no intermediate 1.2/1.3 install is needed. + +### Upgrade notes (for existing installs) + +- **Everyone is logged out once.** Sessions now use the JSON serializer (Django + removed the pickle serializer), so existing session cookies are invalidated on + upgrade. Users simply log in again. +- **Background jobs enqueue on database commit.** Stats/notification jobs are now + handed to the worker only after the surrounding DB transaction commits. Ensure + `run_workers` is running (unchanged) to process them. +- **`run_gunicorn` was removed.** Use `kegbot run_server` (now waitress). Tune + with `--waitress_options` (e.g. `--threads=8`); `$PORT` is still honored. +- The Docker image no longer publishes a `linux/arm/v7` variant (amd64 + arm64 + only). +- The legacy gflags-based Python API client was removed from the server package; + it lives in the separate kegbot-api project. diff --git a/Dockerfile b/Dockerfile index d2f47d05b..23c57f573 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,48 +1,54 @@ -FROM python:3.10.5-bullseye +FROM python:3.14-slim RUN mkdir /app WORKDIR /app ENV SHELL=/bin/sh \ - PIP_NO_CACHE_DIR=1 \ KEGBOT_DATA_DIR=/kegbot-data \ KEGBOT_IN_DOCKER=True \ KEGBOT_ENV=debug -# Install toolchains. Mostly, image libraries that Python PIL/Pillow will require. +# Build/runtime libraries: MySQL + Postgres client headers (mysqlclient, +# psycopg2 build from source), client tools (`kegbot backup`/`restore` +# shell out to mysqldump/mysql/pg_dump/psql), and the image libraries +# Pillow needs. RUN apt-get -qq update \ - && DEBIAN_FRONTEND=noninteractive apt-get -y install \ - curl \ - libffi-dev \ + && DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \ + build-essential \ + pkg-config \ + default-libmysqlclient-dev \ + default-mysql-client \ + libpq-dev \ + postgresql-client \ + libjpeg-dev \ libfreetype6-dev \ - libfribidi-dev \ - libharfbuzz-dev \ - libjpeg-turbo-progs \ - libjpeg62-turbo-dev \ liblcms2-dev \ libopenjp2-7-dev \ - libtiff5-dev \ + libtiff-dev \ libwebp-dev \ - libssl-dev \ zlib1g-dev \ - && rm -rf /var/lib/apt/lists/* \ - && python -m pip install -U pip \ - # The cryptography build requires rust, which adds >1GB to the image. \ - # Install it only to install cryptography, then remove it. \ - && curl https://sh.rustup.rs -sSf | sh -s -- -y \ - && PATH=/root/.cargo/bin:$PATH pip install cryptography \ - && rm -rf /root/.rustup /root/.cargo \ - && pip install poetry \ - && rm -rf /root/.cache + && rm -rf /var/lib/apt/lists/* -# Install python dependencies. -COPY pyproject.toml poetry.lock ./ -ADD pykeg/__init__.py ./pykeg/ -RUN poetry config virtualenvs.create false && poetry install -n +# uv, configured to install into the system Python (no venv). +COPY --from=ghcr.io/astral-sh/uv:0.11 /uv /uvx /bin/ +ENV UV_COMPILE_BYTECODE=1 \ + UV_LINK_MODE=copy \ + UV_NO_DEV=1 \ + UV_PYTHON_DOWNLOADS=0 \ + UV_PROJECT_ENVIRONMENT=/usr/local + +# Install dependencies first (cached) using only the lockfile + manifest. +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv sync --locked --no-install-project # Install the app itself. -ADD bin /usr/local/sbin/ -ADD pykeg ./pykeg +COPY bin /usr/local/sbin/ +COPY pykeg ./pykeg +COPY pyproject.toml uv.lock ./ +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --locked # Collect static files. Use fake versions of required env variables # since they're not relevant at this step. @@ -55,7 +61,7 @@ RUN DATABASE_URL=mysql:// \ ARG GIT_SHORT_SHA="unknown" ARG VERSION="unknown" ARG BUILD_DATE="unknown" -RUN echo "GIT_SHORT_SHA=${GIT_SHORT_SHA}\nVERSION=${VERSION}\nBUILD_DATE=${BUILD_DATE}" > /etc/kegbot-version +RUN printf "GIT_SHORT_SHA=%s\nVERSION=%s\nBUILD_DATE=%s\n" "${GIT_SHORT_SHA}" "${VERSION}" "${BUILD_DATE}" > /etc/kegbot-version VOLUME ["/kegbot-data"] diff --git a/Makefile b/Makefile index f1d317f9f..2ba570ed5 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ fixtures: export KEGBOT_BASE_URL = http://fixture.example.com/ fixtures: export KEGBOT_ENV = test fixtures: rm -f test-db.sqlite - bin/kegbot generate_fixtures + uv run bin/kegbot generate_fixtures rm -f test-db.sqlite .PHONY: fixtures \ No newline at end of file diff --git a/bin/build-legacy-backup.sh b/bin/build-legacy-backup.sh new file mode 100755 index 000000000..641fab770 --- /dev/null +++ b/bin/build-legacy-backup.sh @@ -0,0 +1,28 @@ +#!/bin/sh +# Builds a "v1.3 site" backup zip for upgrade-path testing. +# +# Usage: build-legacy-backup.sh +# +# Requires a fresh, empty database at DATABASE_URL (mysql or postgres), +# redis at REDIS_URL, and a writable KEGBOT_DATA_DIR. The database is +# populated from testdata/demo-site.json, which records a site at +# server_version 1.3.0, then dumped with `kegbot backup` (format 2). +# +# Normally run inside docker compose via bin/generate-legacy-backups.sh, +# which builds the frozen zips checked in under testdata/legacy-backups/. + +set -eu + +OUT="${1:?usage: $0 }" + +bin/kegbot migrate --noinput -v 0 +bin/kegbot loaddata testdata/demo-site.json + +ZIP_PATH=$(bin/kegbot backup | sed -n 's/^Path: //p') +if [ -z "$ZIP_PATH" ] || [ ! -f "$ZIP_PATH" ]; then + echo "error: backup zip not found (got: '$ZIP_PATH')" >&2 + exit 1 +fi + +cp "$ZIP_PATH" "$OUT" +echo "Wrote $OUT" diff --git a/bin/check-upgraded-site.py b/bin/check-upgraded-site.py new file mode 100755 index 000000000..e8a8b353d --- /dev/null +++ b/bin/check-upgraded-site.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +"""Verifies a site restored from a v1.3 backup and upgraded to this version. + +Run after `kegbot restore ` + `kegbot upgrade`. Compares the +database against testdata/demo-site.json (the data the legacy backups were +built from) and smoke-tests key pages. Exits nonzero on any failure. +""" + +import json +import os +import sys +from collections import Counter + +import django + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pykeg.settings") +django.setup() + +from django.apps import apps # noqa: E402 +from django.test import Client # noqa: E402 + +from pykeg.core import models # noqa: E402 +from pykeg.core.util import get_version # noqa: E402 + +failures = [] + + +def check(label, expected, actual): + if expected == actual: + print(f"ok: {label}: {actual}") + else: + print(f"FAIL: {label}: expected {expected}, got {actual}") + failures.append(label) + + +with open("testdata/demo-site.json") as f: + fixture_counts = Counter(row["model"] for row in json.load(f)) + +for label, expected in sorted(fixture_counts.items()): + check(f"count {label}", expected, apps.get_model(label).objects.count()) + +site = models.KegbotSite.get() +check("server_version", get_version(), site.server_version) +check("site is_setup", True, site.is_setup) +check("kegs on tap", 2, models.Keg.objects.filter(status=models.Keg.STATUS_ON_TAP).count()) + +client = Client() +for url in ["/", "/kegs/", "/stats/", "/sessions/", "/accounts/login/"]: + response = client.get(url, follow=True) + check(f"GET {url}", 200, response.status_code) + +api_key = models.ApiKey.objects.first() +if api_key: + response = client.get(f"/api/v1/status/?api_key={api_key.key}") + check("GET /api/v1/status (restored api key)", 200, response.status_code) + +if failures: + print(f"\n{len(failures)} check(s) failed") + sys.exit(1) +print("\nAll checks passed.") diff --git a/bin/generate-legacy-backups.sh b/bin/generate-legacy-backups.sh new file mode 100755 index 000000000..b2de4be63 --- /dev/null +++ b/bin/generate-legacy-backups.sh @@ -0,0 +1,28 @@ +#!/bin/sh +# Generates the frozen v1.3 backup fixtures with docker compose. +# +# Usage: bin/generate-legacy-backups.sh +# +# Writes testdata/legacy-backups/kegbot-v1.3-{mysql,postgres}.zip, built +# from testdata/demo-site.json against pinned mysql/postgres images. +# Commit the zips to update the fixtures used by the Legacy Upgrade Test +# workflow. +# +# Regenerate deliberately, not routinely: the fixtures are frozen so that +# future migrations are tested against a database as it existed at v1.3. + +set -eu + +cd "$(dirname "$0")/.." +mkdir -p testdata/legacy-backups + +COMPOSE="docker compose -f testdata/legacy-backup-compose.yml" +trap '$COMPOSE down -v --remove-orphans' EXIT + +$COMPOSE build generate-mysql +$COMPOSE run --rm generate-mysql +$COMPOSE run --rm generate-postgres + +echo "" +echo "Fixtures written:" +ls -l testdata/legacy-backups/*.zip diff --git a/bin/kegbot b/bin/kegbot index e8aa644ea..6c325630c 100755 --- a/bin/kegbot +++ b/bin/kegbot @@ -1,9 +1,7 @@ #!/usr/bin/env python import os -import subprocess import sys -from builtins import * import django from django.core import management @@ -11,12 +9,6 @@ from django.core import management from pykeg.core.util import get_version from pykeg.util import bugreport -if sys.version_info < (2, 7): - sys.stderr.write( - "Error: Kegbot needs Python 2.7 or newer.\n\n" "Current version: %s\n" % sys.version - ) - sys.exit(1) - if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pykeg.settings") @@ -25,16 +17,8 @@ if __name__ == "__main__": cmd = sys.argv[1] if cmd == "version": # Hack: Django's `version` command cannot be overridden the usual way. - print("kegbot-server {}".format(get_version())) + print(f"kegbot-server {get_version()}") sys.exit(0) - elif cmd == "run_gunicorn": - # run_gunicorn was deprecated upstream. - cmd = "gunicorn pykeg.web.wsgi:application {}".format(" ".join(sys.argv[2:])) - sys.stderr.write( - "Warning: run_gunicorn is deprecated, call `{}` instead.\n".format(cmd) - ) - ret = subprocess.call(cmd, shell=True) - sys.exit(ret) elif cmd == "bugreport": ret = bugreport.take_bugreport() sys.exit(ret) diff --git a/docs/rtd-requirements.txt b/docs/rtd-requirements.txt deleted file mode 100644 index ca5e0a104..000000000 --- a/docs/rtd-requirements.txt +++ /dev/null @@ -1 +0,0 @@ -sphinx-issues==1.2.0 diff --git a/docs/source/conf.py b/docs/source/conf.py index d615876f8..63c90c501 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Configuration file for the Sphinx documentation builder. # diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 7ef153878..000000000 --- a/poetry.lock +++ /dev/null @@ -1,2109 +0,0 @@ -[[package]] -name = "addict" -version = "2.4.0" -description = "Addict is a dictionary whose items can be set using both attribute and item syntax." -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "alabaster" -version = "0.7.12" -description = "A configurable sidebar-enabled Sphinx theme" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "asgiref" -version = "3.5.2" -description = "ASGI specs, helper code, and adapters" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.extras] -tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] - -[[package]] -name = "async-timeout" -version = "4.0.2" -description = "Timeout context manager for asyncio programs" -category = "main" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "atomicwrites" -version = "1.4.1" -description = "Atomic file writes." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "attrs" -version = "22.1.0" -description = "Classes Without Boilerplate" -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] -docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"] - -[[package]] -name = "babel" -version = "2.10.3" -description = "Internationalization utilities" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -pytz = ">=2015.7" - -[[package]] -name = "beautifulsoup4" -version = "4.11.1" -description = "Screen-scraping library" -category = "dev" -optional = false -python-versions = ">=3.6.0" - -[package.dependencies] -soupsieve = ">1.2" - -[package.extras] -html5lib = ["html5lib"] -lxml = ["lxml"] - -[[package]] -name = "black" -version = "22.6.0" -description = "The uncompromising code formatter." -category = "dev" -optional = false -python-versions = ">=3.6.2" - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "certifi" -version = "2022.6.15" -description = "Python package for providing Mozilla's CA Bundle." -category = "main" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "cffi" -version = "1.15.1" -description = "Foreign Function Interface for Python calling C code." -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -pycparser = "*" - -[[package]] -name = "charset-normalizer" -version = "2.1.0" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" -optional = false -python-versions = ">=3.6.0" - -[package.extras] -unicode_backport = ["unicodedata2"] - -[[package]] -name = "click" -version = "8.1.3" -description = "Composable command line interface toolkit" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "colorama" -version = "0.4.5" -description = "Cross-platform colored terminal text." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "coloredlogs" -version = "15.0.1" -description = "Colored terminal output for Python's logging module" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[package.dependencies] -humanfriendly = ">=9.1" - -[package.extras] -cron = ["capturer (>=2.4)"] - -[[package]] -name = "confusable-homoglyphs" -version = "3.2.0" -description = "Detect confusable usage of unicode homoglyphs, prevent homograph attacks." -category = "main" -optional = false -python-versions = "*" - -[package.extras] -cli = ["click"] - -[[package]] -name = "deprecated" -version = "1.2.13" -description = "Python @deprecated decorator to deprecate old python classes, functions or methods." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.dependencies] -wrapt = ">=1.10,<2" - -[package.extras] -dev = ["tox", "bump2version (<1)", "sphinx (<2)", "importlib-metadata (<3)", "importlib-resources (<4)", "configparser (<5)", "sphinxcontrib-websupport (<2)", "zipp (<2)", "PyTest (<5)", "PyTest-Cov (<2.6)", "pytest", "pytest-cov"] - -[[package]] -name = "dj-database-url" -version = "1.0.0" -description = "Use Database URLs in your Django Application." -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -Django = ">3.2" - -[[package]] -name = "dj-email-url" -version = "1.0.5" -description = "Use an URL to configure email backend settings in your Django Application." -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "django" -version = "3.2.15" -description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -asgiref = ">=3.3.2,<4" -pytz = "*" -sqlparse = ">=0.2.2" - -[package.extras] -bcrypt = ["bcrypt"] -argon2 = ["argon2-cffi (>=19.1.0)"] - -[[package]] -name = "django-appconf" -version = "1.0.5" -description = "A helper class for handling configuration defaults of packaged apps gracefully." -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -django = "*" - -[[package]] -name = "django-bootstrap-pagination" -version = "1.7.1" -description = "Render Django Page objects as Bootstrap 3.x/4.x Pagination compatible HTML" -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "django-cors-headers" -version = "3.13.0" -description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)." -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -Django = ">=3.2" - -[[package]] -name = "django-crispy-forms" -version = "1.14.0" -description = "Best way to have Django DRY forms" -category = "main" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "django-imagekit" -version = "4.1.0" -description = "Automated image processing for Django models." -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -django-appconf = {version = "*", markers = "python_version > \"3\""} -pilkit = ">=0.2.0" -six = "*" - -[package.extras] -async = ["django-celery (>=3.0)"] -async_dramatiq = ["django-dramatiq (>=0.4.0)"] -async_rq = ["django-rq (>=0.6.0)"] - -[[package]] -name = "django-redis" -version = "5.2.0" -description = "Full featured redis cache backend for Django." -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -Django = ">=2.2" -redis = ">=3,<4.0.0 || >4.0.0,<4.0.1 || >4.0.1" - -[package.extras] -hiredis = ["redis[hiredis] (>=3,!=4.0.0,!=4.0.1)"] - -[[package]] -name = "django-registration" -version = "3.3" -description = "An extensible user-registration application for Django" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -confusable-homoglyphs = ">=3.0,<4.0" -Django = ">=3.2" - -[[package]] -name = "django-rq" -version = "2.5.1" -description = "An app that provides django integration for RQ (Redis Queue)" -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -django = ">=2.0" -redis = ">=3" -rq = ">=1.2" - -[package.extras] -sentry = ["raven (>=6.1.0)"] -testing = ["mock (>=2.0.0)"] - -[[package]] -name = "djangorestframework" -version = "3.13.1" -description = "Web APIs for Django, made easy." -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -django = ">=2.2" -pytz = "*" - -[[package]] -name = "docutils" -version = "0.19" -description = "Docutils -- Python Documentation Utilities" -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "flake8" -version = "5.0.4" -description = "the modular source code checker: pep8 pyflakes and co" -category = "dev" -optional = false -python-versions = ">=3.6.1" - -[package.dependencies] -mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.9.0,<2.10.0" -pyflakes = ">=2.5.0,<2.6.0" - -[[package]] -name = "freezegun" -version = "1.2.1" -description = "Let your Python tests travel through time" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -python-dateutil = ">=2.7" - -[[package]] -name = "furo" -version = "2022.6.21" -description = "A clean customisable Sphinx documentation theme." -category = "dev" -optional = false -python-versions = ">=3.7" -develop = false - -[package.dependencies] -beautifulsoup4 = "*" -pygments = "*" -sphinx = ">=4.0,<6.0" -sphinx-basic-ng = "*" - -[package.source] -type = "git" -url = "https://github.com/Kegbot/furo" -reference = "kegbot/kegbot" -resolved_reference = "493cf3b4a44c8038c1aa0efa3d2aff8ff12b742c" - -[[package]] -name = "future" -version = "0.18.2" -description = "Clean single-source support for Python 3 and 2" -category = "main" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" - -[[package]] -name = "gevent" -version = "21.12.0" -description = "Coroutine-based network library" -category = "main" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5" - -[package.dependencies] -cffi = {version = ">=1.12.2", markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""} -greenlet = {version = ">=1.1.0,<2.0", markers = "platform_python_implementation == \"CPython\""} -"zope.event" = "*" -"zope.interface" = "*" - -[package.extras] -dnspython = ["dnspython (>=1.16.0,<2.0)", "idna"] -docs = ["repoze.sphinx.autointerface", "sphinxcontrib-programoutput", "zope.schema"] -monitor = ["psutil (>=5.7.0)"] -recommended = ["cffi (>=1.12.2)", "dnspython (>=1.16.0,<2.0)", "idna", "selectors2", "backports.socketpair", "psutil (>=5.7.0)"] -test = ["requests", "objgraph", "cffi (>=1.12.2)", "dnspython (>=1.16.0,<2.0)", "idna", "selectors2", "futures", "mock", "backports.socketpair", "contextvars (==2.4)", "coverage (>=5.0)", "coveralls (>=1.7.0)", "psutil (>=5.7.0)"] - -[[package]] -name = "greenlet" -version = "1.1.2" -description = "Lightweight in-process concurrent programming" -category = "main" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" - -[package.extras] -docs = ["sphinx"] - -[[package]] -name = "gunicorn" -version = "20.1.0" -description = "WSGI HTTP Server for UNIX" -category = "main" -optional = false -python-versions = ">=3.5" - -[package.extras] -eventlet = ["eventlet (>=0.24.1)"] -gevent = ["gevent (>=1.4.0)"] -setproctitle = ["setproctitle"] -tornado = ["tornado (>=0.2)"] - -[[package]] -name = "httplib2" -version = "0.20.4" -description = "A comprehensive HTTP client library." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.dependencies] -pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0.2,<3.0.3 || >3.0.3,<4", markers = "python_version > \"3.0\""} - -[[package]] -name = "humanfriendly" -version = "10.0" -description = "Human friendly output for text interfaces using Python" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[package.dependencies] -pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_version >= \"3.8\""} - -[[package]] -name = "idna" -version = "3.3" -description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "imagesize" -version = "1.4.1" -description = "Getting image size from png/jpeg/jpeg2000/gif file" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "iniconfig" -version = "1.1.1" -description = "iniconfig: brain-dead simple config-ini parsing" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "isodate" -version = "0.6.1" -description = "An ISO 8601 date/time/duration parser and formatter" -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -six = "*" - -[[package]] -name = "jinja2" -version = "3.1.2" -description = "A very fast and expressive template engine." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "livereload" -version = "2.6.3" -description = "Python LiveReload is an awesome tool for web developers" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -six = "*" -tornado = {version = "*", markers = "python_version > \"2.7\""} - -[[package]] -name = "markupsafe" -version = "2.1.1" -description = "Safely add untrusted strings to HTML/XML markup." -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "multidict" -version = "6.0.2" -description = "multidict implementation" -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "mypy-extensions" -version = "0.4.3" -description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "mysqlclient" -version = "2.1.1" -description = "Python interface to MySQL" -category = "main" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "oauthlib" -version = "3.2.0" -description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.extras] -rsa = ["cryptography (>=3.0.0)"] -signals = ["blinker (>=1.4.0)"] -signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] - -[[package]] -name = "packaging" -version = "21.3" -description = "Core utilities for Python packages" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" - -[[package]] -name = "pathspec" -version = "0.9.0" -description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" - -[[package]] -name = "pilkit" -version = "2.0" -description = "A collection of utilities and processors for the Python Imaging Libary." -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "pillow" -version = "9.2.0" -description = "Python Imaging Library (Fork)" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"] -tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] - -[[package]] -name = "platformdirs" -version = "2.5.2" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] -test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] - -[[package]] -name = "pluggy" -version = "1.0.0" -description = "plugin and hook calling mechanisms for python" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.extras] -testing = ["pytest-benchmark", "pytest"] -dev = ["tox", "pre-commit"] - -[[package]] -name = "protobuf" -version = "3.20.0" -description = "Protocol Buffers" -category = "main" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "psycopg2" -version = "2.9.3" -description = "psycopg2 - Python-PostgreSQL Database Adapter" -category = "main" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "pycodestyle" -version = "2.9.1" -description = "Python style guide checker" -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "pycparser" -version = "2.21" -description = "C parser in Python" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "pyflakes" -version = "2.5.0" -description = "passive checker of Python programs" -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "pygments" -version = "2.12.0" -description = "Pygments is a syntax highlighting package written in Python." -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "pyparsing" -version = "3.0.9" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -category = "main" -optional = false -python-versions = ">=3.6.8" - -[package.extras] -diagrams = ["railroad-diagrams", "jinja2"] - -[[package]] -name = "pyreadline3" -version = "3.4.1" -description = "A python implementation of GNU readline." -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "pytest" -version = "7.1.2" -description = "pytest: simple powerful testing with Python" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} -attrs = ">=19.2.0" -colorama = {version = "*", markers = "sys_platform == \"win32\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -tomli = ">=1.0.0" - -[package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] - -[[package]] -name = "pytest-django" -version = "4.5.2" -description = "A Django plugin for pytest." -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.dependencies] -pytest = ">=5.4.0" - -[package.extras] -docs = ["sphinx", "sphinx-rtd-theme"] -testing = ["django", "django-configurations (>=2.0)"] - -[[package]] -name = "python-dateutil" -version = "2.8.2" -description = "Extensions to the standard Python datetime module" -category = "main" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "python-gflags" -version = "3.1.2" -description = "Obsolete. Please migrate to absl-py instead." -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "pytz" -version = "2022.1" -description = "World timezone definitions, modern and historical" -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "pyyaml" -version = "6.0" -description = "YAML parser and emitter for Python" -category = "main" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "redis" -version = "4.3.4" -description = "Python client for Redis database and key-value store" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -async-timeout = ">=4.0.2" -deprecated = ">=1.2.3" -packaging = ">=20.4" - -[package.extras] -hiredis = ["hiredis (>=1.0.0)"] -ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] - -[[package]] -name = "requests" -version = "2.28.1" -description = "Python HTTP for Humans." -category = "main" -optional = false -python-versions = ">=3.7, <4" - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<3" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<1.27" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "requests-mock" -version = "1.9.3" -description = "Mock out responses from the requests package" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -requests = ">=2.3,<3" -six = "*" - -[package.extras] -fixture = ["fixtures"] -test = ["fixtures", "mock", "purl", "pytest", "sphinx", "testrepository (>=0.0.18)", "testtools"] - -[[package]] -name = "requests-oauthlib" -version = "1.3.1" -description = "OAuthlib authentication support for Requests." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.dependencies] -oauthlib = ">=3.0.0" -requests = ">=2.0.0" - -[package.extras] -rsa = ["oauthlib[signedtoken] (>=3.0.0)"] - -[[package]] -name = "rq" -version = "1.11.0" -description = "RQ is a simple, lightweight, library for creating background jobs, and processing them." -category = "main" -optional = false -python-versions = ">=3.5" - -[package.dependencies] -click = ">=5.0.0" -redis = ">=3.5.0" - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" - -[[package]] -name = "snowballstemmer" -version = "2.2.0" -description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "soupsieve" -version = "2.3.2.post1" -description = "A modern CSS selector implementation for Beautiful Soup." -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "sphinx" -version = "5.1.1" -description = "Python documentation generator" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -alabaster = ">=0.7,<0.8" -babel = ">=1.3" -colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} -docutils = ">=0.14,<0.20" -imagesize = "*" -Jinja2 = ">=2.3" -packaging = "*" -Pygments = ">=2.0" -requests = ">=2.5.0" -snowballstemmer = ">=1.1" -sphinxcontrib-applehelp = "*" -sphinxcontrib-devhelp = "*" -sphinxcontrib-htmlhelp = ">=2.0.0" -sphinxcontrib-jsmath = "*" -sphinxcontrib-qthelp = "*" -sphinxcontrib-serializinghtml = ">=1.1.5" - -[package.extras] -docs = ["sphinxcontrib-websupport"] -lint = ["flake8 (>=3.5.0)", "flake8-comprehensions", "flake8-bugbear", "isort", "mypy (>=0.971)", "sphinx-lint", "docutils-stubs", "types-typed-ast", "types-requests"] -test = ["pytest (>=4.6)", "html5lib", "cython", "typed-ast"] - -[[package]] -name = "sphinx-autobuild" -version = "2021.3.14" -description = "Rebuild Sphinx documentation on changes, with live-reload in the browser." -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -colorama = "*" -livereload = "*" -sphinx = "*" - -[package.extras] -test = ["pytest", "pytest-cov"] - -[[package]] -name = "sphinx-basic-ng" -version = "0.0.1a12" -description = "A modern skeleton for Sphinx themes." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -sphinx = ">=4.0,<6.0" - -[package.extras] -docs = ["furo", "myst-parser", "sphinx-copybutton", "sphinx-inline-tabs", "ipython"] - -[[package]] -name = "sphinx-issues" -version = "3.0.1" -description = "A Sphinx extension for linking to your project's issue tracker" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -sphinx = "*" - -[package.extras] -dev = ["pytest (>=6.2.0)", "flake8 (==3.9.2)", "flake8-bugbear (==20.11.1)", "pre-commit (>=2.7,<3.0)", "tox"] -lint = ["flake8 (==3.9.2)", "flake8-bugbear (==20.11.1)", "pre-commit (>=2.7,<3.0)"] -tests = ["pytest (>=6.2.0)"] - -[[package]] -name = "sphinxcontrib-applehelp" -version = "1.0.2" -description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.extras] -test = ["pytest"] -lint = ["docutils-stubs", "mypy", "flake8"] - -[[package]] -name = "sphinxcontrib-devhelp" -version = "1.0.2" -description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.extras] -test = ["pytest"] -lint = ["docutils-stubs", "mypy", "flake8"] - -[[package]] -name = "sphinxcontrib-htmlhelp" -version = "2.0.0" -description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.extras] -test = ["html5lib", "pytest"] -lint = ["docutils-stubs", "mypy", "flake8"] - -[[package]] -name = "sphinxcontrib-jsmath" -version = "1.0.1" -description = "A sphinx extension which renders display math in HTML via JavaScript" -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.extras] -test = ["mypy", "flake8", "pytest"] - -[[package]] -name = "sphinxcontrib-qthelp" -version = "1.0.3" -description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.extras] -test = ["pytest"] -lint = ["docutils-stubs", "mypy", "flake8"] - -[[package]] -name = "sphinxcontrib-serializinghtml" -version = "1.1.5" -description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] -test = ["pytest"] - -[[package]] -name = "sqlparse" -version = "0.4.2" -description = "A non-validating SQL parser." -category = "main" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "tornado" -version = "6.2" -description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." -category = "dev" -optional = false -python-versions = ">= 3.7" - -[[package]] -name = "urllib3" -version = "1.26.11" -description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" - -[package.extras] -brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] - -[[package]] -name = "vcrpy" -version = "4.2.0" -description = "Automatically mock your HTTP interactions to simplify and speed up testing" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -PyYAML = "*" -six = ">=1.5" -wrapt = "*" -yarl = "*" - -[[package]] -name = "whitenoise" -version = "6.2.0" -description = "Radically simplified static file serving for WSGI applications" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.extras] -brotli = ["brotli"] - -[[package]] -name = "wrapt" -version = "1.14.1" -description = "Module for decorators, wrappers and monkey patching." -category = "main" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" - -[[package]] -name = "yarl" -version = "1.8.1" -description = "Yet another URL library" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -idna = ">=2.0" -multidict = ">=4.0" - -[[package]] -name = "zope.event" -version = "4.5.0" -description = "Very basic event publishing system" -category = "main" -optional = false -python-versions = "*" - -[package.extras] -docs = ["sphinx"] -test = ["zope.testrunner"] - -[[package]] -name = "zope.interface" -version = "5.4.0" -description = "Interfaces for Python" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[package.extras] -docs = ["sphinx", "repoze.sphinx.autointerface"] -test = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] -testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] - -[metadata] -lock-version = "1.1" -python-versions = "^3.10.4" -content-hash = "1e70a1452915a5f3a870a2589a8e3a5568bba208d16a5f59df014bce67fadfec" - -[metadata.files] -addict = [ - {file = "addict-2.4.0-py3-none-any.whl", hash = "sha256:249bb56bbfd3cdc2a004ea0ff4c2b6ddc84d53bc2194761636eb314d5cfa5dfc"}, - {file = "addict-2.4.0.tar.gz", hash = "sha256:b3b2210e0e067a281f5646c8c5db92e99b7231ea8b0eb5f74dbdf9e259d4e494"}, -] -alabaster = [ - {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, - {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, -] -asgiref = [ - {file = "asgiref-3.5.2-py3-none-any.whl", hash = "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4"}, - {file = "asgiref-3.5.2.tar.gz", hash = "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"}, -] -async-timeout = [ - {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, - {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, -] -atomicwrites = [ - {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, -] -attrs = [ - {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, - {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, -] -babel = [ - {file = "Babel-2.10.3-py3-none-any.whl", hash = "sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb"}, - {file = "Babel-2.10.3.tar.gz", hash = "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51"}, -] -beautifulsoup4 = [ - {file = "beautifulsoup4-4.11.1-py3-none-any.whl", hash = "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30"}, - {file = "beautifulsoup4-4.11.1.tar.gz", hash = "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693"}, -] -black = [ - {file = "black-22.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69"}, - {file = "black-22.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b270a168d69edb8b7ed32c193ef10fd27844e5c60852039599f9184460ce0807"}, - {file = "black-22.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6797f58943fceb1c461fb572edbe828d811e719c24e03375fd25170ada53825e"}, - {file = "black-22.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c85928b9d5f83b23cee7d0efcb310172412fbf7cb9d9ce963bd67fd141781def"}, - {file = "black-22.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6fe02afde060bbeef044af7996f335fbe90b039ccf3f5eb8f16df8b20f77666"}, - {file = "black-22.6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cfaf3895a9634e882bf9d2363fed5af8888802d670f58b279b0bece00e9a872d"}, - {file = "black-22.6.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94783f636bca89f11eb5d50437e8e17fbc6a929a628d82304c80fa9cd945f256"}, - {file = "black-22.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2ea29072e954a4d55a2ff58971b83365eba5d3d357352a07a7a4df0d95f51c78"}, - {file = "black-22.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e439798f819d49ba1c0bd9664427a05aab79bfba777a6db94fd4e56fae0cb849"}, - {file = "black-22.6.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:187d96c5e713f441a5829e77120c269b6514418f4513a390b0499b0987f2ff1c"}, - {file = "black-22.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:074458dc2f6e0d3dab7928d4417bb6957bb834434516f21514138437accdbe90"}, - {file = "black-22.6.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a218d7e5856f91d20f04e931b6f16d15356db1c846ee55f01bac297a705ca24f"}, - {file = "black-22.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:568ac3c465b1c8b34b61cd7a4e349e93f91abf0f9371eda1cf87194663ab684e"}, - {file = "black-22.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6c1734ab264b8f7929cef8ae5f900b85d579e6cbfde09d7387da8f04771b51c6"}, - {file = "black-22.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9a3ac16efe9ec7d7381ddebcc022119794872abce99475345c5a61aa18c45ad"}, - {file = "black-22.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:b9fd45787ba8aa3f5e0a0a98920c1012c884622c6c920dbe98dbd05bc7c70fbf"}, - {file = "black-22.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7ba9be198ecca5031cd78745780d65a3f75a34b2ff9be5837045dce55db83d1c"}, - {file = "black-22.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3db5b6409b96d9bd543323b23ef32a1a2b06416d525d27e0f67e74f1446c8f2"}, - {file = "black-22.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:560558527e52ce8afba936fcce93a7411ab40c7d5fe8c2463e279e843c0328ee"}, - {file = "black-22.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b154e6bbde1e79ea3260c4b40c0b7b3109ffcdf7bc4ebf8859169a6af72cd70b"}, - {file = "black-22.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:4af5bc0e1f96be5ae9bd7aaec219c901a94d6caa2484c21983d043371c733fc4"}, - {file = "black-22.6.0-py3-none-any.whl", hash = "sha256:ac609cf8ef5e7115ddd07d85d988d074ed00e10fbc3445aee393e70164a2219c"}, - {file = "black-22.6.0.tar.gz", hash = "sha256:6c6d39e28aed379aec40da1c65434c77d75e65bb59a1e1c283de545fb4e7c6c9"}, -] -certifi = [ - {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, - {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, -] -cffi = [ - {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, - {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, - {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, - {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, - {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, - {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, - {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, - {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, - {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, - {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, - {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, - {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, - {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, - {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, - {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, - {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, - {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, - {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, - {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, -] -charset-normalizer = [ - {file = "charset-normalizer-2.1.0.tar.gz", hash = "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"}, - {file = "charset_normalizer-2.1.0-py3-none-any.whl", hash = "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5"}, -] -click = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, -] -colorama = [ - {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, - {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, -] -coloredlogs = [ - {file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"}, - {file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"}, -] -confusable-homoglyphs = [ - {file = "confusable_homoglyphs-3.2.0-py2.py3-none-any.whl", hash = "sha256:e3ce611028d882b74a5faa69e3cbb5bd4dcd9f69936da6e73d33eda42c917944"}, - {file = "confusable_homoglyphs-3.2.0.tar.gz", hash = "sha256:3b4a0d9fa510669498820c91a0bfc0c327568cecec90648cf3819d4a6fc6a751"}, -] -deprecated = [ - {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, - {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, -] -dj-database-url = [ - {file = "dj-database-url-1.0.0.tar.gz", hash = "sha256:ccf3e8718f75ddd147a1e212fca88eecdaa721759ee48e38b485481c77bca3dc"}, - {file = "dj_database_url-1.0.0-py3-none-any.whl", hash = "sha256:cd354a3b7a9136d78d64c17b2aec369e2ae5616fbca6bfbe435ef15bb372ce39"}, -] -dj-email-url = [ - {file = "dj-email-url-1.0.5.tar.gz", hash = "sha256:ef36f8a324ec57cf3be5c7a7ef44ed6900ca0208624a918ab33adc1cf6427b39"}, - {file = "dj_email_url-1.0.5-py2.py3-none-any.whl", hash = "sha256:64257c4f9d8139a4af8e5267229d32260e433fbf257b0cf8fc855bb0cc39ca7d"}, -] -django = [ - {file = "Django-3.2.15-py3-none-any.whl", hash = "sha256:115baf5049d5cf4163e43492cdc7139c306ed6d451e7d3571fe9612903903713"}, - {file = "Django-3.2.15.tar.gz", hash = "sha256:f71934b1a822f14a86c9ac9634053689279cd04ae69cb6ade4a59471b886582b"}, -] -django-appconf = [ - {file = "django-appconf-1.0.5.tar.gz", hash = "sha256:be3db0be6c81fa84742000b89a81c016d70ae66a7ccb620cdef592b1f1a6aaa4"}, - {file = "django_appconf-1.0.5-py3-none-any.whl", hash = "sha256:ae9f864ee1958c815a965ed63b3fba4874eec13de10236ba063a788f9a17389d"}, -] -django-bootstrap-pagination = [ - {file = "django-bootstrap-pagination-1.7.1.tar.gz", hash = "sha256:69d826d92217325611cb86e49944d8261e3c92eaa4deafea5a605d79fd363883"}, - {file = "django_bootstrap_pagination-1.7.1-py3-none-any.whl", hash = "sha256:47e742679cf109e12f10ae09d5263433f94440818cf7b023e90a3f5252849639"}, -] -django-cors-headers = [ - {file = "django-cors-headers-3.13.0.tar.gz", hash = "sha256:f9dc6b4e3f611c3199700b3e5f3398c28757dcd559c2f82932687f3d0443cfdf"}, - {file = "django_cors_headers-3.13.0-py3-none-any.whl", hash = "sha256:37e42883b5f1f2295df6b4bba96eb2417a14a03270cb24b2a07f021cd4487cf4"}, -] -django-crispy-forms = [ - {file = "django-crispy-forms-1.14.0.tar.gz", hash = "sha256:35887b8851a931374dd697207a8f56c57a9c5cb9dbf0b9fa54314da5666cea5b"}, - {file = "django_crispy_forms-1.14.0-py3-none-any.whl", hash = "sha256:bc4d2037f6de602d39c0bc452ac3029d1f5d65e88458872cc4dbc01c3a400604"}, -] -django-imagekit = [ - {file = "django-imagekit-4.1.0.tar.gz", hash = "sha256:e559aeaae43a33b34f87631a9fa5696455e4451ffa738a42635fde442fedac5c"}, - {file = "django_imagekit-4.1.0-py2.py3-none-any.whl", hash = "sha256:87e36f8dc1d8745647881f4366ef4965225f048042dacebbee0dcb87425defef"}, -] -django-redis = [ - {file = "django-redis-5.2.0.tar.gz", hash = "sha256:8a99e5582c79f894168f5865c52bd921213253b7fd64d16733ae4591564465de"}, - {file = "django_redis-5.2.0-py3-none-any.whl", hash = "sha256:1d037dc02b11ad7aa11f655d26dac3fb1af32630f61ef4428860a2e29ff92026"}, -] -django-registration = [ - {file = "django-registration-3.3.tar.gz", hash = "sha256:884a4cc9ec87b9f1c0ceb6b6c4b7ba491c1877997a3cd29cc923697dac785eb8"}, - {file = "django_registration-3.3-py3-none-any.whl", hash = "sha256:dfa176f594fb465c93495caa55686be723a15829769511383e25172d2efbd0e6"}, -] -django-rq = [ - {file = "django-rq-2.5.1.tar.gz", hash = "sha256:f08486602664d73a6e335872c868d79663e380247e6307496d01b8fa770fefd8"}, - {file = "django_rq-2.5.1-py2.py3-none-any.whl", hash = "sha256:7be1e10e7091555f9f36edf100b0dbb205ea2b98683d74443d2bdf3c6649a03f"}, -] -djangorestframework = [ - {file = "djangorestframework-3.13.1-py3-none-any.whl", hash = "sha256:24c4bf58ed7e85d1fe4ba250ab2da926d263cd57d64b03e8dcef0ac683f8b1aa"}, - {file = "djangorestframework-3.13.1.tar.gz", hash = "sha256:0c33407ce23acc68eca2a6e46424b008c9c02eceb8cf18581921d0092bc1f2ee"}, -] -docutils = [ - {file = "docutils-0.19-py3-none-any.whl", hash = "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc"}, - {file = "docutils-0.19.tar.gz", hash = "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6"}, -] -flake8 = [ - {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, - {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, -] -freezegun = [ - {file = "freezegun-1.2.1-py3-none-any.whl", hash = "sha256:15103a67dfa868ad809a8f508146e396be2995172d25f927e48ce51c0bf5cb09"}, - {file = "freezegun-1.2.1.tar.gz", hash = "sha256:b4c64efb275e6bc68dc6e771b17ffe0ff0f90b81a2a5189043550b6519926ba4"}, -] -furo = [] -future = [ - {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, -] -gevent = [ - {file = "gevent-21.12.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:2afa3f3ad528155433f6ac8bd64fa5cc303855b97004416ec719a6b1ca179481"}, - {file = "gevent-21.12.0-cp27-cp27m-win32.whl", hash = "sha256:177f93a3a90f46a5009e0841fef561601e5c637ba4332ab8572edd96af650101"}, - {file = "gevent-21.12.0-cp27-cp27m-win_amd64.whl", hash = "sha256:a5ad4ed8afa0a71e1927623589f06a9b5e8b5e77810be3125cb4d93050d3fd1f"}, - {file = "gevent-21.12.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:eae3c46f9484eaacd67ffcdf4eaf6ca830f587edd543613b0f5c4eb3c11d052d"}, - {file = "gevent-21.12.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e1899b921219fc8959ff9afb94dae36be82e0769ed13d330a393594d478a0b3a"}, - {file = "gevent-21.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c21cb5c9f4e14d75b3fe0b143ec875d7dbd1495fad6d49704b00e57e781ee0f"}, - {file = "gevent-21.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:542ae891e2aa217d2cf6d8446538fcd2f3263a40eec123b970b899bac391c47a"}, - {file = "gevent-21.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:0082d8a5d23c35812ce0e716a91ede597f6dd2c5ff508a02a998f73598c59397"}, - {file = "gevent-21.12.0-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:da8d2d51a49b2a5beb02ad619ca9ddbef806ef4870ba04e5ac7b8b41a5b61db3"}, - {file = "gevent-21.12.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cfff82f05f14b7f5d9ed53ccb7a609ae8604df522bb05c971bca78ec9d8b2b9"}, - {file = "gevent-21.12.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:7909780f0cf18a1fc32aafd8c8e130cdd93c6e285b11263f7f2d1a0f3678bc50"}, - {file = "gevent-21.12.0-cp36-cp36m-win32.whl", hash = "sha256:bb5cb8db753469c7a9a0b8a972d2660fe851aa06eee699a1ca42988afb0aaa02"}, - {file = "gevent-21.12.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c43f081cbca41d27fd8fef9c6a32cf83cb979345b20abc07bf68df165cdadb24"}, - {file = "gevent-21.12.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:74fc1ef16b86616cfddcc74f7292642b0f72dde4dd95aebf4c45bb236744be54"}, - {file = "gevent-21.12.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cc2fef0f98ee180704cf95ec84f2bc2d86c6c3711bb6b6740d74e0afe708b62c"}, - {file = "gevent-21.12.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08b4c17064e28f4eb85604486abc89f442c7407d2aed249cf54544ce5c9baee6"}, - {file = "gevent-21.12.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:973749bacb7bc4f4181a8fb2a7e0e2ff44038de56d08e856dd54a5ac1d7331b4"}, - {file = "gevent-21.12.0-cp37-cp37m-win32.whl", hash = "sha256:6a02a88723ed3f0fd92cbf1df3c4cd2fbd87d82b0a4bac3e36a8875923115214"}, - {file = "gevent-21.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f289fae643a3f1c3b909d6b033e6921b05234a4907e9c9c8c3f1fe403e6ac452"}, - {file = "gevent-21.12.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:3baeeccc4791ba3f8db27179dff11855a8f9210ddd754f6c9b48e0d2561c2aea"}, - {file = "gevent-21.12.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05c5e8a50cd6868dd36536c92fb4468d18090e801bd63611593c0717bab63692"}, - {file = "gevent-21.12.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d86438ede1cbe0fde6ef4cc3f72bf2f1ecc9630d8b633ff344a3aeeca272cdd"}, - {file = "gevent-21.12.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:01928770972181ad8866ee37ea3504f1824587b188fcab782ef1619ce7538766"}, - {file = "gevent-21.12.0-cp38-cp38-win32.whl", hash = "sha256:3c012c73e6c61f13c75e3a4869dbe6a2ffa025f103421a6de9c85e627e7477b1"}, - {file = "gevent-21.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:b7709c64afa8bb3000c28bb91ec42c79594a7cb0f322e20427d57f9762366a5b"}, - {file = "gevent-21.12.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:ec21f9eaaa6a7b1e62da786132d6788675b314f25f98d9541f1bf00584ed4749"}, - {file = "gevent-21.12.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:22ce1f38fdfe2149ffe8ec2131ca45281791c1e464db34b3b4321ae9d8d2efbb"}, - {file = "gevent-21.12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ccffcf708094564e442ac6fde46f0ae9e40015cb69d995f4b39cc29a7643881"}, - {file = "gevent-21.12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:24d3550fbaeef5fddd794819c2853bca45a86c3d64a056a2c268d981518220d1"}, - {file = "gevent-21.12.0-cp39-cp39-win32.whl", hash = "sha256:2bcec9f80196c751fdcf389ca9f7141e7b0db960d8465ed79be5e685bfcad682"}, - {file = "gevent-21.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:3dad62f55fad839d498c801e139481348991cee6e1c7706041b5fe096cb6a279"}, - {file = "gevent-21.12.0-pp27-pypy_73-win_amd64.whl", hash = "sha256:9f9652d1e4062d4b5b5a0a49ff679fa890430b5f76969d35dccb2df114c55e0f"}, - {file = "gevent-21.12.0.tar.gz", hash = "sha256:f48b64578c367b91fa793bf8eaaaf4995cb93c8bc45860e473bf868070ad094e"}, -] -greenlet = [ - {file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"}, - {file = "greenlet-1.1.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a"}, - {file = "greenlet-1.1.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:833e1551925ed51e6b44c800e71e77dacd7e49181fdc9ac9a0bf3714d515785d"}, - {file = "greenlet-1.1.2-cp27-cp27m-win32.whl", hash = "sha256:aa5b467f15e78b82257319aebc78dd2915e4c1436c3c0d1ad6f53e47ba6e2713"}, - {file = "greenlet-1.1.2-cp27-cp27m-win_amd64.whl", hash = "sha256:40b951f601af999a8bf2ce8c71e8aaa4e8c6f78ff8afae7b808aae2dc50d4c40"}, - {file = "greenlet-1.1.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:95e69877983ea39b7303570fa6760f81a3eec23d0e3ab2021b7144b94d06202d"}, - {file = "greenlet-1.1.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:356b3576ad078c89a6107caa9c50cc14e98e3a6c4874a37c3e0273e4baf33de8"}, - {file = "greenlet-1.1.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8639cadfda96737427330a094476d4c7a56ac03de7265622fcf4cfe57c8ae18d"}, - {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497"}, - {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1"}, - {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58"}, - {file = "greenlet-1.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b336501a05e13b616ef81ce329c0e09ac5ed8c732d9ba7e3e983fcc1a9e86965"}, - {file = "greenlet-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708"}, - {file = "greenlet-1.1.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23"}, - {file = "greenlet-1.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee"}, - {file = "greenlet-1.1.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:fa877ca7f6b48054f847b61d6fa7bed5cebb663ebc55e018fda12db09dcc664c"}, - {file = "greenlet-1.1.2-cp35-cp35m-win32.whl", hash = "sha256:7cbd7574ce8e138bda9df4efc6bf2ab8572c9aff640d8ecfece1b006b68da963"}, - {file = "greenlet-1.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:903bbd302a2378f984aef528f76d4c9b1748f318fe1294961c072bdc7f2ffa3e"}, - {file = "greenlet-1.1.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:049fe7579230e44daef03a259faa24511d10ebfa44f69411d99e6a184fe68073"}, - {file = "greenlet-1.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:dd0b1e9e891f69e7675ba5c92e28b90eaa045f6ab134ffe70b52e948aa175b3c"}, - {file = "greenlet-1.1.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7418b6bfc7fe3331541b84bb2141c9baf1ec7132a7ecd9f375912eca810e714e"}, - {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce"}, - {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08"}, - {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168"}, - {file = "greenlet-1.1.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b8c008de9d0daba7b6666aa5bbfdc23dcd78cafc33997c9b7741ff6353bafb7f"}, - {file = "greenlet-1.1.2-cp36-cp36m-win32.whl", hash = "sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa"}, - {file = "greenlet-1.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d"}, - {file = "greenlet-1.1.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4"}, - {file = "greenlet-1.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fdcec0b8399108577ec290f55551d926d9a1fa6cad45882093a7a07ac5ec147b"}, - {file = "greenlet-1.1.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:93f81b134a165cc17123626ab8da2e30c0455441d4ab5576eed73a64c025b25c"}, - {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1"}, - {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28"}, - {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5"}, - {file = "greenlet-1.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c5d5b35f789a030ebb95bff352f1d27a93d81069f2adb3182d99882e095cefe"}, - {file = "greenlet-1.1.2-cp37-cp37m-win32.whl", hash = "sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc"}, - {file = "greenlet-1.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06"}, - {file = "greenlet-1.1.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0"}, - {file = "greenlet-1.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:eb6ea6da4c787111adf40f697b4e58732ee0942b5d3bd8f435277643329ba627"}, - {file = "greenlet-1.1.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:f3acda1924472472ddd60c29e5b9db0cec629fbe3c5c5accb74d6d6d14773478"}, - {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43"}, - {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711"}, - {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b"}, - {file = "greenlet-1.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2bde6792f313f4e918caabc46532aa64aa27a0db05d75b20edfc5c6f46479de2"}, - {file = "greenlet-1.1.2-cp38-cp38-win32.whl", hash = "sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd"}, - {file = "greenlet-1.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3"}, - {file = "greenlet-1.1.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67"}, - {file = "greenlet-1.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:572e1787d1460da79590bf44304abbc0a2da944ea64ec549188fa84d89bba7ab"}, - {file = "greenlet-1.1.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:be5f425ff1f5f4b3c1e33ad64ab994eed12fc284a6ea71c5243fd564502ecbe5"}, - {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88"}, - {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b"}, - {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3"}, - {file = "greenlet-1.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0051c6f1f27cb756ffc0ffbac7d2cd48cb0362ac1736871399a739b2885134d3"}, - {file = "greenlet-1.1.2-cp39-cp39-win32.whl", hash = "sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf"}, - {file = "greenlet-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd"}, - {file = "greenlet-1.1.2.tar.gz", hash = "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a"}, -] -gunicorn = [ - {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, - {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, -] -httplib2 = [ - {file = "httplib2-0.20.4-py3-none-any.whl", hash = "sha256:8b6a905cb1c79eefd03f8669fd993c36dc341f7c558f056cb5a33b5c2f458543"}, - {file = "httplib2-0.20.4.tar.gz", hash = "sha256:58a98e45b4b1a48273073f905d2961666ecf0fbac4250ea5b47aef259eb5c585"}, -] -humanfriendly = [ - {file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"}, - {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, -] -idna = [ - {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, - {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, -] -imagesize = [ - {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, - {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, -] -iniconfig = [ - {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, - {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, -] -isodate = [ - {file = "isodate-0.6.1-py2.py3-none-any.whl", hash = "sha256:0751eece944162659049d35f4f549ed815792b38793f07cf73381c1c87cbed96"}, - {file = "isodate-0.6.1.tar.gz", hash = "sha256:48c5881de7e8b0a0d648cb024c8062dc84e7b840ed81e864c7614fd3c127bde9"}, -] -jinja2 = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, -] -livereload = [ - {file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"}, -] -markupsafe = [ - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, - {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, -] -mccabe = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] -multidict = [ - {file = "multidict-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2"}, - {file = "multidict-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3"}, - {file = "multidict-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:041b81a5f6b38244b34dc18c7b6aba91f9cdaf854d9a39e5ff0b58e2b5773b9c"}, - {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fdda29a3c7e76a064f2477c9aab1ba96fd94e02e386f1e665bca1807fc5386f"}, - {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3368bf2398b0e0fcbf46d85795adc4c259299fec50c1416d0f77c0a843a3eed9"}, - {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4f052ee022928d34fe1f4d2bc743f32609fb79ed9c49a1710a5ad6b2198db20"}, - {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:225383a6603c086e6cef0f2f05564acb4f4d5f019a4e3e983f572b8530f70c88"}, - {file = "multidict-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50bd442726e288e884f7be9071016c15a8742eb689a593a0cac49ea093eef0a7"}, - {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:47e6a7e923e9cada7c139531feac59448f1f47727a79076c0b1ee80274cd8eee"}, - {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0556a1d4ea2d949efe5fd76a09b4a82e3a4a30700553a6725535098d8d9fb672"}, - {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:626fe10ac87851f4cffecee161fc6f8f9853f0f6f1035b59337a51d29ff3b4f9"}, - {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:8064b7c6f0af936a741ea1efd18690bacfbae4078c0c385d7c3f611d11f0cf87"}, - {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2d36e929d7f6a16d4eb11b250719c39560dd70545356365b494249e2186bc389"}, - {file = "multidict-6.0.2-cp310-cp310-win32.whl", hash = "sha256:fcb91630817aa8b9bc4a74023e4198480587269c272c58b3279875ed7235c293"}, - {file = "multidict-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:8cbf0132f3de7cc6c6ce00147cc78e6439ea736cee6bca4f068bcf892b0fd658"}, - {file = "multidict-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:05f6949d6169878a03e607a21e3b862eaf8e356590e8bdae4227eedadacf6e51"}, - {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2c2e459f7050aeb7c1b1276763364884595d47000c1cddb51764c0d8976e608"}, - {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0509e469d48940147e1235d994cd849a8f8195e0bca65f8f5439c56e17872a3"}, - {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:514fe2b8d750d6cdb4712346a2c5084a80220821a3e91f3f71eec11cf8d28fd4"}, - {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19adcfc2a7197cdc3987044e3f415168fc5dc1f720c932eb1ef4f71a2067e08b"}, - {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9d153e7f1f9ba0b23ad1568b3b9e17301e23b042c23870f9ee0522dc5cc79e8"}, - {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:aef9cc3d9c7d63d924adac329c33835e0243b5052a6dfcbf7732a921c6e918ba"}, - {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4571f1beddff25f3e925eea34268422622963cd8dc395bb8778eb28418248e43"}, - {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:d48b8ee1d4068561ce8033d2c344cf5232cb29ee1a0206a7b828c79cbc5982b8"}, - {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:45183c96ddf61bf96d2684d9fbaf6f3564d86b34cb125761f9a0ef9e36c1d55b"}, - {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:75bdf08716edde767b09e76829db8c1e5ca9d8bb0a8d4bd94ae1eafe3dac5e15"}, - {file = "multidict-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:a45e1135cb07086833ce969555df39149680e5471c04dfd6a915abd2fc3f6dbc"}, - {file = "multidict-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6f3cdef8a247d1eafa649085812f8a310e728bdf3900ff6c434eafb2d443b23a"}, - {file = "multidict-6.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0327292e745a880459ef71be14e709aaea2f783f3537588fb4ed09b6c01bca60"}, - {file = "multidict-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e875b6086e325bab7e680e4316d667fc0e5e174bb5611eb16b3ea121c8951b86"}, - {file = "multidict-6.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feea820722e69451743a3d56ad74948b68bf456984d63c1a92e8347b7b88452d"}, - {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc57c68cb9139c7cd6fc39f211b02198e69fb90ce4bc4a094cf5fe0d20fd8b0"}, - {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:497988d6b6ec6ed6f87030ec03280b696ca47dbf0648045e4e1d28b80346560d"}, - {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89171b2c769e03a953d5969b2f272efa931426355b6c0cb508022976a17fd376"}, - {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684133b1e1fe91eda8fa7447f137c9490a064c6b7f392aa857bba83a28cfb693"}, - {file = "multidict-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd9fc9c4849a07f3635ccffa895d57abce554b467d611a5009ba4f39b78a8849"}, - {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e07c8e79d6e6fd37b42f3250dba122053fddb319e84b55dd3a8d6446e1a7ee49"}, - {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4070613ea2227da2bfb2c35a6041e4371b0af6b0be57f424fe2318b42a748516"}, - {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:47fbeedbf94bed6547d3aa632075d804867a352d86688c04e606971595460227"}, - {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5774d9218d77befa7b70d836004a768fb9aa4fdb53c97498f4d8d3f67bb9cfa9"}, - {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2957489cba47c2539a8eb7ab32ff49101439ccf78eab724c828c1a54ff3ff98d"}, - {file = "multidict-6.0.2-cp38-cp38-win32.whl", hash = "sha256:e5b20e9599ba74391ca0cfbd7b328fcc20976823ba19bc573983a25b32e92b57"}, - {file = "multidict-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:8004dca28e15b86d1b1372515f32eb6f814bdf6f00952699bdeb541691091f96"}, - {file = "multidict-6.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2e4a0785b84fb59e43c18a015ffc575ba93f7d1dbd272b4cdad9f5134b8a006c"}, - {file = "multidict-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6701bf8a5d03a43375909ac91b6980aea74b0f5402fbe9428fc3f6edf5d9677e"}, - {file = "multidict-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a007b1638e148c3cfb6bf0bdc4f82776cef0ac487191d093cdc316905e504071"}, - {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07a017cfa00c9890011628eab2503bee5872f27144936a52eaab449be5eaf032"}, - {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c207fff63adcdf5a485969131dc70e4b194327666b7e8a87a97fbc4fd80a53b2"}, - {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:373ba9d1d061c76462d74e7de1c0c8e267e9791ee8cfefcf6b0b2495762c370c"}, - {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfba7c6d5d7c9099ba21f84662b037a0ffd4a5e6b26ac07d19e423e6fdf965a9"}, - {file = "multidict-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19d9bad105dfb34eb539c97b132057a4e709919ec4dd883ece5838bcbf262b80"}, - {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:de989b195c3d636ba000ee4281cd03bb1234635b124bf4cd89eeee9ca8fcb09d"}, - {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7c40b7bbece294ae3a87c1bc2abff0ff9beef41d14188cda94ada7bcea99b0fb"}, - {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:d16cce709ebfadc91278a1c005e3c17dd5f71f5098bfae1035149785ea6e9c68"}, - {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:a2c34a93e1d2aa35fbf1485e5010337c72c6791407d03aa5f4eed920343dd360"}, - {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:feba80698173761cddd814fa22e88b0661e98cb810f9f986c54aa34d281e4937"}, - {file = "multidict-6.0.2-cp39-cp39-win32.whl", hash = "sha256:23b616fdc3c74c9fe01d76ce0d1ce872d2d396d8fa8e4899398ad64fb5aa214a"}, - {file = "multidict-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae"}, - {file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"}, -] -mypy-extensions = [ - {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, - {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, -] -mysqlclient = [ - {file = "mysqlclient-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:c1ed71bd6244993b526113cca3df66428609f90e4652f37eb51c33496d478b37"}, - {file = "mysqlclient-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:c812b67e90082a840efb82a8978369e6e69fc62ce1bda4ca8f3084a9d862308b"}, - {file = "mysqlclient-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:0d1cd3a5a4d28c222fa199002810e8146cffd821410b67851af4cc80aeccd97c"}, - {file = "mysqlclient-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b355c8b5a7d58f2e909acdbb050858390ee1b0e13672ae759e5e784110022994"}, - {file = "mysqlclient-2.1.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:996924f3483fd36a34a5812210c69e71dea5a3d5978d01199b78b7f6d485c855"}, - {file = "mysqlclient-2.1.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:dea88c8d3f5a5d9293dfe7f087c16dd350ceb175f2f6631c9cf4caf3e19b7a96"}, - {file = "mysqlclient-2.1.1.tar.gz", hash = "sha256:828757e419fb11dd6c5ed2576ec92c3efaa93a0f7c39e263586d1ee779c3d782"}, -] -oauthlib = [ - {file = "oauthlib-3.2.0-py3-none-any.whl", hash = "sha256:6db33440354787f9b7f3a6dbd4febf5d0f93758354060e802f6c06cb493022fe"}, - {file = "oauthlib-3.2.0.tar.gz", hash = "sha256:23a8208d75b902797ea29fd31fa80a15ed9dc2c6c16fe73f5d346f83f6fa27a2"}, -] -packaging = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, -] -pathspec = [ - {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, - {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, -] -pilkit = [ - {file = "pilkit-2.0.tar.gz", hash = "sha256:ddb30c2f0198a147e56b151476c3bb9fe045fbfd5b0a0fa2a3148dba62d1559f"}, -] -pillow = [ - {file = "Pillow-9.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a9c9bc489f8ab30906d7a85afac4b4944a572a7432e00698a7239f44a44e6efb"}, - {file = "Pillow-9.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:510cef4a3f401c246cfd8227b300828715dd055463cdca6176c2e4036df8bd4f"}, - {file = "Pillow-9.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7888310f6214f19ab2b6df90f3f06afa3df7ef7355fc025e78a3044737fab1f5"}, - {file = "Pillow-9.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:831e648102c82f152e14c1a0938689dbb22480c548c8d4b8b248b3e50967b88c"}, - {file = "Pillow-9.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cc1d2451e8a3b4bfdb9caf745b58e6c7a77d2e469159b0d527a4554d73694d1"}, - {file = "Pillow-9.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:136659638f61a251e8ed3b331fc6ccd124590eeff539de57c5f80ef3a9594e58"}, - {file = "Pillow-9.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6e8c66f70fb539301e064f6478d7453e820d8a2c631da948a23384865cd95544"}, - {file = "Pillow-9.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:37ff6b522a26d0538b753f0b4e8e164fdada12db6c6f00f62145d732d8a3152e"}, - {file = "Pillow-9.2.0-cp310-cp310-win32.whl", hash = "sha256:c79698d4cd9318d9481d89a77e2d3fcaeff5486be641e60a4b49f3d2ecca4e28"}, - {file = "Pillow-9.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:254164c57bab4b459f14c64e93df11eff5ded575192c294a0c49270f22c5d93d"}, - {file = "Pillow-9.2.0-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:408673ed75594933714482501fe97e055a42996087eeca7e5d06e33218d05aa8"}, - {file = "Pillow-9.2.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:727dd1389bc5cb9827cbd1f9d40d2c2a1a0c9b32dd2261db522d22a604a6eec9"}, - {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50dff9cc21826d2977ef2d2a205504034e3a4563ca6f5db739b0d1026658e004"}, - {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb6259196a589123d755380b65127ddc60f4c64b21fc3bb46ce3a6ea663659b0"}, - {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b0554af24df2bf96618dac71ddada02420f946be943b181108cac55a7a2dcd4"}, - {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:15928f824870535c85dbf949c09d6ae7d3d6ac2d6efec80f3227f73eefba741c"}, - {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:bdd0de2d64688ecae88dd8935012c4a72681e5df632af903a1dca8c5e7aa871a"}, - {file = "Pillow-9.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5b87da55a08acb586bad5c3aa3b86505f559b84f39035b233d5bf844b0834b1"}, - {file = "Pillow-9.2.0-cp311-cp311-win32.whl", hash = "sha256:b6d5e92df2b77665e07ddb2e4dbd6d644b78e4c0d2e9272a852627cdba0d75cf"}, - {file = "Pillow-9.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6bf088c1ce160f50ea40764f825ec9b72ed9da25346216b91361eef8ad1b8f8c"}, - {file = "Pillow-9.2.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:2c58b24e3a63efd22554c676d81b0e57f80e0a7d3a5874a7e14ce90ec40d3069"}, - {file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eef7592281f7c174d3d6cbfbb7ee5984a671fcd77e3fc78e973d492e9bf0eb3f"}, - {file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dcd7b9c7139dc8258d164b55696ecd16c04607f1cc33ba7af86613881ffe4ac8"}, - {file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a138441e95562b3c078746a22f8fca8ff1c22c014f856278bdbdd89ca36cff1b"}, - {file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:93689632949aff41199090eff5474f3990b6823404e45d66a5d44304e9cdc467"}, - {file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:f3fac744f9b540148fa7715a435d2283b71f68bfb6d4aae24482a890aed18b59"}, - {file = "Pillow-9.2.0-cp37-cp37m-win32.whl", hash = "sha256:fa768eff5f9f958270b081bb33581b4b569faabf8774726b283edb06617101dc"}, - {file = "Pillow-9.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:69bd1a15d7ba3694631e00df8de65a8cb031911ca11f44929c97fe05eb9b6c1d"}, - {file = "Pillow-9.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:030e3460861488e249731c3e7ab59b07c7853838ff3b8e16aac9561bb345da14"}, - {file = "Pillow-9.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:74a04183e6e64930b667d321524e3c5361094bb4af9083db5c301db64cd341f3"}, - {file = "Pillow-9.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d33a11f601213dcd5718109c09a52c2a1c893e7461f0be2d6febc2879ec2402"}, - {file = "Pillow-9.2.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fd6f5e3c0e4697fa7eb45b6e93996299f3feee73a3175fa451f49a74d092b9f"}, - {file = "Pillow-9.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a647c0d4478b995c5e54615a2e5360ccedd2f85e70ab57fbe817ca613d5e63b8"}, - {file = "Pillow-9.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:4134d3f1ba5f15027ff5c04296f13328fecd46921424084516bdb1b2548e66ff"}, - {file = "Pillow-9.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:bc431b065722a5ad1dfb4df354fb9333b7a582a5ee39a90e6ffff688d72f27a1"}, - {file = "Pillow-9.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1536ad017a9f789430fb6b8be8bf99d2f214c76502becc196c6f2d9a75b01b76"}, - {file = "Pillow-9.2.0-cp38-cp38-win32.whl", hash = "sha256:2ad0d4df0f5ef2247e27fc790d5c9b5a0af8ade9ba340db4a73bb1a4a3e5fb4f"}, - {file = "Pillow-9.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:ec52c351b35ca269cb1f8069d610fc45c5bd38c3e91f9ab4cbbf0aebc136d9c8"}, - {file = "Pillow-9.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ed2c4ef2451de908c90436d6e8092e13a43992f1860275b4d8082667fbb2ffc"}, - {file = "Pillow-9.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ad2f835e0ad81d1689f1b7e3fbac7b01bb8777d5a985c8962bedee0cc6d43da"}, - {file = "Pillow-9.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea98f633d45f7e815db648fd7ff0f19e328302ac36427343e4432c84432e7ff4"}, - {file = "Pillow-9.2.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7761afe0126d046974a01e030ae7529ed0ca6a196de3ec6937c11df0df1bc91c"}, - {file = "Pillow-9.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a54614049a18a2d6fe156e68e188da02a046a4a93cf24f373bffd977e943421"}, - {file = "Pillow-9.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:5aed7dde98403cd91d86a1115c78d8145c83078e864c1de1064f52e6feb61b20"}, - {file = "Pillow-9.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:13b725463f32df1bfeacbf3dd197fb358ae8ebcd8c5548faa75126ea425ccb60"}, - {file = "Pillow-9.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:808add66ea764ed97d44dda1ac4f2cfec4c1867d9efb16a33d158be79f32b8a4"}, - {file = "Pillow-9.2.0-cp39-cp39-win32.whl", hash = "sha256:337a74fd2f291c607d220c793a8135273c4c2ab001b03e601c36766005f36885"}, - {file = "Pillow-9.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:fac2d65901fb0fdf20363fbd345c01958a742f2dc62a8dd4495af66e3ff502a4"}, - {file = "Pillow-9.2.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:ad2277b185ebce47a63f4dc6302e30f05762b688f8dc3de55dbae4651872cdf3"}, - {file = "Pillow-9.2.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c7b502bc34f6e32ba022b4a209638f9e097d7a9098104ae420eb8186217ebbb"}, - {file = "Pillow-9.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d1f14f5f691f55e1b47f824ca4fdcb4b19b4323fe43cc7bb105988cad7496be"}, - {file = "Pillow-9.2.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:dfe4c1fedfde4e2fbc009d5ad420647f7730d719786388b7de0999bf32c0d9fd"}, - {file = "Pillow-9.2.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:f07f1f00e22b231dd3d9b9208692042e29792d6bd4f6639415d2f23158a80013"}, - {file = "Pillow-9.2.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1802f34298f5ba11d55e5bb09c31997dc0c6aed919658dfdf0198a2fe75d5490"}, - {file = "Pillow-9.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17d4cafe22f050b46d983b71c707162d63d796a1235cdf8b9d7a112e97b15bac"}, - {file = "Pillow-9.2.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96b5e6874431df16aee0c1ba237574cb6dff1dcb173798faa6a9d8b399a05d0e"}, - {file = "Pillow-9.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:0030fdbd926fb85844b8b92e2f9449ba89607231d3dd597a21ae72dc7fe26927"}, - {file = "Pillow-9.2.0.tar.gz", hash = "sha256:75e636fd3e0fb872693f23ccb8a5ff2cd578801251f3a4f6854c6a5d437d3c04"}, -] -platformdirs = [ - {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, - {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, -] -pluggy = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, -] -protobuf = [ - {file = "protobuf-3.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9d0f3aca8ca51c8b5e204ab92bd8afdb2a8e3df46bd0ce0bd39065d79aabcaa4"}, - {file = "protobuf-3.20.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:001c2160c03b6349c04de39cf1a58e342750da3632f6978a1634a3dcca1ec10e"}, - {file = "protobuf-3.20.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5b5860b790498f233cdc8d635a17fc08de62e59d4dcd8cdb6c6c0d38a31edf2b"}, - {file = "protobuf-3.20.0-cp310-cp310-win32.whl", hash = "sha256:0b250c60256c8824219352dc2a228a6b49987e5bf94d3ffcf4c46585efcbd499"}, - {file = "protobuf-3.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:a1eebb6eb0653e594cb86cd8e536b9b083373fca9aba761ade6cd412d46fb2ab"}, - {file = "protobuf-3.20.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:bc14037281db66aa60856cd4ce4541a942040686d290e3f3224dd3978f88f554"}, - {file = "protobuf-3.20.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:47257d932de14a7b6c4ae1b7dbf592388153ee35ec7cae216b87ae6490ed39a3"}, - {file = "protobuf-3.20.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fbcbb068ebe67c4ff6483d2e2aa87079c325f8470b24b098d6bf7d4d21d57a69"}, - {file = "protobuf-3.20.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:542f25a4adf3691a306dcc00bf9a73176554938ec9b98f20f929a044f80acf1b"}, - {file = "protobuf-3.20.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fd7133b885e356fa4920ead8289bb45dc6f185a164e99e10279f33732ed5ce15"}, - {file = "protobuf-3.20.0-cp37-cp37m-win32.whl", hash = "sha256:8d84453422312f8275455d1cb52d850d6a4d7d714b784e41b573c6f5bfc2a029"}, - {file = "protobuf-3.20.0-cp37-cp37m-win_amd64.whl", hash = "sha256:52bae32a147c375522ce09bd6af4d2949aca32a0415bc62df1456b3ad17c6001"}, - {file = "protobuf-3.20.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:25d2fcd6eef340082718ec9ad2c58d734429f2b1f7335d989523852f2bba220b"}, - {file = "protobuf-3.20.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:88c8be0558bdfc35e68c42ae5bf785eb9390d25915d4863bbc7583d23da77074"}, - {file = "protobuf-3.20.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:38fd9eb74b852e4ee14b16e9670cd401d147ee3f3ec0d4f7652e0c921d6227f8"}, - {file = "protobuf-3.20.0-cp38-cp38-win32.whl", hash = "sha256:7dcd84dc31ebb35ade755e06d1561d1bd3b85e85dbdbf6278011fc97b22810db"}, - {file = "protobuf-3.20.0-cp38-cp38-win_amd64.whl", hash = "sha256:1eb13f5a5a59ca4973bcfa2fc8fff644bd39f2109c3f7a60bd5860cb6a49b679"}, - {file = "protobuf-3.20.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d24c81c2310f0063b8fc1c20c8ed01f3331be9374b4b5c2de846f69e11e21fb"}, - {file = "protobuf-3.20.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:8be43a91ab66fe995e85ccdbdd1046d9f0443d59e060c0840319290de25b7d33"}, - {file = "protobuf-3.20.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7a53d4035427b9dbfbb397f46642754d294f131e93c661d056366f2a31438263"}, - {file = "protobuf-3.20.0-cp39-cp39-win32.whl", hash = "sha256:32bf4a90c207a0b4e70ca6dd09d43de3cb9898f7d5b69c2e9e3b966a7f342820"}, - {file = "protobuf-3.20.0-cp39-cp39-win_amd64.whl", hash = "sha256:6efe066a7135233f97ce51a1aa007d4fb0be28ef093b4f88dac4ad1b3a2b7b6f"}, - {file = "protobuf-3.20.0-py2.py3-none-any.whl", hash = "sha256:4eda68bd9e2a4879385e6b1ea528c976f59cd9728382005cc54c28bcce8db983"}, - {file = "protobuf-3.20.0.tar.gz", hash = "sha256:71b2c3d1cd26ed1ec7c8196834143258b2ad7f444efff26fdc366c6f5e752702"}, -] -psycopg2 = [ - {file = "psycopg2-2.9.3-cp310-cp310-win32.whl", hash = "sha256:083707a696e5e1c330af2508d8fab36f9700b26621ccbcb538abe22e15485362"}, - {file = "psycopg2-2.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:d3ca6421b942f60c008f81a3541e8faf6865a28d5a9b48544b0ee4f40cac7fca"}, - {file = "psycopg2-2.9.3-cp36-cp36m-win32.whl", hash = "sha256:9572e08b50aed176ef6d66f15a21d823bb6f6d23152d35e8451d7d2d18fdac56"}, - {file = "psycopg2-2.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:a81e3866f99382dfe8c15a151f1ca5fde5815fde879348fe5a9884a7c092a305"}, - {file = "psycopg2-2.9.3-cp37-cp37m-win32.whl", hash = "sha256:cb10d44e6694d763fa1078a26f7f6137d69f555a78ec85dc2ef716c37447e4b2"}, - {file = "psycopg2-2.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:4295093a6ae3434d33ec6baab4ca5512a5082cc43c0505293087b8a46d108461"}, - {file = "psycopg2-2.9.3-cp38-cp38-win32.whl", hash = "sha256:34b33e0162cfcaad151f249c2649fd1030010c16f4bbc40a604c1cb77173dcf7"}, - {file = "psycopg2-2.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:0762c27d018edbcb2d34d51596e4346c983bd27c330218c56c4dc25ef7e819bf"}, - {file = "psycopg2-2.9.3-cp39-cp39-win32.whl", hash = "sha256:8cf3878353cc04b053822896bc4922b194792df9df2f1ad8da01fb3043602126"}, - {file = "psycopg2-2.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:06f32425949bd5fe8f625c49f17ebb9784e1e4fe928b7cce72edc36fb68e4c0c"}, - {file = "psycopg2-2.9.3.tar.gz", hash = "sha256:8e841d1bf3434da985cc5ef13e6f75c8981ced601fd70cc6bf33351b91562981"}, -] -py = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] -pycodestyle = [ - {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, - {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, -] -pycparser = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, -] -pyflakes = [ - {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, - {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, -] -pygments = [ - {file = "Pygments-2.12.0-py3-none-any.whl", hash = "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"}, - {file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"}, -] -pyparsing = [ - {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, - {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, -] -pyreadline3 = [ - {file = "pyreadline3-3.4.1-py3-none-any.whl", hash = "sha256:b0efb6516fd4fb07b45949053826a62fa4cb353db5be2bbb4a7aa1fdd1e345fb"}, - {file = "pyreadline3-3.4.1.tar.gz", hash = "sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae"}, -] -pytest = [ - {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, - {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, -] -pytest-django = [ - {file = "pytest-django-4.5.2.tar.gz", hash = "sha256:d9076f759bb7c36939dbdd5ae6633c18edfc2902d1a69fdbefd2426b970ce6c2"}, - {file = "pytest_django-4.5.2-py3-none-any.whl", hash = "sha256:c60834861933773109334fe5a53e83d1ef4828f2203a1d6a0fa9972f4f75ab3e"}, -] -python-dateutil = [ - {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"}, -] -python-gflags = [ - {file = "python-gflags-3.1.2.tar.gz", hash = "sha256:40ae131e899ef68e9e14aa53ca063839c34f6a168afe622217b5b875492a1ee2"}, -] -pytz = [ - {file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"}, - {file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"}, -] -pyyaml = [ - {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, - {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, - {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, - {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, - {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, - {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, - {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, - {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, - {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, - {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, - {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, - {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, - {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, - {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, - {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, - {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, -] -redis = [ - {file = "redis-4.3.4-py3-none-any.whl", hash = "sha256:a52d5694c9eb4292770084fa8c863f79367ca19884b329ab574d5cb2036b3e54"}, - {file = "redis-4.3.4.tar.gz", hash = "sha256:ddf27071df4adf3821c4f2ca59d67525c3a82e5f268bed97b813cb4fabf87880"}, -] -requests = [ - {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, - {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, -] -requests-mock = [ - {file = "requests-mock-1.9.3.tar.gz", hash = "sha256:8d72abe54546c1fc9696fa1516672f1031d72a55a1d66c85184f972a24ba0eba"}, - {file = "requests_mock-1.9.3-py2.py3-none-any.whl", hash = "sha256:0a2d38a117c08bb78939ec163522976ad59a6b7fdd82b709e23bb98004a44970"}, -] -requests-oauthlib = [ - {file = "requests-oauthlib-1.3.1.tar.gz", hash = "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a"}, - {file = "requests_oauthlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5"}, -] -rq = [ - {file = "rq-1.11.0-py2.py3-none-any.whl", hash = "sha256:c0bbf898b56817da053cdc992ab46da2729a7ccd70dd50316ad9209b7d7b71c1"}, - {file = "rq-1.11.0.tar.gz", hash = "sha256:50d0cf687cfb2530eac9396c7426e420958a166e8f4666bd2096bdcf7f4ad03e"}, -] -six = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] -snowballstemmer = [ - {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, - {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, -] -soupsieve = [ - {file = "soupsieve-2.3.2.post1-py3-none-any.whl", hash = "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759"}, - {file = "soupsieve-2.3.2.post1.tar.gz", hash = "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"}, -] -sphinx = [ - {file = "Sphinx-5.1.1-py3-none-any.whl", hash = "sha256:309a8da80cb6da9f4713438e5b55861877d5d7976b69d87e336733637ea12693"}, - {file = "Sphinx-5.1.1.tar.gz", hash = "sha256:ba3224a4e206e1fbdecf98a4fae4992ef9b24b85ebf7b584bb340156eaf08d89"}, -] -sphinx-autobuild = [ - {file = "sphinx-autobuild-2021.3.14.tar.gz", hash = "sha256:de1ca3b66e271d2b5b5140c35034c89e47f263f2cd5db302c9217065f7443f05"}, - {file = "sphinx_autobuild-2021.3.14-py3-none-any.whl", hash = "sha256:8fe8cbfdb75db04475232f05187c776f46f6e9e04cacf1e49ce81bdac649ccac"}, -] -sphinx-basic-ng = [ - {file = "sphinx_basic_ng-0.0.1a12-py3-none-any.whl", hash = "sha256:e8b6efd2c5ece014156de76065eda01ddfca0fee465aa020b1e3c12f84570bbe"}, - {file = "sphinx_basic_ng-0.0.1a12.tar.gz", hash = "sha256:cffffb14914ddd26c94b1330df1d72dab5a42e220aaeb5953076a40b9c50e801"}, -] -sphinx-issues = [ - {file = "sphinx-issues-3.0.1.tar.gz", hash = "sha256:b7c1dc1f4808563c454d11c1112796f8c176cdecfee95f0fd2302ef98e21e3d6"}, - {file = "sphinx_issues-3.0.1-py3-none-any.whl", hash = "sha256:8b25dc0301159375468f563b3699af7a63720fd84caf81c1442036fcd418b20c"}, -] -sphinxcontrib-applehelp = [ - {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, - {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, -] -sphinxcontrib-devhelp = [ - {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, - {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, -] -sphinxcontrib-htmlhelp = [ - {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, - {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, -] -sphinxcontrib-jsmath = [ - {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, - {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, -] -sphinxcontrib-qthelp = [ - {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, - {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, -] -sphinxcontrib-serializinghtml = [ - {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, - {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, -] -sqlparse = [ - {file = "sqlparse-0.4.2-py3-none-any.whl", hash = "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"}, - {file = "sqlparse-0.4.2.tar.gz", hash = "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae"}, -] -tomli = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] -tornado = [ - {file = "tornado-6.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:20f638fd8cc85f3cbae3c732326e96addff0a15e22d80f049e00121651e82e72"}, - {file = "tornado-6.2-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:87dcafae3e884462f90c90ecc200defe5e580a7fbbb4365eda7c7c1eb809ebc9"}, - {file = "tornado-6.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba09ef14ca9893954244fd872798b4ccb2367c165946ce2dd7376aebdde8e3ac"}, - {file = "tornado-6.2-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8150f721c101abdef99073bf66d3903e292d851bee51910839831caba341a75"}, - {file = "tornado-6.2-cp37-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3a2f5999215a3a06a4fc218026cd84c61b8b2b40ac5296a6db1f1451ef04c1e"}, - {file = "tornado-6.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5f8c52d219d4995388119af7ccaa0bcec289535747620116a58d830e7c25d8a8"}, - {file = "tornado-6.2-cp37-abi3-musllinux_1_1_i686.whl", hash = "sha256:6fdfabffd8dfcb6cf887428849d30cf19a3ea34c2c248461e1f7d718ad30b66b"}, - {file = "tornado-6.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:1d54d13ab8414ed44de07efecb97d4ef7c39f7438cf5e976ccd356bebb1b5fca"}, - {file = "tornado-6.2-cp37-abi3-win32.whl", hash = "sha256:5c87076709343557ef8032934ce5f637dbb552efa7b21d08e89ae7619ed0eb23"}, - {file = "tornado-6.2-cp37-abi3-win_amd64.whl", hash = "sha256:e5f923aa6a47e133d1cf87d60700889d7eae68988704e20c75fb2d65677a8e4b"}, - {file = "tornado-6.2.tar.gz", hash = "sha256:9b630419bde84ec666bfd7ea0a4cb2a8a651c2d5cccdbdd1972a0c859dfc3c13"}, -] -urllib3 = [ - {file = "urllib3-1.26.11-py2.py3-none-any.whl", hash = "sha256:c33ccba33c819596124764c23a97d25f32b28433ba0dedeb77d873a38722c9bc"}, - {file = "urllib3-1.26.11.tar.gz", hash = "sha256:ea6e8fb210b19d950fab93b60c9009226c63a28808bc8386e05301e25883ac0a"}, -] -vcrpy = [ - {file = "vcrpy-4.2.0-py2.py3-none-any.whl", hash = "sha256:7ec280c8d5385652f1117fe32a200e6676614007d9f946af9f07df1e5f92254c"}, - {file = "vcrpy-4.2.0.tar.gz", hash = "sha256:94520b86fb765925adc8c77ff934e89a5e156c28e74a314320217ef1b454afe0"}, -] -whitenoise = [ - {file = "whitenoise-6.2.0-py3-none-any.whl", hash = "sha256:8e9c600a5c18bd17655ef668ad55b5edf6c24ce9bdca5bf607649ca4b1e8e2c2"}, - {file = "whitenoise-6.2.0.tar.gz", hash = "sha256:8fa943c6d4cd9e27673b70c21a07b0aa120873901e099cd46cab40f7cc96d567"}, -] -wrapt = [ - {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, - {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, - {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, - {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, - {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, - {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, - {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, - {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, - {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, - {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, - {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, - {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, - {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, - {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, - {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, - {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, -] -yarl = [ - {file = "yarl-1.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:abc06b97407868ef38f3d172762f4069323de52f2b70d133d096a48d72215d28"}, - {file = "yarl-1.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:07b21e274de4c637f3e3b7104694e53260b5fc10d51fb3ec5fed1da8e0f754e3"}, - {file = "yarl-1.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9de955d98e02fab288c7718662afb33aab64212ecb368c5dc866d9a57bf48880"}, - {file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ec362167e2c9fd178f82f252b6d97669d7245695dc057ee182118042026da40"}, - {file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:20df6ff4089bc86e4a66e3b1380460f864df3dd9dccaf88d6b3385d24405893b"}, - {file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5999c4662631cb798496535afbd837a102859568adc67d75d2045e31ec3ac497"}, - {file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed19b74e81b10b592084a5ad1e70f845f0aacb57577018d31de064e71ffa267a"}, - {file = "yarl-1.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e4808f996ca39a6463f45182e2af2fae55e2560be586d447ce8016f389f626f"}, - {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2d800b9c2eaf0684c08be5f50e52bfa2aa920e7163c2ea43f4f431e829b4f0fd"}, - {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6628d750041550c5d9da50bb40b5cf28a2e63b9388bac10fedd4f19236ef4957"}, - {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f5af52738e225fcc526ae64071b7e5342abe03f42e0e8918227b38c9aa711e28"}, - {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:76577f13333b4fe345c3704811ac7509b31499132ff0181f25ee26619de2c843"}, - {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0c03f456522d1ec815893d85fccb5def01ffaa74c1b16ff30f8aaa03eb21e453"}, - {file = "yarl-1.8.1-cp310-cp310-win32.whl", hash = "sha256:ea30a42dc94d42f2ba4d0f7c0ffb4f4f9baa1b23045910c0c32df9c9902cb272"}, - {file = "yarl-1.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:9130ddf1ae9978abe63808b6b60a897e41fccb834408cde79522feb37fb72fb0"}, - {file = "yarl-1.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0ab5a138211c1c366404d912824bdcf5545ccba5b3ff52c42c4af4cbdc2c5035"}, - {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0fb2cb4204ddb456a8e32381f9a90000429489a25f64e817e6ff94879d432fc"}, - {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:85cba594433915d5c9a0d14b24cfba0339f57a2fff203a5d4fd070e593307d0b"}, - {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ca7e596c55bd675432b11320b4eacc62310c2145d6801a1f8e9ad160685a231"}, - {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0f77539733e0ec2475ddcd4e26777d08996f8cd55d2aef82ec4d3896687abda"}, - {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29e256649f42771829974e742061c3501cc50cf16e63f91ed8d1bf98242e5507"}, - {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7fce6cbc6c170ede0221cc8c91b285f7f3c8b9fe28283b51885ff621bbe0f8ee"}, - {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:59ddd85a1214862ce7c7c66457f05543b6a275b70a65de366030d56159a979f0"}, - {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:12768232751689c1a89b0376a96a32bc7633c08da45ad985d0c49ede691f5c0d"}, - {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:b19255dde4b4f4c32e012038f2c169bb72e7f081552bea4641cab4d88bc409dd"}, - {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6c8148e0b52bf9535c40c48faebb00cb294ee577ca069d21bd5c48d302a83780"}, - {file = "yarl-1.8.1-cp37-cp37m-win32.whl", hash = "sha256:de839c3a1826a909fdbfe05f6fe2167c4ab033f1133757b5936efe2f84904c07"}, - {file = "yarl-1.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:dd032e8422a52e5a4860e062eb84ac94ea08861d334a4bcaf142a63ce8ad4802"}, - {file = "yarl-1.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:19cd801d6f983918a3f3a39f3a45b553c015c5aac92ccd1fac619bd74beece4a"}, - {file = "yarl-1.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6347f1a58e658b97b0a0d1ff7658a03cb79bdbda0331603bed24dd7054a6dea1"}, - {file = "yarl-1.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c0da7e44d0c9108d8b98469338705e07f4bb7dab96dbd8fa4e91b337db42548"}, - {file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5587bba41399854703212b87071c6d8638fa6e61656385875f8c6dff92b2e461"}, - {file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31a9a04ecccd6b03e2b0e12e82131f1488dea5555a13a4d32f064e22a6003cfe"}, - {file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:205904cffd69ae972a1707a1bd3ea7cded594b1d773a0ce66714edf17833cdae"}, - {file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea513a25976d21733bff523e0ca836ef1679630ef4ad22d46987d04b372d57fc"}, - {file = "yarl-1.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0b51530877d3ad7a8d47b2fff0c8df3b8f3b8deddf057379ba50b13df2a5eae"}, - {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d2b8f245dad9e331540c350285910b20dd913dc86d4ee410c11d48523c4fd546"}, - {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ab2a60d57ca88e1d4ca34a10e9fb4ab2ac5ad315543351de3a612bbb0560bead"}, - {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:449c957ffc6bc2309e1fbe67ab7d2c1efca89d3f4912baeb8ead207bb3cc1cd4"}, - {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a165442348c211b5dea67c0206fc61366212d7082ba8118c8c5c1c853ea4d82e"}, - {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b3ded839a5c5608eec8b6f9ae9a62cb22cd037ea97c627f38ae0841a48f09eae"}, - {file = "yarl-1.8.1-cp38-cp38-win32.whl", hash = "sha256:c1445a0c562ed561d06d8cbc5c8916c6008a31c60bc3655cdd2de1d3bf5174a0"}, - {file = "yarl-1.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:56c11efb0a89700987d05597b08a1efcd78d74c52febe530126785e1b1a285f4"}, - {file = "yarl-1.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e80ed5a9939ceb6fda42811542f31c8602be336b1fb977bccb012e83da7e4936"}, - {file = "yarl-1.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6afb336e23a793cd3b6476c30f030a0d4c7539cd81649683b5e0c1b0ab0bf350"}, - {file = "yarl-1.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4c322cbaa4ed78a8aac89b2174a6df398faf50e5fc12c4c191c40c59d5e28357"}, - {file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fae37373155f5ef9b403ab48af5136ae9851151f7aacd9926251ab26b953118b"}, - {file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5395da939ffa959974577eff2cbfc24b004a2fb6c346918f39966a5786874e54"}, - {file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:076eede537ab978b605f41db79a56cad2e7efeea2aa6e0fa8f05a26c24a034fb"}, - {file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d1a50e461615747dd93c099f297c1994d472b0f4d2db8a64e55b1edf704ec1c"}, - {file = "yarl-1.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7de89c8456525650ffa2bb56a3eee6af891e98f498babd43ae307bd42dca98f6"}, - {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4a88510731cd8d4befaba5fbd734a7dd914de5ab8132a5b3dde0bbd6c9476c64"}, - {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2d93a049d29df172f48bcb09acf9226318e712ce67374f893b460b42cc1380ae"}, - {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:21ac44b763e0eec15746a3d440f5e09ad2ecc8b5f6dcd3ea8cb4773d6d4703e3"}, - {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:d0272228fabe78ce00a3365ffffd6f643f57a91043e119c289aaba202f4095b0"}, - {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:99449cd5366fe4608e7226c6cae80873296dfa0cde45d9b498fefa1de315a09e"}, - {file = "yarl-1.8.1-cp39-cp39-win32.whl", hash = "sha256:8b0af1cf36b93cee99a31a545fe91d08223e64390c5ecc5e94c39511832a4bb6"}, - {file = "yarl-1.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:de49d77e968de6626ba7ef4472323f9d2e5a56c1d85b7c0e2a190b2173d3b9be"}, - {file = "yarl-1.8.1.tar.gz", hash = "sha256:af887845b8c2e060eb5605ff72b6f2dd2aab7a761379373fd89d314f4752abbf"}, -] -"zope.event" = [ - {file = "zope.event-4.5.0-py2.py3-none-any.whl", hash = "sha256:2666401939cdaa5f4e0c08cf7f20c9b21423b95e88f4675b1443973bdb080c42"}, - {file = "zope.event-4.5.0.tar.gz", hash = "sha256:5e76517f5b9b119acf37ca8819781db6c16ea433f7e2062c4afc2b6fbedb1330"}, -] -"zope.interface" = [ - {file = "zope.interface-5.4.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:7df1e1c05304f26faa49fa752a8c690126cf98b40b91d54e6e9cc3b7d6ffe8b7"}, - {file = "zope.interface-5.4.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2c98384b254b37ce50eddd55db8d381a5c53b4c10ee66e1e7fe749824f894021"}, - {file = "zope.interface-5.4.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:08f9636e99a9d5410181ba0729e0408d3d8748026ea938f3b970a0249daa8192"}, - {file = "zope.interface-5.4.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:0ea1d73b7c9dcbc5080bb8aaffb776f1c68e807767069b9ccdd06f27a161914a"}, - {file = "zope.interface-5.4.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:273f158fabc5ea33cbc936da0ab3d4ba80ede5351babc4f577d768e057651531"}, - {file = "zope.interface-5.4.0-cp27-cp27m-win32.whl", hash = "sha256:a1e6e96217a0f72e2b8629e271e1b280c6fa3fe6e59fa8f6701bec14e3354325"}, - {file = "zope.interface-5.4.0-cp27-cp27m-win_amd64.whl", hash = "sha256:877473e675fdcc113c138813a5dd440da0769a2d81f4d86614e5d62b69497155"}, - {file = "zope.interface-5.4.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f7ee479e96f7ee350db1cf24afa5685a5899e2b34992fb99e1f7c1b0b758d263"}, - {file = "zope.interface-5.4.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:b0297b1e05fd128d26cc2460c810d42e205d16d76799526dfa8c8ccd50e74959"}, - {file = "zope.interface-5.4.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:af310ec8335016b5e52cae60cda4a4f2a60a788cbb949a4fbea13d441aa5a09e"}, - {file = "zope.interface-5.4.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:9a9845c4c6bb56e508651f005c4aeb0404e518c6f000d5a1123ab077ab769f5c"}, - {file = "zope.interface-5.4.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0b465ae0962d49c68aa9733ba92a001b2a0933c317780435f00be7ecb959c702"}, - {file = "zope.interface-5.4.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:5dd9ca406499444f4c8299f803d4a14edf7890ecc595c8b1c7115c2342cadc5f"}, - {file = "zope.interface-5.4.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:469e2407e0fe9880ac690a3666f03eb4c3c444411a5a5fddfdabc5d184a79f05"}, - {file = "zope.interface-5.4.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:52de7fc6c21b419078008f697fd4103dbc763288b1406b4562554bd47514c004"}, - {file = "zope.interface-5.4.0-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:3dd4952748521205697bc2802e4afac5ed4b02909bb799ba1fe239f77fd4e117"}, - {file = "zope.interface-5.4.0-cp35-cp35m-win32.whl", hash = "sha256:dd93ea5c0c7f3e25335ab7d22a507b1dc43976e1345508f845efc573d3d779d8"}, - {file = "zope.interface-5.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:3748fac0d0f6a304e674955ab1365d515993b3a0a865e16a11ec9d86fb307f63"}, - {file = "zope.interface-5.4.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:66c0061c91b3b9cf542131148ef7ecbecb2690d48d1612ec386de9d36766058f"}, - {file = "zope.interface-5.4.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:d0c1bc2fa9a7285719e5678584f6b92572a5b639d0e471bb8d4b650a1a910920"}, - {file = "zope.interface-5.4.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2876246527c91e101184f63ccd1d716ec9c46519cc5f3d5375a3351c46467c46"}, - {file = "zope.interface-5.4.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:334701327f37c47fa628fc8b8d28c7d7730ce7daaf4bda1efb741679c2b087fc"}, - {file = "zope.interface-5.4.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:71aace0c42d53abe6fc7f726c5d3b60d90f3c5c055a447950ad6ea9cec2e37d9"}, - {file = "zope.interface-5.4.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:5bb3489b4558e49ad2c5118137cfeaf59434f9737fa9c5deefc72d22c23822e2"}, - {file = "zope.interface-5.4.0-cp36-cp36m-win32.whl", hash = "sha256:1c0e316c9add0db48a5b703833881351444398b04111188069a26a61cfb4df78"}, - {file = "zope.interface-5.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f0c02cbb9691b7c91d5009108f975f8ffeab5dff8f26d62e21c493060eff2a1"}, - {file = "zope.interface-5.4.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:7d97a4306898b05404a0dcdc32d9709b7d8832c0c542b861d9a826301719794e"}, - {file = "zope.interface-5.4.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:867a5ad16892bf20e6c4ea2aab1971f45645ff3102ad29bd84c86027fa99997b"}, - {file = "zope.interface-5.4.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5f931a1c21dfa7a9c573ec1f50a31135ccce84e32507c54e1ea404894c5eb96f"}, - {file = "zope.interface-5.4.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:194d0bcb1374ac3e1e023961610dc8f2c78a0f5f634d0c737691e215569e640d"}, - {file = "zope.interface-5.4.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:8270252effc60b9642b423189a2fe90eb6b59e87cbee54549db3f5562ff8d1b8"}, - {file = "zope.interface-5.4.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:15e7d1f7a6ee16572e21e3576d2012b2778cbacf75eb4b7400be37455f5ca8bf"}, - {file = "zope.interface-5.4.0-cp37-cp37m-win32.whl", hash = "sha256:8892f89999ffd992208754851e5a052f6b5db70a1e3f7d54b17c5211e37a98c7"}, - {file = "zope.interface-5.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2e5a26f16503be6c826abca904e45f1a44ff275fdb7e9d1b75c10671c26f8b94"}, - {file = "zope.interface-5.4.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:0f91b5b948686659a8e28b728ff5e74b1be6bf40cb04704453617e5f1e945ef3"}, - {file = "zope.interface-5.4.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:4de4bc9b6d35c5af65b454d3e9bc98c50eb3960d5a3762c9438df57427134b8e"}, - {file = "zope.interface-5.4.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bf68f4b2b6683e52bec69273562df15af352e5ed25d1b6641e7efddc5951d1a7"}, - {file = "zope.interface-5.4.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:63b82bb63de7c821428d513607e84c6d97d58afd1fe2eb645030bdc185440120"}, - {file = "zope.interface-5.4.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:db1fa631737dab9fa0b37f3979d8d2631e348c3b4e8325d6873c2541d0ae5a48"}, - {file = "zope.interface-5.4.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:f44e517131a98f7a76696a7b21b164bcb85291cee106a23beccce454e1f433a4"}, - {file = "zope.interface-5.4.0-cp38-cp38-win32.whl", hash = "sha256:a9506a7e80bcf6eacfff7f804c0ad5350c8c95b9010e4356a4b36f5322f09abb"}, - {file = "zope.interface-5.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:3c02411a3b62668200910090a0dff17c0b25aaa36145082a5a6adf08fa281e54"}, - {file = "zope.interface-5.4.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:0cee5187b60ed26d56eb2960136288ce91bcf61e2a9405660d271d1f122a69a4"}, - {file = "zope.interface-5.4.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:a8156e6a7f5e2a0ff0c5b21d6bcb45145efece1909efcbbbf48c56f8da68221d"}, - {file = "zope.interface-5.4.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:205e40ccde0f37496904572035deea747390a8b7dc65146d30b96e2dd1359a83"}, - {file = "zope.interface-5.4.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:3f24df7124c323fceb53ff6168da70dbfbae1442b4f3da439cd441681f54fe25"}, - {file = "zope.interface-5.4.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:5208ebd5152e040640518a77827bdfcc73773a15a33d6644015b763b9c9febc1"}, - {file = "zope.interface-5.4.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:17776ecd3a1fdd2b2cd5373e5ef8b307162f581c693575ec62e7c5399d80794c"}, - {file = "zope.interface-5.4.0-cp39-cp39-win32.whl", hash = "sha256:d4d9d6c1a455d4babd320203b918ccc7fcbefe308615c521062bc2ba1aa4d26e"}, - {file = "zope.interface-5.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:0cba8477e300d64a11a9789ed40ee8932b59f9ee05f85276dbb4b59acee5dd09"}, - {file = "zope.interface-5.4.0.tar.gz", hash = "sha256:5dba5f530fec3f0988d83b78cc591b58c0b6eb8431a85edd1569a0539a8a5a0e"}, -] diff --git a/pykeg/api/auth.py b/pykeg/api/auth.py index f197a9554..bbbe9cf33 100644 --- a/pykeg/api/auth.py +++ b/pykeg/api/auth.py @@ -1,13 +1,10 @@ -import logging -import re - from rest_framework import authentication from rest_framework.exceptions import AuthenticationFailed from pykeg.core import models -class ApiKeyAuthHolder(object): +class ApiKeyAuthHolder: """DRF auth holder for our api keys.""" def __init__(self, api_key): diff --git a/pykeg/api/models.py b/pykeg/api/models.py index 71a836239..6b2021999 100644 --- a/pykeg/api/models.py +++ b/pykeg/api/models.py @@ -1,3 +1 @@ -from django.db import models - # Create your models here. diff --git a/pykeg/api/tests.py b/pykeg/api/tests.py index a3e4414b2..d2b4b1fba 100644 --- a/pykeg/api/tests.py +++ b/pykeg/api/tests.py @@ -4,6 +4,7 @@ from rest_framework.test import APIClient from pykeg.core import models +from pykeg.core.util import get_version class ApiClient: @@ -35,6 +36,10 @@ class V2ApiTestCase(TestCase): def setUp(self): self.client = ApiClient() self.site = models.KegbotSite.objects.all().first() + # The fixture bakes in an older server_version; keep it current so the + # "upgrade required" gate doesn't intercept API requests under test. + self.site.server_version = get_version() + self.site.save() self.user = models.User.objects.all().first() self.api_key = models.ApiKey.objects.get_or_create(user=self.user)[0] diff --git a/pykeg/backup/backup.py b/pykeg/backup/backup.py index 5d542b6bb..77bdec7cf 100644 --- a/pykeg/backup/backup.py +++ b/pykeg/backup/backup.py @@ -34,6 +34,7 @@ import isodate from django.conf import settings from django.core.files.storage import default_storage +from django.core.management import call_command from django.db import connection, transaction from django.utils import timezone from django.utils.text import slugify @@ -41,6 +42,7 @@ from pykeg.core.util import get_version from pykeg.util import kbjson +from . import legacy from .exceptions import AlreadyInstalledError, BackupError, InvalidBackup logger = logging.getLogger(__name__) @@ -123,13 +125,13 @@ def add_files(storage, dirname, destdir): output_dirname = os.path.dirname(output_filename) if not os.path.exists(output_dirname): os.makedirs(output_dirname) - with storage.open(full_filename, "r") as srcfile: - with open(output_filename, "w") as dstfile: - logger.debug("+++ Creating {}".format(output_filename)) + with storage.open(full_filename, "rb") as srcfile: + with open(output_filename, "wb") as dstfile: + logger.debug(f"+++ Creating {output_filename}") shutil.copyfileobj(srcfile, dstfile) metadata[META_NUM_MEDIA_FILES] += 1 for subdir in subdirs: - add_files(storage, os.path.join((dirname, subdir)), destdir) + add_files(storage, os.path.join(dirname, subdir), destdir) if include_media: destdir = os.path.join(backup_dir, "media") @@ -162,7 +164,7 @@ def add_files(storage, dirname, destdir): def create_backup_zip(backup_dir, backup_name): """Creates a zipfile based on a tree created with `create_backup_tree()`.""" zipfile_fileno, zipfile_path = tempfile.mkstemp() - zipfile_fd = os.fdopen(zipfile_fileno, "w") + zipfile_fd = os.fdopen(zipfile_fileno, "wb") zf = zipfile.ZipFile(zipfile_fd, "w") for dirname, subdirs, files in os.walk(backup_dir): @@ -171,7 +173,7 @@ def create_backup_zip(backup_dir, backup_name): rel_path = os.path.relpath(full_path, backup_dir) archive_path = os.path.join(backup_name, rel_path) zf.write(full_path, archive_path) - logger.debug("Added to zip: {}".format(archive_path)) + logger.debug(f"Added to zip: {archive_path}") zf.close() return zipfile_path @@ -186,7 +188,7 @@ def backup(storage=default_storage, include_media=True): date = timezone.now() site_slug = slugify(get_title()) date_str = date.strftime("%Y%m%d-%H%M%S") - backup_name = "{slug}-{date}".format(slug=site_slug, date=date_str) + backup_name = f"{site_slug}-{date_str}" backup_dir = create_backup_tree(date=date, storage=storage, include_media=include_media) try: @@ -198,8 +200,8 @@ def backup(storage=default_storage, include_media=True): for chunk in iter(lambda: f.read(2**20), b""): sha1.update(chunk) digest = sha1.hexdigest() - saved_zip_name = os.path.join(BACKUPS_DIRNAME, "{}-{}.zip".format(backup_name, digest)) - with open(temp_zip, "r") as temp_zip_file: + saved_zip_name = os.path.join(BACKUPS_DIRNAME, f"{backup_name}-{digest}.zip") + with open(temp_zip, "rb") as temp_zip_file: ret = storage.save(saved_zip_name, temp_zip_file) return ret finally: @@ -211,7 +213,7 @@ def backup(storage=default_storage, include_media=True): def extract_backup(backup_file): assert sys.version_info[:3] >= (2, 7, 4), "Unsafe to extract ZIPs in this Python version" backup_dir = tempfile.mkdtemp() - logger.debug("Extracting backup to {}".format(backup_dir)) + logger.debug(f"Extracting backup to {backup_dir}") zf = zipfile.ZipFile(backup_file, mode="r") zf.extractall(path=backup_dir) return backup_dir @@ -222,17 +224,24 @@ def verify_backup_directory(backup_dir): if not os.path.exists(metadata_file): raise InvalidBackup("Metadata file does not exist") - if not os.path.exists(metadata_file): + if not os.path.exists(os.path.join(backup_dir, SQL_FILENAME)): raise InvalidBackup("SQL dumpfile does not exist") - metadata = kbjson.loads(open(metadata_file, "r").read()) + metadata = kbjson.loads(open(metadata_file).read()) format = metadata.get(META_BACKUP_FORMAT) if format != BACKUP_FORMAT: - raise InvalidBackup("Unsupported backup format: {}".format(format)) + raise InvalidBackup(f"Unsupported backup format: {format}") return metadata +def read_metadata_from_directory(backup_dir): + metadata_file = os.path.join(backup_dir, METADATA_FILENAME) + if not os.path.exists(metadata_file): + raise InvalidBackup("Metadata file does not exist") + return kbjson.loads(open(metadata_file).read()) + + def restore_media(backup_dir, storage): media_dir = os.path.join(backup_dir, "media") @@ -240,13 +249,22 @@ def restore_media(backup_dir, storage): for filename in files: full_path = os.path.join(dirname, filename) rel_path = os.path.relpath(full_path, media_dir) - with open(full_path, "r") as data: - logger.debug("+++ Restoring file {}".format(rel_path)) + with open(full_path, "rb") as data: + logger.debug(f"+++ Restoring file {rel_path}") storage.save(rel_path, data) def restore_from_directory(backup_dir, storage=default_storage): - logger.info("Restoring from {} ...".format(backup_dir)) + logger.info(f"Restoring from {backup_dir} ...") + + if read_metadata_from_directory(backup_dir).get(META_BACKUP_FORMAT) == 1: + # Legacy (v1.1.x) backups have no SQL dump; data is restored + # through the ORM into a freshly migrated database. + call_command("migrate", interactive=False, verbosity=0) + legacy.restore_data(backup_dir) + restore_media(backup_dir, storage) + logger.info("Restore completed successfully.") + return if db_impl.is_installed(): raise AlreadyInstalledError("You must erase this system before restoring.") @@ -255,12 +273,10 @@ def restore_from_directory(backup_dir, storage=default_storage): current_engine = db_impl.engine_name() saved_engine = metadata[META_DB_ENGINE] if current_engine != saved_engine: - raise BackupError( - "Current DB is {}; cannot restore from {}".format(current_engine, db_impl) - ) + raise BackupError(f"Current DB is {current_engine}; cannot restore from {db_impl}") input_filename = os.path.join(backup_dir, SQL_FILENAME) - with open(input_filename, "r") as in_fd: + with open(input_filename) as in_fd: db_impl.restore(in_fd) restore_media(backup_dir, storage) @@ -277,7 +293,7 @@ def restore(backup_file): restore_from_directory(backup_file) return - logger.info("Restoring from {} ...".format(backup_file)) + logger.info(f"Restoring from {backup_file} ...") unzipped_dir = extract_backup(backup_file) try: dirs = os.listdir(unzipped_dir) @@ -298,7 +314,7 @@ def delete_files(dirname): subdirs, files = storage.listdir(dirname) for filename in files: full_name = os.path.join(dirname, filename) - logger.debug("Deleting file: {}".format(full_name)) + logger.debug(f"Deleting file: {full_name}") storage.delete(full_name) for subdir in subdirs: delete_files(dirname) diff --git a/pykeg/backup/backup_test.py b/pykeg/backup/backup_test.py index 39052ce0a..7dcedab37 100644 --- a/pykeg/backup/backup_test.py +++ b/pykeg/backup/backup_test.py @@ -124,6 +124,6 @@ def recursive_diff(self, dir1, dir2): f1 = open(f1_full).read() f2 = open(f2_full).read() if f1 != f2: - message = 'Files not equal: "{}" and "{}" differ.'.format(f1_full, f2_full) + message = f'Files not equal: "{f1_full}" and "{f2_full}" differ.' message += "\n" + "".join(difflib.ndiff(f1.splitlines(True), f2.splitlines(True))) self.fail(message) diff --git a/pykeg/backup/legacy.py b/pykeg/backup/legacy.py new file mode 100644 index 000000000..72c77aae5 --- /dev/null +++ b/pykeg/backup/legacy.py @@ -0,0 +1,149 @@ +"""Restore support for legacy format-1 backups (Kegbot v1.1.x). + +Format-1 backups store per-table Django fixture JSON under `tables/` +rather than a SQL dump. The v1.1 restore procedure loaded those fixtures +through the ORM into a freshly-migrated database, so the modern +equivalent does the same: migrate to the current schema, transform the +fixtures across the (small) v1.1 -> v1.2/1.3 model changes, and load +them with `loaddata`. + +Because data lands directly in the current schema, a completed legacy +restore needs no separate `kegbot upgrade`: the stored server version is +set to the current version and stats are regenerated as a final step. +""" + +import glob +import json +import logging +import os +import tempfile + +from django.apps import apps +from django.core.management import call_command +from django.core.management.color import no_style +from django.db import connection + +from .exceptions import InvalidBackup + +logger = logging.getLogger(__name__) + +LEGACY_BACKUP_FORMAT = 1 +TABLES_DIRNAME = "tables" + +# Tables that existed in v1.1 but have no modern equivalent. +IGNORED_TABLES = {"south_migrationhistory"} + + +def load_table_dumps(backup_dir): + """Reads all fixture files from a format-1 backup's `tables/` dir.""" + tables_dir = os.path.join(backup_dir, TABLES_DIRNAME) + if not os.path.isdir(tables_dir): + raise InvalidBackup("Backup does not contain a tables/ directory") + + tables = {} + for path in sorted(glob.glob(os.path.join(tables_dir, "*.json"))): + name = os.path.basename(path)[: -len(".json")] + with open(path) as f: + tables[name] = json.load(f) + return tables + + +def transform_tables(tables): + """Transforms v1.1-era fixtures for the current models. + + Applies the same changes the core 0002-0004 migrations performed on + live data: + + - keg: finished/online -> status ("on_tap" when a tap's current_keg + points at the keg, else "finished" or "available") + - kegtap: description -> notes + - kegbotsite: drop check_for_updates + - drinkingsession: timezone <- site timezone + + Returns a flat list of serialized objects, ready for `loaddata`. + """ + tables = {k: v for k, v in tables.items() if k not in IGNORED_TABLES} + + site_tz = "UTC" + for row in tables.get("core_kegbotsite", []): + fields = row["fields"] + site_tz = fields.get("timezone", site_tz) + fields.pop("check_for_updates", None) + + on_tap_kegs = { + row["fields"]["current_keg"] + for row in tables.get("core_kegtap", []) + if row["fields"].get("current_keg") + } + for row in tables.get("core_keg", []): + fields = row["fields"] + finished = fields.pop("finished", False) + fields.pop("online", None) + if row["pk"] in on_tap_kegs: + fields["status"] = "on_tap" + elif finished: + fields["status"] = "finished" + else: + fields["status"] = "available" + + for row in tables.get("core_kegtap", []): + fields = row["fields"] + if "description" in fields: + fields["notes"] = fields.pop("description") + + for row in tables.get("core_drinkingsession", []): + row["fields"].setdefault("timezone", site_tz) + + objects = [] + for rows in tables.values(): + objects.extend(rows) + return objects + + +def load_data(objects): + """Loads transformed fixture objects into the current database.""" + # loaddata (rather than direct deserialization) so foreign key checks + # are deferred for the duration of the load. + with tempfile.TemporaryDirectory() as tmpdir: + fixture_path = os.path.join(tmpdir, "legacy_backup.json") + with open(fixture_path, "w") as f: + json.dump(objects, f) + call_command("loaddata", fixture_path, verbosity=0) + + # Rows were inserted with explicit pks; realign auto-increment + # sequences (needed on postgres; a no-op elsewhere). Only the loaded + # models: other apps may have models without tables. + loaded_models = {apps.get_model(o["model"]) for o in objects} + sequence_sql = connection.ops.sequence_reset_sql(no_style(), sorted(loaded_models, key=str)) + if sequence_sql: + with connection.cursor() as cursor: + for statement in sequence_sql: + cursor.execute(statement) + + +def restore_data(backup_dir): + """Restores a format-1 backup's data into a migrated, empty database. + + The caller is responsible for schema (migrations) and media. + """ + from pykeg.core import models, stats + from pykeg.core.util import get_version + + from .exceptions import AlreadyInstalledError + + if models.KegbotSite.objects.exists(): + raise AlreadyInstalledError("You must erase this system before restoring.") + + tables = load_table_dumps(backup_dir) + objects = transform_tables(tables) + logger.info(f"Loading {len(objects)} records from legacy backup ...") + load_data(objects) + + # Data now lives in the current schema; no separate upgrade step remains. + site = models.KegbotSite.get() + site.server_version = get_version() + site.save(update_fields=["server_version"]) + + logger.info("Regenerating stats ...") + stats.invalidate_all() + stats.rebuild_from_id(0) diff --git a/pykeg/backup/legacy_test.py b/pykeg/backup/legacy_test.py new file mode 100644 index 000000000..76fe85b32 --- /dev/null +++ b/pykeg/backup/legacy_test.py @@ -0,0 +1,165 @@ +"""Tests for legacy (format-1) backup restore.""" + +import json +import os +import shutil +import tempfile +import unittest + +from django.apps import apps +from django.core import serializers +from django.core.files.storage import FileSystemStorage +from django.core.management import call_command +from django.test import TransactionTestCase + +from pykeg.backup import backup, legacy +from pykeg.core import defaults, models +from pykeg.core.util import get_version + +METER_NAME = "kegboard.flow0" +FAKE_BEER_NAME = "Testy Beer" +FAKE_BREWER_NAME = "Unittest Brewery" +FAKE_BEER_STYLE = "Test-Driven Pale Ale" + +MEDIA_CONTENTS = b"\xff\xd8\xff fake jpeg bytes" + + +class TransformTablesTestCase(unittest.TestCase): + def test_keg_status(self): + tables = { + "core_kegtap": [{"pk": 1, "model": "core.kegtap", "fields": {"current_keg": 2}}], + "core_keg": [ + {"pk": 1, "model": "core.keg", "fields": {"finished": True, "online": False}}, + {"pk": 2, "model": "core.keg", "fields": {"finished": False, "online": True}}, + {"pk": 3, "model": "core.keg", "fields": {"finished": False, "online": False}}, + ], + } + objects = legacy.transform_tables(tables) + statuses = {o["pk"]: o["fields"]["status"] for o in objects if o["model"] == "core.keg"} + self.assertEqual({1: "finished", 2: "on_tap", 3: "available"}, statuses) + for o in objects: + self.assertNotIn("finished", o["fields"]) + self.assertNotIn("online", o["fields"]) + + def test_kegtap_description_renamed(self): + tables = { + "core_kegtap": [ + {"pk": 1, "model": "core.kegtap", "fields": {"description": "spare tap"}} + ] + } + (tap,) = legacy.transform_tables(tables) + self.assertEqual("spare tap", tap["fields"]["notes"]) + self.assertNotIn("description", tap["fields"]) + + def test_site_and_session_fields(self): + tables = { + "core_kegbotsite": [ + { + "pk": 1, + "model": "core.kegbotsite", + "fields": {"check_for_updates": True, "timezone": "US/Pacific"}, + } + ], + "core_drinkingsession": [ + {"pk": 1, "model": "core.drinkingsession", "fields": {}}, + ], + "south_migrationhistory": [ + {"pk": 1, "model": "south.migrationhistory", "fields": {}}, + ], + } + objects = legacy.transform_tables(tables) + by_model = {o["model"]: o for o in objects} + self.assertNotIn("south.migrationhistory", by_model) + self.assertNotIn("check_for_updates", by_model["core.kegbotsite"]["fields"]) + self.assertEqual("US/Pacific", by_model["core.drinkingsession"]["fields"]["timezone"]) + + +class LegacyRestoreTestCase(TransactionTestCase): + def setUp(self): + defaults.set_defaults(set_is_setup=True, create_controller=True) + models.Keg.start_keg( + METER_NAME, + beverage_name=FAKE_BEER_NAME, + beverage_type="beer", + producer_name=FAKE_BREWER_NAME, + style_name=FAKE_BEER_STYLE, + ) + models.Drink.record_drink(METER_NAME, ticks=2200, volume_ml=1000) + models.Drink.record_drink(METER_NAME, ticks=1100, volume_ml=500) + + self.backup_dir = tempfile.mkdtemp() + self.media_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, self.backup_dir) + self.addCleanup(shutil.rmtree, self.media_dir) + + def build_format1_backup(self): + """Serializes current site data devolved to v1.1 fixture shape.""" + tables = {} + for model in apps.get_app_config("core").get_models(): + rows = json.loads(serializers.serialize("json", model.objects.all())) + if rows: + tables[model._meta.db_table] = rows + + for row in tables["core_kegbotsite"]: + row["fields"]["check_for_updates"] = True + row["fields"]["server_version"] = "1.1" + for row in tables["core_kegtap"]: + row["fields"]["description"] = row["fields"].pop("notes", "") + for row in tables["core_keg"]: + status = row["fields"].pop("status") + row["fields"]["finished"] = status == "finished" + row["fields"]["online"] = status == "on_tap" + for row in tables.get("core_drinkingsession", []): + row["fields"].pop("timezone", None) + + tables_dir = os.path.join(self.backup_dir, legacy.TABLES_DIRNAME) + os.makedirs(tables_dir) + for name, rows in tables.items(): + with open(os.path.join(tables_dir, f"{name}.json"), "w") as f: + json.dump(rows, f) + + with open(os.path.join(self.backup_dir, "metadata.json"), "w") as f: + json.dump({"backup_format": 1, "server_version": "1.1.0"}, f) + + pics_dir = os.path.join(self.backup_dir, "media", "pics") + os.makedirs(pics_dir) + with open(os.path.join(pics_dir, "test.jpg"), "wb") as f: + f.write(MEDIA_CONTENTS) + + return tables + + def test_restore(self): + tables = self.build_format1_backup() + expected_counts = {name: len(rows) for name, rows in tables.items()} + + call_command("flush", interactive=False, verbosity=0) + self.assertFalse(models.KegbotSite.objects.exists()) + + storage = FileSystemStorage(location=self.media_dir) + backup.restore_from_directory(self.backup_dir, storage=storage) + + site = models.KegbotSite.get() + self.assertEqual(get_version(), site.server_version) + + for model in apps.get_app_config("core").get_models(): + expected = expected_counts.get(model._meta.db_table, 0) + self.assertEqual( + expected, model.objects.count(), f"count mismatch for {model._meta.db_table}" + ) + + keg = models.Keg.objects.get() + self.assertEqual(models.Keg.STATUS_ON_TAP, keg.status) + self.assertEqual(2, models.Drink.objects.count()) + + with storage.open("pics/test.jpg") as f: + self.assertEqual(MEDIA_CONTENTS, f.read()) + + # Sequences realigned: creating new records must not collide. + models.Drink.record_drink(METER_NAME, ticks=1100, volume_ml=500) + self.assertEqual(3, models.Drink.objects.count()) + + def test_restore_requires_empty_site(self): + self.build_format1_backup() + storage = FileSystemStorage(location=self.media_dir) + with self.assertRaises(backup.AlreadyInstalledError): + backup.restore_from_directory(self.backup_dir, storage=storage) diff --git a/pykeg/backup/mysql.py b/pykeg/backup/mysql.py index 42eb13b23..cf8b79b94 100644 --- a/pykeg/backup/mysql.py +++ b/pykeg/backup/mysql.py @@ -2,7 +2,6 @@ import logging import subprocess -from builtins import str from django.apps import apps from django.conf import settings @@ -30,6 +29,13 @@ if PARAMS.get("port"): DEFAULT_ARGS.append("--port={}".format(PARAMS["port"])) +# MariaDB 11.4+ clients verify server certificates by default, which fails +# against the self-signed certs MySQL servers auto-generate. Keep TLS but +# skip verification, matching how the Django connection behaves. The +# "loose-" prefix makes clients without this option (Oracle mysql) warn +# instead of exit. +DEFAULT_ARGS.append("--loose-ssl-verify-server-cert=0") + def engine_name(): return "mysql" @@ -40,23 +46,14 @@ def is_installed(): args += ["-e", "'show tables like \"core_kegbotsite\";'"] cmd = " ".join(args) - logger.info("command: {}".format(cmd)) - output = subprocess.check_output(cmd, shell=True) - logger.info("result: {}".format(output)) + logger.info(f"command: {cmd}") + output = subprocess.check_output(cmd, shell=True, text=True) + logger.info(f"result: {output}") return "core_kegbotsite" in output def dump(output_fd): - args = ["mysqldump", "--skip-dump-date", "--single-transaction"] - if PARAMS.get("user"): - args.append("--user={}".format(PARAMS["user"])) - if PARAMS.get("password"): - args.append("--password={}".format(PARAMS["password"])) - if PARAMS.get("host"): - args.append("--host={}".format(PARAMS["host"])) - if PARAMS.get("port"): - args.append("--port={}".format(PARAMS["port"])) - + args = ["mysqldump", "--skip-dump-date", "--single-transaction"] + DEFAULT_ARGS args.append(PARAMS["db"]) cmd = " ".join(args) logger.info(cmd) @@ -73,24 +70,14 @@ def restore(input_fd): def erase(): - args = ["mysql"] - if PARAMS.get("user"): - args.append("--user={}".format(PARAMS["user"])) - if PARAMS.get("password"): - args.append("--password={}".format(PARAMS["password"])) - if PARAMS.get("host"): - args.append("--host={}".format(PARAMS["host"])) - if PARAMS.get("port"): - args.append("--port={}".format(PARAMS["port"])) - - args += [PARAMS["db"]] + args = ["mysql"] + DEFAULT_ARGS + [PARAMS["db"]] # Build the sql command. tables = [str(model._meta.db_table) for model in apps.get_models()] - query = ["DROP TABLE IF EXISTS {};".format(t) for t in tables] + query = [f"DROP TABLE IF EXISTS {t};" for t in tables] query = ["SET FOREIGN_KEY_CHECKS=0;"] + query + ["SET FOREIGN_KEY_CHECKS=1;"] query = " ".join(query) - cmd = " ".join(args + ["-e", "'{}'".format(query)]) + cmd = " ".join(args + ["-e", f"'{query}'"]) logger.info(cmd) subprocess.check_call(cmd, shell=True) diff --git a/pykeg/backup/postgres.py b/pykeg/backup/postgres.py index de387e99c..355877ed5 100644 --- a/pykeg/backup/postgres.py +++ b/pykeg/backup/postgres.py @@ -41,7 +41,7 @@ def is_installed(): args += ["-qt", "-c \"select * from pg_tables where schemaname='public';\"", PARAMS["db"]] cmd = " ".join(args) logger.info(cmd) - output = subprocess.check_output(cmd, env=DEFAULT_ENV, shell=True) + output = subprocess.check_output(cmd, env=DEFAULT_ENV, shell=True, text=True) return "core_" in output @@ -58,7 +58,7 @@ def restore(input_fd): args.append(PARAMS["db"]) cmd = " ".join(args) logger.info(cmd) - return subprocess.check_call(cmd, stdin=input_fd, shell=True) + return subprocess.check_call(cmd, stdin=input_fd, env=DEFAULT_ENV, shell=True) def erase(): @@ -66,4 +66,4 @@ def erase(): args += [PARAMS["db"], "-c 'drop schema public cascade; create schema public;'"] cmd = " ".join(args) logger.info(cmd) - subprocess.check_call(cmd, shell=True) + subprocess.check_call(cmd, env=DEFAULT_ENV, shell=True) diff --git a/pykeg/config.py b/pykeg/config.py index 2063c2af0..c18c0c543 100644 --- a/pykeg/config.py +++ b/pykeg/config.py @@ -34,7 +34,7 @@ def boolstr(val): def uristr(val): parsed = urllib.parse.urlparse(val) if not parsed.scheme: - raise ValueError(f"not a URI") + raise ValueError("not a URI") return val @@ -53,7 +53,7 @@ def define_setting(name, typefn=str, *, default, required): required: true if the setting _must_ be present in env """ if name in ALL_SETTINGS: - raise ValueError("Bug: Setting {} registered more than once.".format(name)) + raise ValueError(f"Bug: Setting {name} registered more than once.") ALL_SETTINGS[name] = (default, required, typefn) @@ -123,7 +123,9 @@ def validate(exit_on_error=True): "REDIS_URL", default=os.getenv("REDIS_URL", "redis://localhost:6379/0"), typefn=uristr, - required=True, + # Optional under pytest so the suite can fall back to an in-process + # fakeredis (see settings.py) with no redis server; required otherwise. + required=not IS_RUNNING_PYTEST, ) define_setting("KEGBOT_SECRET_KEY", default=None, required=True) diff --git a/pykeg/contrib/webhook/plugin.py b/pykeg/contrib/webhook/plugin.py index 0d47b4681..2a58b1717 100644 --- a/pykeg/contrib/webhook/plugin.py +++ b/pykeg/contrib/webhook/plugin.py @@ -24,7 +24,7 @@ def handle_new_events(self, events): self.handle_event(event) def handle_event(self, event): - self.logger.info("Handling new event: %s" % event.id) + self.logger.info(f"Handling new event: {event.id}") settings = self.get_site_settings() urls = settings.get("webhook_urls", "").strip().split() event_dict = protolib.ToDict(event, full=True) diff --git a/pykeg/contrib/webhook/tasks.py b/pykeg/contrib/webhook/tasks.py index f580d2f4b..c00b4e784 100644 --- a/pykeg/contrib/webhook/tasks.py +++ b/pykeg/contrib/webhook/tasks.py @@ -20,7 +20,7 @@ def webhook_post(url, event_dict): Event payloads are in the same format as the /api/events/ endpoint. """ - logger.info("Posting webhook: url=%s event=%s" % (url, event_dict)) + logger.info(f"Posting webhook: url={url} event={event_dict}") hook_dict = { "type": "event", @@ -29,11 +29,11 @@ def webhook_post(url, event_dict): headers = { "content-type": "application/json", - "user-agent": "Kegbot/%s" % get_version(), + "user-agent": f"Kegbot/{get_version()}", } try: return requests.post(url, data=kbjson.dumps(hook_dict), headers=headers) except requests.exceptions.RequestException as e: - logger.warning("Error posting hook: %s" % e) + logger.warning(f"Error posting hook: {e}") return False diff --git a/pykeg/core/admin.py b/pykeg/core/admin.py index e0c78c2a2..68c16ef99 100644 --- a/pykeg/core/admin.py +++ b/pykeg/core/admin.py @@ -81,11 +81,11 @@ class ThermoSensorAdmin(admin.ModelAdmin): def thermolog_deg_c(obj): - return "%.2f C" % (obj.temp,) + return f"{obj.temp:.2f} C" def thermolog_deg_f(obj): - return "%.2f F" % (CtoF(obj.temp),) + return f"{CtoF(obj.temp):.2f} F" class ThermologAdmin(admin.ModelAdmin): diff --git a/pykeg/core/apps.py b/pykeg/core/apps.py index 2783637dd..1ae5046c1 100644 --- a/pykeg/core/apps.py +++ b/pykeg/core/apps.py @@ -6,4 +6,4 @@ class CoreApp(AppConfig): def ready(self): # Implicitly connect signal handlers decorated with @receiver. - from . import signal_handlers + from . import signal_handlers # noqa: F401 (import registers signal receivers) diff --git a/pykeg/core/backend_test.py b/pykeg/core/backend_test.py index 4cfe412fb..22df7b9fb 100644 --- a/pykeg/core/backend_test.py +++ b/pykeg/core/backend_test.py @@ -1,9 +1,6 @@ """Unittests for backends module.""" -from builtins import range - from django.test import TransactionTestCase -from django.test.utils import override_settings from pykeg.core import defaults, models from pykeg.web.util import get_base_url @@ -284,17 +281,17 @@ def test_urls(self): producer_name=FAKE_BREWER_NAME, style_name=FAKE_BEER_STYLE, ) - self.assertEqual("http://test.example.com/kegs/{}/".format(keg.id), keg.full_url()) + self.assertEqual(f"http://test.example.com/kegs/{keg.id}/", keg.full_url()) drink = models.Drink.record_drink(METER_NAME, ticks=1, volume_ml=100, photo="foo") - self.assertEqual("http://test.example.com/d/{}/".format(drink.id), drink.short_url()) + self.assertEqual(f"http://test.example.com/d/{drink.id}/", drink.short_url()) self.assertEqual( - "http://test.example.com/s/{}/".format(drink.session.id), drink.session.short_url() + f"http://test.example.com/s/{drink.session.id}/", drink.session.short_url() ) start = drink.session.start_time - datepart = "{}/{}/{}".format(start.year, start.month, start.day) + datepart = f"{start.year}/{start.month}/{start.day}" self.assertEqual( - "http://test.example.com/sessions/{}/{}/".format(datepart, drink.session.id), + f"http://test.example.com/sessions/{datepart}/{drink.session.id}/", drink.session.full_url(), ) diff --git a/pykeg/core/cache.py b/pykeg/core/cache.py index 31ae08845..f031aa785 100644 --- a/pykeg/core/cache.py +++ b/pykeg/core/cache.py @@ -1,5 +1,4 @@ import time -from builtins import object, str from django.conf import settings from django.core.cache import cache as django_cache @@ -10,7 +9,7 @@ SEP = ":" -class KegbotCache(object): +class KegbotCache: """Wrapper around django cache, supporting Kegbot-specific features. Primarily, this wrapper facilitates "generational" or "namespaced" caching, diff --git a/pykeg/core/fields.py b/pykeg/core/fields.py index 3af57824a..4b4483bd4 100644 --- a/pykeg/core/fields.py +++ b/pykeg/core/fields.py @@ -250,7 +250,7 @@ def __init__(self, *args, **kwargs): kwargs.setdefault("max_length", 3) kwargs.setdefault("choices", COUNTRIES) - super(CountryField, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def get_internal_type(self): return "CharField" diff --git a/pykeg/core/management/commands/backup.py b/pykeg/core/management/commands/backup.py index 14c4ebb5c..66d373a29 100644 --- a/pykeg/core/management/commands/backup.py +++ b/pykeg/core/management/commands/backup.py @@ -1,6 +1,6 @@ import os -from django.core.files.storage import get_storage_class +from django.core.files.storage import default_storage from django.core.management.base import BaseCommand from pykeg.backup import backup @@ -21,14 +21,14 @@ def add_arguments(self, parser): def handle(self, **options): location = backup.backup(include_media=not options.get("no_media")) - storage = get_storage_class()() + storage = default_storage path = location if hasattr(storage, "location"): path = os.path.join(storage.location, path) print("Backup complete!") - print("Path: {}".format(path)) + print(f"Path: {path}") try: - print("URL: {}".format(storage.url(location))) - except (NotImplementedError, UnknownBaseUrlException): + print(f"URL: {storage.url(location)}") + except NotImplementedError, UnknownBaseUrlException: pass diff --git a/pykeg/core/management/commands/common.py b/pykeg/core/management/commands/common.py index b66ec6fa5..f3c212d14 100644 --- a/pykeg/core/management/commands/common.py +++ b/pykeg/core/management/commands/common.py @@ -1,7 +1,6 @@ import os import signal import sys -from builtins import object from django.core.management.base import BaseCommand @@ -25,21 +24,21 @@ def progbar(title, pos, total, width=40): chars = int((float(pos) / total) * width) rem = width - chars inner = "+" * chars + " " * rem - sys.stdout.write("%-30s [%s] %i/%i\r" % (title, inner, pos, total)) + sys.stdout.write(f"{title:<30} [{inner}] {pos}/{total}\r") sys.stdout.flush() def check_and_create_pid_file(pid_file): if os.path.exists(pid_file): - print("Error: already running ({})".format(pid_file)) + print(f"Error: already running ({pid_file})") sys.exit(1) f = open(pid_file, "w") - f.write("{}\n".format(os.getpid())) + f.write(f"{os.getpid()}\n") f.close() -class check_pidfile(object): +class check_pidfile: """Context manager that creates a pidfile, or fails if it exists.""" def __init__(self, pid_file): @@ -47,9 +46,9 @@ def __init__(self, pid_file): def __enter__(self): if os.path.exists(self.pid_file): - raise RuntimeError("Error: already running ({})".format(self.pid_file)) + raise RuntimeError(f"Error: already running ({self.pid_file})") f = open(self.pid_file, "w") - f.write("{}\n".format(os.getpid())) + f.write(f"{os.getpid()}\n") f.close() def __exit__(self, exc_type, exc_val, exc_tb): diff --git a/pykeg/core/management/commands/create_api_key.py b/pykeg/core/management/commands/create_api_key.py index 95b9ae889..83f29d8d2 100644 --- a/pykeg/core/management/commands/create_api_key.py +++ b/pykeg/core/management/commands/create_api_key.py @@ -1,15 +1,14 @@ -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import BaseCommand from pykeg.core import models class Command(BaseCommand): - args = "" help = "Creates an API key with the given description." - def handle(self, *args, **options): - if len(args) < 1: - raise CommandError("Must specify description") + def add_arguments(self, parser): + parser.add_argument("description") - key = models.ApiKey.objects.create(description=args[0]) + def handle(self, *args, **options): + key = models.ApiKey.objects.create(description=options["description"]) print(key.key) diff --git a/pykeg/core/management/commands/erase.py b/pykeg/core/management/commands/erase.py index 9c0d7b854..8ba8d93c7 100644 --- a/pykeg/core/management/commands/erase.py +++ b/pykeg/core/management/commands/erase.py @@ -1,5 +1,4 @@ import sys -from builtins import input from django.conf import settings from django.core.management.base import BaseCommand @@ -18,7 +17,7 @@ def handle(self, *args, **options): print(" CANNOT BE UNDONE.") print("") print(" Database: {}".format(settings.DATABASES["default"]["NAME"])) - print(" Media: {}".format(settings.MEDIA_ROOT)) + print(f" Media: {settings.MEDIA_ROOT}") print(" ************************************************************************") print("") print("Are you SURE you want to continue? ") diff --git a/pykeg/core/management/commands/generate_fixtures.py b/pykeg/core/management/commands/generate_fixtures.py index b9df3599c..6cde8d9ed 100644 --- a/pykeg/core/management/commands/generate_fixtures.py +++ b/pykeg/core/management/commands/generate_fixtures.py @@ -54,8 +54,8 @@ def create_objects(self): beverage1 = models.Beverage.objects.create(name="Test Beverage 1", producer=producer1) beverage2 = models.Beverage.objects.create(name="Test Beverage 2", producer=producer2) - keg1 = models.Keg.start_keg("kegboard.flow0", beverage1) - keg2 = models.Keg.start_keg("kegboard.flow1", beverage2) + models.Keg.start_keg("kegboard.flow0", beverage1) + models.Keg.start_keg("kegboard.flow1", beverage2) users = {"alice": None, "bob": None, "carol": None, "mallory": None} for username in users.keys(): diff --git a/pykeg/core/management/commands/rename_user.py b/pykeg/core/management/commands/rename_user.py index e234b8ae4..493234f60 100644 --- a/pykeg/core/management/commands/rename_user.py +++ b/pykeg/core/management/commands/rename_user.py @@ -5,15 +5,15 @@ class Command(BaseCommand): - args = " " help = "Renames user from to ." - def handle(self, *args, **options): - if len(args) < 2: - raise CommandError("Must specify and ") + def add_arguments(self, parser): + parser.add_argument("from_username", metavar="from") + parser.add_argument("to_username", metavar="to") - from_username = args[0] - to_username = args[1] + def handle(self, *args, **options): + from_username = options["from_username"] + to_username = options["to_username"] if from_username == "guest": raise CommandError("Cannot rename the guest user.") @@ -22,9 +22,9 @@ def handle(self, *args, **options): try: user = models.User.objects.get(username=from_username) except models.User.DoesNotExist: - raise CommandError('User named "{}" does not exist'.format(from_username)) + raise CommandError(f'User named "{from_username}" does not exist') user.username = to_username user.save() - print('"{}" has been renamed "{}"'.format(from_username, to_username)) + print(f'"{from_username}" has been renamed "{to_username}"') diff --git a/pykeg/core/management/commands/restore.py b/pykeg/core/management/commands/restore.py index 8b81263a5..4c0e1a762 100644 --- a/pykeg/core/management/commands/restore.py +++ b/pykeg/core/management/commands/restore.py @@ -8,14 +8,14 @@ class Command(BaseCommand): help = "Restores a zipfile backup of the current Kegbot system." - args = "" + + def add_arguments(self, parser): + parser.add_argument("zipfile") def handle(self, *args, **options): - if len(args) != 1: - raise CommandError("Must provide a zipfile path") - backup_path = os.path.normpath(os.path.expanduser(args[0])) + backup_path = os.path.normpath(os.path.expanduser(options["zipfile"])) if not os.path.exists(backup_path): - raise CommandError("Archive does not exist: {}".format(backup_path)) + raise CommandError(f"Archive does not exist: {backup_path}") try: backup.restore(backup_path) @@ -25,6 +25,6 @@ def handle(self, *args, **options): sys.exit(1) except backup.BackupError as e: sys.stderr.write("Error: ") - sys.stderr.write(e) + sys.stderr.write(str(e)) sys.stderr.write("\n") sys.exit(1) diff --git a/pykeg/core/management/commands/run_all.py b/pykeg/core/management/commands/run_all.py index b500e6b91..db64522bc 100644 --- a/pykeg/core/management/commands/run_all.py +++ b/pykeg/core/management/commands/run_all.py @@ -11,12 +11,12 @@ def get_commands(self, options): server_command = "kegbot run_server" if logs_dir: - server_command += " --logs_dir={}".format(logs_dir) + server_command += f" --logs_dir={logs_dir}" ret.append(("server", server_command)) workers_command = "kegbot run_workers" if logs_dir: - workers_command += " --logs_dir={}".format(logs_dir) + workers_command += f" --logs_dir={logs_dir}" ret.append(("workers", workers_command)) return ret diff --git a/pykeg/core/management/commands/run_server.py b/pykeg/core/management/commands/run_server.py index f33d8f111..9f2583358 100644 --- a/pykeg/core/management/commands/run_server.py +++ b/pykeg/core/management/commands/run_server.py @@ -1,3 +1,5 @@ +import os + from pykeg.core.management.commands.common import RunnerCommand @@ -6,22 +8,21 @@ class Command(RunnerCommand): pidfile_name = "kegbot_run_server.pid" def add_arguments(self, parser): - super(Command, self).add_arguments(parser) + super().add_arguments(parser) parser.add_argument( - "--gunicorn_options", + "--waitress_options", action="store", - dest="gunicorn_options", - default="-w 3", - help="Specifies extra options to pass to gunicorn.", + dest="waitress_options", + default="--threads=4", + help="Specifies extra options to pass to waitress-serve.", ) def get_commands(self, options): - ret = [] - extra_options = options.get("gunicorn_options", "") + # Honor $PORT (e.g. Heroku) but default to 8000. + port = os.getenv("PORT") or "8000" + extra_options = options.get("waitress_options", "") command_line = ( - "gunicorn pykeg.web.wsgi:application " - "--config=python:pykeg.web.gunicorn_conf " - f"{extra_options}" + f"waitress-serve --host=0.0.0.0 --port={port} " + f"{extra_options} pykeg.web.wsgi:application" ).strip() - ret.append(("guincorn", command_line)) - return ret + return [("waitress", command_line)] diff --git a/pykeg/core/management/commands/upgrade.py b/pykeg/core/management/commands/upgrade.py index b6bf483c0..10b66bab2 100644 --- a/pykeg/core/management/commands/upgrade.py +++ b/pykeg/core/management/commands/upgrade.py @@ -1,5 +1,4 @@ import sys -from builtins import str from django.contrib.staticfiles.management.commands import collectstatic from django.core.management.base import BaseCommand @@ -19,8 +18,8 @@ def run(cmd, args=[]): cmdname = cmd.__module__.split(".")[-1] - arg_str = " ".join("%s" % a for a in args) - print("--- Running command: %s %s" % (cmdname, arg_str)) + arg_str = " ".join(f"{a}" for a in args) + print(f"--- Running command: {cmdname} {arg_str}") cmd.run_from_argv([sys.argv[0], cmdname] + args) @@ -60,32 +59,26 @@ def handle(self, *args, **options): sys.exit(1) if installed_version == app_version and not force: - print("Version {} already installed.".format(installed_version)) + print(f"Version {installed_version} already installed.") return if installed_version > app_version: - print( - "Installed version {} is newer than app version {}".format( - installed_version, app_version - ) - ) + print(f"Installed version {installed_version} is newer than app version {app_version}") sys.exit(1) if installed_version < MINIMUM_INSTALLED_VERSION: print("") print("ERROR: This version of Kegbot can only upgrade systems running on version") print( - "v{} or newer. Please install Kegbot v{} and run `kegbot upgrade` again.".format( - MINIMUM_INSTALLED_VERSION, MINIMUM_INSTALLED_VERSION - ) + f"v{MINIMUM_INSTALLED_VERSION} or newer. Please install Kegbot v{MINIMUM_INSTALLED_VERSION} and run `kegbot upgrade` again." ) - print("(Existing version: {})".format(installed_version)) + print(f"(Existing version: {installed_version})") print("") print("More help: https://github.com/Kegbot/kegbot-server/wiki/Upgrading-Old-Versions") print("") sys.exit(1) - print("Upgrading from {} to {}".format(installed_version, app_version)) + print(f"Upgrading from {installed_version} to {app_version}") self.do_version_upgrades(installed_version) run(migrate.Command(), args=["--noinput", "-v", "0"]) diff --git a/pykeg/core/models.py b/pykeg/core/models.py index 064142f89..3bc49a532 100644 --- a/pykeg/core/models.py +++ b/pykeg/core/models.py @@ -4,10 +4,8 @@ import random import re import urllib.parse -from builtins import object, str from uuid import uuid4 -import pytz from addict import Dict from django.conf import settings from django.contrib.auth.models import AbstractBaseUser, UserManager @@ -34,6 +32,7 @@ signals, time_series, ) +from pykeg.core.timezones import COMMON_TIMEZONES from pykeg.core.util import CtoF, get_version from pykeg.util import kbjson, units from pykeg.util.email import build_message @@ -42,7 +41,11 @@ """Django models definition for the kegbot database.""" -TIMEZONE_CHOICES = ((z, z) for z in pytz.common_timezones) +# TODO(temporary): COMMON_TIMEZONES is the historical pytz.common_timezones list, +# vendored so this field's `choices` stay byte-for-byte identical and we avoid a +# data-only migration during the 2.0 upgrade. Replace with a zoneinfo-derived +# list (and ship the accompanying migration) as a follow-up. +TIMEZONE_CHOICES = ((z, z) for z in COMMON_TIMEZONES) logger = logging.getLogger(__name__) @@ -73,7 +76,7 @@ class User(AbstractBaseUser): _("superuser status"), default=False, help_text=_( - "Designates that this user has all permissions without " "explicitly assigning them." + "Designates that this user has all permissions without explicitly assigning them." ), ) @@ -81,9 +84,7 @@ class User(AbstractBaseUser): _("username"), max_length=30, unique=True, - help_text=_( - "Required. 30 characters or fewer. Letters, numbers and " "@/./+/-/_ characters" - ), + help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"), validators=[ validators.RegexValidator( re.compile(kb_common.USERNAME_REGEX), _("Enter a valid username."), "invalid" @@ -100,7 +101,7 @@ class User(AbstractBaseUser): is_staff = models.BooleanField( _("staff status"), default=False, - help_text=_("Designates whether the user can log into this admin " "site."), + help_text=_("Designates whether the user can log into this admin site."), ) is_active = models.BooleanField( _("active"), @@ -129,7 +130,7 @@ class User(AbstractBaseUser): USERNAME_FIELD = "username" REQUIRED_FIELDS = ["email"] - class Meta(object): + class Meta: verbose_name = _("user") verbose_name_plural = _("users") @@ -237,7 +238,6 @@ def send(self): class KegbotSite(models.Model): - VOLUME_DISPLAY_UNITS_CHOICES = ( ("metric", "Metric (mL, L)"), ("imperial", "Imperial (oz, pint)"), @@ -301,13 +301,13 @@ class KegbotSite(models.Model): blank=True, null=True, max_length=64, - help_text="Set to your Google Analytics ID to enable tracking. " "Example: UA-XXXX-y", + help_text="Set to your Google Analytics ID to enable tracking. Example: UA-XXXX-y", ) session_timeout_minutes = models.PositiveIntegerField( default=kb_common.DRINK_SESSION_TIME_MINUTES, help_text="Maximum time, in minutes, that a session may be idle (no pours) " "before it is considered to be finished. " - "Recommended value is %s." % kb_common.DRINK_SESSION_TIME_MINUTES, + f"Recommended value is {kb_common.DRINK_SESSION_TIME_MINUTES}.", ) privacy = models.CharField( max_length=63, @@ -389,10 +389,10 @@ def reverse_full(self, *args, **kwargs): def format_volume(self, volume_ml): if self.volume_display_units == "metric": if volume_ml < 500: - return "%d mL" % int(volume_ml) - return "%.1f L" % (volume_ml / 1000.0) + return f"{int(volume_ml)} mL" + return f"{volume_ml / 1000.0:.1f} L" else: - return "%1.f oz" % units.Quantity(volume_ml).InOunces() + return f"{units.Quantity(volume_ml).InOunces():1.0f} oz" def can_invite(self, user): if not user: @@ -454,7 +454,7 @@ def regenerate(self): @classmethod def generate_key(cls): """Returns a new random key.""" - return "%032x" % random.randint(0, 2**128 - 1) + return f"{random.randint(0, 2**128 - 1):032x}" def _sitesettings_post_save(sender, instance, **kwargs): @@ -494,7 +494,7 @@ class BeverageProducer(models.Model): max_length=255, blank=True, null=True, help_text="Future use." ) - class Meta(object): + class Meta: ordering = ("name",) def __str__(self): @@ -604,17 +604,17 @@ class Beverage(models.Model): max_length=255, blank=True, null=True, help_text="Future use." ) - class Meta(object): + class Meta: ordering = ("name",) def __str__(self): - return "{} by {}".format(self.name, self.producer) + return f"{self.name} by {self.producer}" class KegTap(models.Model): """A physical tap of beer.""" - class Meta(object): + class Meta: ordering = ("sort_order", "id") name = models.CharField( @@ -641,7 +641,7 @@ class Meta(object): ) def __str__(self): - return "{}: {}".format(self.name, self.current_keg) + return f"{self.name}: {self.current_keg}" def is_active(self): """Returns True if the tap has an active Keg.""" @@ -824,11 +824,11 @@ class Controller(models.Model): ) def __str__(self): - return "Controller: {}".format(self.name) + return f"Controller: {self.name}" class FlowMeter(models.Model): - class Meta(object): + class Meta: unique_together = ("controller", "port_name") controller = models.ForeignKey( @@ -850,12 +850,12 @@ class Meta(object): ) ticks_per_ml = models.FloatField( default=kb_common.DEFAULT_TICKS_PER_ML, - help_text="Flow meter pulses per mL of fluid. Common values: %s " - "(FT330-RJ), 5.4 (SF800)" % kb_common.DEFAULT_TICKS_PER_ML, + help_text=f"Flow meter pulses per mL of fluid. Common values: {kb_common.DEFAULT_TICKS_PER_ML} " + "(FT330-RJ), 5.4 (SF800)", ) def meter_name(self): - return "{}.{}".format(self.controller.name, self.port_name) + return f"{self.controller.name}.{self.port_name}" def __str__(self): return self.meter_name() @@ -880,20 +880,20 @@ def get_or_create_from_meter_name(cls, meter_name): def get_from_meter_name(cls, meter_name): idx = meter_name.find(".") if idx <= 0: - raise cls.DoesNotExist("Illegal meter_name: %s" % repr(meter_name)) + raise cls.DoesNotExist(f"Illegal meter_name: {repr(meter_name)}") controller_name = meter_name[:idx] port_name = meter_name[idx + 1 :] try: controller = Controller.objects.get(name=controller_name) except Controller.DoesNotExist: - raise cls.DoesNotExist("No such controller: %s" % repr(controller_name)) + raise cls.DoesNotExist(f"No such controller: {repr(controller_name)}") return cls.objects.get(controller=controller, port_name=port_name) class FlowToggle(models.Model): - class Meta(object): + class Meta: unique_together = ("controller", "port_name") controller = models.ForeignKey( @@ -915,10 +915,10 @@ class Meta(object): ) def toggle_name(self): - return "{}.{}".format(self.controller.name, self.port_name) + return f"{self.controller.name}.{self.port_name}" def __str__(self): - return "{} (tap: {})".format(self.toggle_name(), self.tap) + return f"{self.toggle_name()} (tap: {self.tap})" @classmethod def get_or_create_from_toggle_name(cls, toggle_name): @@ -940,14 +940,14 @@ def get_or_create_from_toggle_name(cls, toggle_name): def get_from_toggle_name(cls, toggle_name): idx = toggle_name.find(".") if idx <= 0: - raise cls.DoesNotExist("Illegal toggle_name: %s" % repr(toggle_name)) + raise cls.DoesNotExist(f"Illegal toggle_name: {repr(toggle_name)}") controller_name = toggle_name[:idx] port_name = toggle_name[idx + 1 :] try: controller = Controller.objects.get(name=controller_name) except Controller.DoesNotExist: - raise cls.DoesNotExist("No such controller: %s" % repr(controller_name)) + raise cls.DoesNotExist(f"No such controller: {repr(controller_name)}") return cls.objects.get(controller=controller, port_name=port_name) @@ -1062,7 +1062,7 @@ def get_illustration(self, thumbnail=False): else: level = 0 kind = "thumb" if thumbnail else "full" - img_path = "images/keg/{}/keg-srm14-{}.png".format(kind, level) + img_path = f"images/keg/{kind}/keg-srm14-{level}.png" url = urllib.parse.urljoin(settings.STATIC_URL, img_path) if not urllib.parse.urlparse(url).scheme: @@ -1219,7 +1219,7 @@ def create_keg( )[0] if keg_type not in keg_sizes.DESCRIPTIONS: - raise ValueError("Unrecognized keg type: %s" % keg_type) + raise ValueError(f"Unrecognized keg type: {keg_type}") if full_volume_ml is None: full_volume_ml = keg_sizes.VOLUMES_ML[keg_type] else: @@ -1272,7 +1272,7 @@ def end_keg(self, when=None): return self def __str__(self): - return "Keg #{} - {}".format(self.id, self.type) + return f"Keg #{self.id} - {self.type}" def _keg_pre_save(sender, instance, **kwargs): @@ -1281,6 +1281,11 @@ def _keg_pre_save(sender, instance, **kwargs): if keg.status == keg.STATUS_ON_TAP: return + # A brand-new (unsaved) keg has no drinks yet; Django 5.x also forbids + # using a reverse relation before the instance has a primary key. + if keg.pk is None: + return + # Determine first drink date & set keg start date to it if earlier. drinks = keg.drinks.all().order_by("time") if drinks: @@ -1302,7 +1307,7 @@ def _keg_pre_save(sender, instance, **kwargs): class Drink(models.Model): """Table of drinks records""" - class Meta(object): + class Meta: get_latest_by = "time" ordering = ("-time",) @@ -1509,7 +1514,7 @@ def record_drink( # it again. If malformed, just junk it; it's non-essential information. tick_time_series = time_series.to_string(time_series.from_string(tick_time_series)) except ValueError as e: - logger.warning("Time series invalid, ignoring. Error was: %s" % e) + logger.warning(f"Time series invalid, ignoring. Error was: {e}") tick_time_series = "" d = Drink( @@ -1577,13 +1582,13 @@ def cancel_drink(self, spilled=False): signals.drink_canceled.send_robust(sender=self.__class__, drink_id=drink_id) def __str__(self): - return "Drink {} by {}".format(self.id, self.user) + return f"Drink {self.id} by {self.user}" class AuthenticationToken(models.Model): """A secret token to authenticate a user, optionally pin-protected.""" - class Meta(object): + class Meta: unique_together = ("auth_device", "token_value") auth_device = models.CharField(max_length=64, help_text="Namespace for this token.") @@ -1627,9 +1632,9 @@ def __str__(self): elif auth_device == "core.onewire": auth_device = "OneWire" - ret = "{} {}".format(auth_device, self.token_value) + ret = f"{auth_device} {self.token_value}" if self.nice_name: - ret += " ({})".format(self.nice_name) + ret += f" ({self.nice_name})" return ret def get_auth_device(self): @@ -1688,7 +1693,7 @@ def _auth_token_pre_save(sender, instance, **kwargs): class DrinkingSession(models.Model): """A collection of contiguous drinks.""" - class Meta(object): + class Meta: get_latest_by = "start_time" ordering = ("-start_time",) @@ -1701,7 +1706,7 @@ class Meta(object): name = models.CharField(max_length=256, blank=True, null=True) def __str__(self): - return "Session #{}: {}".format(self.id, self.start_time) + return f"Session #{self.id}: {self.start_time}" def Duration(self): return self.end_time - self.start_time @@ -1770,18 +1775,18 @@ def summarize_drinkers(self): ret = "{}, {} and {}".format(*names) else: if guest_trailer: - return "%s, %s and at least %i others" % (names[0], names[1], num - 2) + return f"{names[0]}, {names[1]} and at least {num - 2} others" else: - return "%s, %s and %i others" % (names[0], names[1], num - 2) + return f"{names[0]}, {names[1]} and {num - 2} others" - return "%s%s" % (ret, guest_trailer) + return f"{ret}{guest_trailer}" def GetTitle(self): if self.name: return self.name else: if self.id: - return "Session %s" % (self.id,) + return f"Session {self.id}" else: # Not yet saved. return "New Session" @@ -1853,7 +1858,7 @@ class ThermoSensor(models.Model): def __str__(self): if self.nice_name: - return "{} ({})".format(self.nice_name, self.raw_name) + return f"{self.nice_name} ({self.raw_name})" return self.raw_name def LastLog(self): @@ -1919,7 +1924,7 @@ def log_sensor_reading(self, temperature, when=None): class Thermolog(models.Model): """A log from an ITemperatureSensor device of periodic measurements.""" - class Meta(object): + class Meta: get_latest_by = "time" ordering = ("-time",) @@ -1928,7 +1933,7 @@ class Meta(object): time = models.DateTimeField() def __str__(self): - return "%.2f C / %.2f F [%s]" % (self.TempC(), self.TempF(), self.time) + return f"{self.TempC():.2f} C / {self.TempF():.2f} F [{self.time}]" def TempC(self): return self.temp @@ -1966,7 +1971,7 @@ class Stats(models.Model): DrinkingSession, related_name="stats", null=True, on_delete=models.CASCADE ) - class Meta(object): + class Meta: get_latest_by = "id" unique_together = ("drink", "user", "keg", "session") @@ -1977,7 +1982,7 @@ def apply_usernames(cls, stats): def safe_get_user(pk): try: return User.objects.get(pk=pk) - except (User.DoesNotExist, ValueError): + except User.DoesNotExist, ValueError: return None orig = stats.get("registered_drinkers", []) @@ -2009,7 +2014,7 @@ def get_latest_for_view(cls, user=None, keg=None, session=None): class SystemEvent(models.Model): - class Meta(object): + class Meta: ordering = ("-id",) get_latest_by = "time" @@ -2068,22 +2073,22 @@ class Meta(object): def __str__(self): if self.kind == self.DRINK_POURED: - ret = "Drink {} poured".format(self.drink.id) + ret = f"Drink {self.drink.id} poured" elif self.kind == self.SESSION_STARTED: - ret = "Session {} started by drink {}".format(self.session.id, self.drink.id) + ret = f"Session {self.session.id} started by drink {self.drink.id}" elif self.kind == self.SESSION_JOINED: - ret = "Session {} joined by {} (drink {})".format( - self.session.id, self.user.username, self.drink.id + ret = ( + f"Session {self.session.id} joined by {self.user.username} (drink {self.drink.id})" ) elif self.kind == self.KEG_TAPPED: - ret = "Keg {} tapped".format(self.keg.id) + ret = f"Keg {self.keg.id} tapped" elif self.kind == self.KEG_VOLUME_LOW: - ret = "Keg {} volume low".format(self.keg.id) + ret = f"Keg {self.keg.id} volume low" elif self.kind == self.KEG_ENDED: - ret = "Keg {} ended".format(self.keg.id) + ret = f"Keg {self.keg.id} ended" else: - ret = "Unknown event type ({})".format(self.kind) - return "Event {}: {}".format(self.id, ret) + ret = f"Unknown event type ({self.kind})" + return f"Event {self.id}: {ret}" @classmethod def build_events_for_keg(cls, keg): @@ -2167,7 +2172,7 @@ def _pics_file_name(instance, filename, now=None, uuid_str=None): uuid_str = str(uuid4()).replace("-", "") date_str = now.strftime("%Y%m%d%H%M%S") ext = os.path.splitext(filename)[1] - new_filename = "%s-%s%s" % (date_str, uuid_str, ext) + new_filename = f"{date_str}-{uuid_str}{ext}" return os.path.join("pics", new_filename) @@ -2227,16 +2232,16 @@ class Picture(models.Model): ) def __str__(self): - return "Picture: {}".format(self.image) + return f"Picture: {self.image}" def get_caption(self): if self.caption: return self.caption elif self.drink: if self.user: - return "{} pouring drink {}".format(self.user.username, self.drink.id) + return f"{self.user.username} pouring drink {self.drink.id}" else: - return "An unknown drinker pouring drink {}".format(self.drink.id) + return f"An unknown drinker pouring drink {self.drink.id}" return "" def erase_and_delete(self): @@ -2246,27 +2251,19 @@ def erase_and_delete(self): exists = bool(image_file) if not exists: logger.debug( - "erase_and_delete: image.id={} spec={}: does not exist".format( - self.id, spec - ) + f"erase_and_delete: image.id={self.id} spec={spec}: does not exist" ) continue - logger.debug( - "erase_and_delete: image.id={} spec={}: deleting ...".format(self.id, spec) - ) + logger.debug(f"erase_and_delete: image.id={self.id} spec={spec}: deleting ...") try: try: default_storage.delete(image_file.path) except NotImplementedError: default_storage.delete(image_file.name) - logger.debug( - "erase_and_delete: image.id={} spec={}: deleted".format(self.id, spec) - ) - except IOError as e: - logger.warning( - "erase_and_delete: image.id={} spec={}: error: {}".format(self.id, spec, e) - ) + logger.debug(f"erase_and_delete: image.id={self.id} spec={spec}: deleted") + except OSError as e: + logger.warning(f"erase_and_delete: image.id={self.id} spec={spec}: error: {e}") finally: self.delete() @@ -2274,7 +2271,7 @@ def erase_and_delete(self): class NotificationSettings(models.Model): """Stores a user's notification settings for a notification backend.""" - class Meta(object): + class Meta: unique_together = ("user", "backend") user = models.ForeignKey(User, on_delete=models.CASCADE, help_text="User for these settings.") @@ -2294,7 +2291,7 @@ class Meta(object): class PluginData(models.Model): """Key/value JSON data store for plugins.""" - class Meta(object): + class Meta: unique_together = ("plugin_name", "key") plugin_name = models.CharField(max_length=127, help_text="Plugin short name") diff --git a/pykeg/core/stats.py b/pykeg/core/stats.py index 56f71b244..e8cd53320 100644 --- a/pykeg/core/stats.py +++ b/pykeg/core/stats.py @@ -3,9 +3,8 @@ import copy import inspect import logging -from builtins import object, str +import zoneinfo -import pytz from addict import Dict from django.utils.timezone import localtime @@ -16,7 +15,7 @@ logger = logging.getLogger(__name__) -class StatsView(object): +class StatsView: def __init__(self, user=None, session=None, keg=None): self.user = user self.session = session @@ -27,11 +26,11 @@ def __str__(self): if not self.user and not self.keg and not self.session: ret += "system" if self.user: - ret += "user={} ".format(self.user.username) + ret += f"user={self.user.username} " if self.session: - ret += "session={} ".format(self.session.id) + ret += f"session={self.session.id} " if self.keg: - ret += "keg={}".format(self.keg.id) + ret += f"keg={self.keg.id}" return ret def as_tuple(self): @@ -57,7 +56,7 @@ def get_prior_drinks(self, drink): return qs.order_by("-id") -class StatsBuilder(object): +class StatsBuilder: """Derives statistics from drinks.""" def __init__(self): @@ -70,7 +69,7 @@ def build(self, drink, previous_stats): if previous_stats is None: previous_stats = Dict() - logger.debug("build: drink={}".format(drink.id)) + logger.debug(f"build: drink={drink.id}") stats = Dict() for statname, fn in self.functions: @@ -81,7 +80,7 @@ def build(self, drink, previous_stats): val = fn(drink, previous_stats) if val is None: - raise ValueError("Stat generator for %s returned None" % statname) + raise ValueError(f"Stat generator for {statname} returned None") stats[statname] = val return stats @@ -117,7 +116,7 @@ def greatest_volume_id(self, drink, previous_stats, previous_value=0): def volume_by_day_of_week(self, drink, previous_stats, previous_value={}): ret = copy.copy(previous_value) - tz = pytz.timezone(drink.session.timezone) + tz = zoneinfo.ZoneInfo(drink.session.timezone) local_time = localtime(drink.session.start_time, timezone=tz) drink_weekday = str(local_time.strftime("%w")) ret[drink_weekday] = ret.get(drink_weekday, 0) + drink.volume_ml @@ -180,7 +179,7 @@ def largest_session(self, drink, previous_stats, previous_value={}): def invalidate(drink_id): """Clears all statistics starting from (and including) drink_id.""" - logger.debug("--- Invalidating stats since id {}".format(drink_id)) + logger.debug(f"--- Invalidating stats since id {drink_id}") models.Stats.objects.filter(drink_id__gte=drink_id).delete() @@ -196,7 +195,7 @@ def build_for_id(drink_id): try: drink = models.Drink.objects.get(pk=drink_id) except models.Drink.DoesNotExist: - logger.warning("No drink exists with id={}".format(drink_id)) + logger.warning(f"No drink exists with id={drink_id}") return _build_all_views(drink) @@ -224,7 +223,7 @@ def _build_single_view(drink, view, prior_stats=None): indicates that the prior stats are unknown. When prior stats are unknown, they will be queried or generated as needed. """ - logger.debug(">>> Building stats for {}".format(view)) + logger.debug(f">>> Building stats for {view}") build_list = [drink] if prior_stats is None: @@ -249,7 +248,7 @@ def _build_single_view(drink, view, prior_stats=None): # Build all drinks on the hit list. for build_drink in build_list: - logger.debug(" - operating on drink {}".format(build_drink.id)) + logger.debug(f" - operating on drink {build_drink.id}") stats = BUILDER.build(drink=build_drink, previous_stats=prior_stats) models.Stats.objects.create( drink=build_drink, diff --git a/pykeg/core/stats_test.py b/pykeg/core/stats_test.py index 6b1ebc07c..9f558128d 100644 --- a/pykeg/core/stats_test.py +++ b/pykeg/core/stats_test.py @@ -2,7 +2,6 @@ from addict import Dict from django.test import TransactionTestCase -from django.test.utils import override_settings from . import models from .testutils import make_datetime @@ -16,7 +15,7 @@ def setUp(self): test_usernames = ("user1", "user2", "user3") self.users = [ - models.User.create_new_user(name, "%s@example.com" % name) for name in test_usernames + models.User.create_new_user(name, f"{name}@example.com") for name in test_usernames ] self.taps = [ diff --git a/pykeg/core/tasks.py b/pykeg/core/tasks.py index 8d71c4fd7..fbc485887 100644 --- a/pykeg/core/tasks.py +++ b/pykeg/core/tasks.py @@ -20,13 +20,13 @@ def schedule_tasks(events): try: plugin.handle_new_events(events) except Exception: - logger.exception("Error dispatching events to plugin {}".format(plugin.get_name())) + logger.exception(f"Error dispatching events to plugin {plugin.get_name()}") notification.handle_new_system_events(events) @job("stats") def build_stats(drink_id, rebuild_following): - logger.info("build_stats drink_id={} rebuild_following={}".format(drink_id, rebuild_following)) + logger.info(f"build_stats drink_id={drink_id} rebuild_following={rebuild_following}") with transaction.atomic(): if rebuild_following: stats.rebuild_from_id(drink_id) diff --git a/pykeg/core/tests.py b/pykeg/core/tests.py index 01f42fa03..9db157743 100644 --- a/pykeg/core/tests.py +++ b/pykeg/core/tests.py @@ -20,10 +20,10 @@ class CoreTests(TestCase): def test_flake8(self): root_path = path_for_import("pykeg") config_file = os.path.join(root_path, "setup.cfg") - command = "flake8 --config={} {}".format(config_file, root_path) + command = f"flake8 --config={config_file} {root_path}" try: subprocess.check_output(command.split()) except subprocess.CalledProcessError as e: - print("command: {}".format(command)) + print(f"command: {command}") print(e.output) - self.fail("flake8 failed with return code {}.".format(e.returncode)) + self.fail(f"flake8 failed with return code {e.returncode}.") diff --git a/pykeg/core/testutils.py b/pykeg/core/testutils.py index da4094f3e..d90bdfbdc 100644 --- a/pykeg/core/testutils.py +++ b/pykeg/core/testutils.py @@ -5,7 +5,6 @@ import vcr from django.conf import settings -from django.utils import timezone TESTDATA_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../testdata/")) CASSETTE_DIR = os.path.join(TESTDATA_DIR, "request_fixtures") @@ -17,7 +16,7 @@ def get_filename(f): def make_datetime(*args): if settings.USE_TZ: - return datetime.datetime(*args, tzinfo=timezone.utc) + return datetime.datetime(*args, tzinfo=datetime.UTC) else: return datetime.datetime(*args) diff --git a/pykeg/core/time_series.py b/pykeg/core/time_series.py index bcb1e2236..c1be73d0f 100644 --- a/pykeg/core/time_series.py +++ b/pykeg/core/time_series.py @@ -20,11 +20,11 @@ def from_string(s): time = int(time) amount = int(amount) if time < 0: - raise ValueError("Time cannot be less than zero: %s" % time) + raise ValueError(f"Time cannot be less than zero: {time}") ret.append((time, amount)) return ret def to_string(pairs): """Converts a series of (int, int) tuples to a time series string.""" - return " ".join("%i:%i" % pair for pair in pairs) + return " ".join(f"{pair[0]}:{pair[1]}" for pair in pairs) diff --git a/pykeg/core/timezones.py b/pykeg/core/timezones.py new file mode 100644 index 000000000..82f900f1a --- /dev/null +++ b/pykeg/core/timezones.py @@ -0,0 +1,449 @@ +"""Curated list of common time zones for the KegbotSite.timezone field. + +TODO(temporary): this is the historical pytz.common_timezones list, vendored so +the field's choices stay stable (avoiding a data-only migration) after moving +from pytz to zoneinfo. Time-zone math elsewhere uses stdlib zoneinfo. Replace +with a zoneinfo-derived list (plus its migration) as a follow-up. +""" + +COMMON_TIMEZONES = [ + "Africa/Abidjan", + "Africa/Accra", + "Africa/Addis_Ababa", + "Africa/Algiers", + "Africa/Asmara", + "Africa/Bamako", + "Africa/Bangui", + "Africa/Banjul", + "Africa/Bissau", + "Africa/Blantyre", + "Africa/Brazzaville", + "Africa/Bujumbura", + "Africa/Cairo", + "Africa/Casablanca", + "Africa/Ceuta", + "Africa/Conakry", + "Africa/Dakar", + "Africa/Dar_es_Salaam", + "Africa/Djibouti", + "Africa/Douala", + "Africa/El_Aaiun", + "Africa/Freetown", + "Africa/Gaborone", + "Africa/Harare", + "Africa/Johannesburg", + "Africa/Juba", + "Africa/Kampala", + "Africa/Khartoum", + "Africa/Kigali", + "Africa/Kinshasa", + "Africa/Lagos", + "Africa/Libreville", + "Africa/Lome", + "Africa/Luanda", + "Africa/Lubumbashi", + "Africa/Lusaka", + "Africa/Malabo", + "Africa/Maputo", + "Africa/Maseru", + "Africa/Mbabane", + "Africa/Mogadishu", + "Africa/Monrovia", + "Africa/Nairobi", + "Africa/Ndjamena", + "Africa/Niamey", + "Africa/Nouakchott", + "Africa/Ouagadougou", + "Africa/Porto-Novo", + "Africa/Sao_Tome", + "Africa/Tripoli", + "Africa/Tunis", + "Africa/Windhoek", + "America/Adak", + "America/Anchorage", + "America/Anguilla", + "America/Antigua", + "America/Araguaina", + "America/Argentina/Buenos_Aires", + "America/Argentina/Catamarca", + "America/Argentina/Cordoba", + "America/Argentina/Jujuy", + "America/Argentina/La_Rioja", + "America/Argentina/Mendoza", + "America/Argentina/Rio_Gallegos", + "America/Argentina/Salta", + "America/Argentina/San_Juan", + "America/Argentina/San_Luis", + "America/Argentina/Tucuman", + "America/Argentina/Ushuaia", + "America/Aruba", + "America/Asuncion", + "America/Atikokan", + "America/Bahia", + "America/Bahia_Banderas", + "America/Barbados", + "America/Belem", + "America/Belize", + "America/Blanc-Sablon", + "America/Boa_Vista", + "America/Bogota", + "America/Boise", + "America/Cambridge_Bay", + "America/Campo_Grande", + "America/Cancun", + "America/Caracas", + "America/Cayenne", + "America/Cayman", + "America/Chicago", + "America/Chihuahua", + "America/Costa_Rica", + "America/Creston", + "America/Cuiaba", + "America/Curacao", + "America/Danmarkshavn", + "America/Dawson", + "America/Dawson_Creek", + "America/Denver", + "America/Detroit", + "America/Dominica", + "America/Edmonton", + "America/Eirunepe", + "America/El_Salvador", + "America/Fort_Nelson", + "America/Fortaleza", + "America/Glace_Bay", + "America/Goose_Bay", + "America/Grand_Turk", + "America/Grenada", + "America/Guadeloupe", + "America/Guatemala", + "America/Guayaquil", + "America/Guyana", + "America/Halifax", + "America/Havana", + "America/Hermosillo", + "America/Indiana/Indianapolis", + "America/Indiana/Knox", + "America/Indiana/Marengo", + "America/Indiana/Petersburg", + "America/Indiana/Tell_City", + "America/Indiana/Vevay", + "America/Indiana/Vincennes", + "America/Indiana/Winamac", + "America/Inuvik", + "America/Iqaluit", + "America/Jamaica", + "America/Juneau", + "America/Kentucky/Louisville", + "America/Kentucky/Monticello", + "America/Kralendijk", + "America/La_Paz", + "America/Lima", + "America/Los_Angeles", + "America/Lower_Princes", + "America/Maceio", + "America/Managua", + "America/Manaus", + "America/Marigot", + "America/Martinique", + "America/Matamoros", + "America/Mazatlan", + "America/Menominee", + "America/Merida", + "America/Metlakatla", + "America/Mexico_City", + "America/Miquelon", + "America/Moncton", + "America/Monterrey", + "America/Montevideo", + "America/Montserrat", + "America/Nassau", + "America/New_York", + "America/Nipigon", + "America/Nome", + "America/Noronha", + "America/North_Dakota/Beulah", + "America/North_Dakota/Center", + "America/North_Dakota/New_Salem", + "America/Nuuk", + "America/Ojinaga", + "America/Panama", + "America/Pangnirtung", + "America/Paramaribo", + "America/Phoenix", + "America/Port-au-Prince", + "America/Port_of_Spain", + "America/Porto_Velho", + "America/Puerto_Rico", + "America/Punta_Arenas", + "America/Rainy_River", + "America/Rankin_Inlet", + "America/Recife", + "America/Regina", + "America/Resolute", + "America/Rio_Branco", + "America/Santarem", + "America/Santiago", + "America/Santo_Domingo", + "America/Sao_Paulo", + "America/Scoresbysund", + "America/Sitka", + "America/St_Barthelemy", + "America/St_Johns", + "America/St_Kitts", + "America/St_Lucia", + "America/St_Thomas", + "America/St_Vincent", + "America/Swift_Current", + "America/Tegucigalpa", + "America/Thule", + "America/Thunder_Bay", + "America/Tijuana", + "America/Toronto", + "America/Tortola", + "America/Vancouver", + "America/Whitehorse", + "America/Winnipeg", + "America/Yakutat", + "America/Yellowknife", + "Antarctica/Casey", + "Antarctica/Davis", + "Antarctica/DumontDUrville", + "Antarctica/Macquarie", + "Antarctica/Mawson", + "Antarctica/McMurdo", + "Antarctica/Palmer", + "Antarctica/Rothera", + "Antarctica/Syowa", + "Antarctica/Troll", + "Antarctica/Vostok", + "Arctic/Longyearbyen", + "Asia/Aden", + "Asia/Almaty", + "Asia/Amman", + "Asia/Anadyr", + "Asia/Aqtau", + "Asia/Aqtobe", + "Asia/Ashgabat", + "Asia/Atyrau", + "Asia/Baghdad", + "Asia/Bahrain", + "Asia/Baku", + "Asia/Bangkok", + "Asia/Barnaul", + "Asia/Beirut", + "Asia/Bishkek", + "Asia/Brunei", + "Asia/Chita", + "Asia/Choibalsan", + "Asia/Colombo", + "Asia/Damascus", + "Asia/Dhaka", + "Asia/Dili", + "Asia/Dubai", + "Asia/Dushanbe", + "Asia/Famagusta", + "Asia/Gaza", + "Asia/Hebron", + "Asia/Ho_Chi_Minh", + "Asia/Hong_Kong", + "Asia/Hovd", + "Asia/Irkutsk", + "Asia/Jakarta", + "Asia/Jayapura", + "Asia/Jerusalem", + "Asia/Kabul", + "Asia/Kamchatka", + "Asia/Karachi", + "Asia/Kathmandu", + "Asia/Khandyga", + "Asia/Kolkata", + "Asia/Krasnoyarsk", + "Asia/Kuala_Lumpur", + "Asia/Kuching", + "Asia/Kuwait", + "Asia/Macau", + "Asia/Magadan", + "Asia/Makassar", + "Asia/Manila", + "Asia/Muscat", + "Asia/Nicosia", + "Asia/Novokuznetsk", + "Asia/Novosibirsk", + "Asia/Omsk", + "Asia/Oral", + "Asia/Phnom_Penh", + "Asia/Pontianak", + "Asia/Pyongyang", + "Asia/Qatar", + "Asia/Qostanay", + "Asia/Qyzylorda", + "Asia/Riyadh", + "Asia/Sakhalin", + "Asia/Samarkand", + "Asia/Seoul", + "Asia/Shanghai", + "Asia/Singapore", + "Asia/Srednekolymsk", + "Asia/Taipei", + "Asia/Tashkent", + "Asia/Tbilisi", + "Asia/Tehran", + "Asia/Thimphu", + "Asia/Tokyo", + "Asia/Tomsk", + "Asia/Ulaanbaatar", + "Asia/Urumqi", + "Asia/Ust-Nera", + "Asia/Vientiane", + "Asia/Vladivostok", + "Asia/Yakutsk", + "Asia/Yangon", + "Asia/Yekaterinburg", + "Asia/Yerevan", + "Atlantic/Azores", + "Atlantic/Bermuda", + "Atlantic/Canary", + "Atlantic/Cape_Verde", + "Atlantic/Faroe", + "Atlantic/Madeira", + "Atlantic/Reykjavik", + "Atlantic/South_Georgia", + "Atlantic/St_Helena", + "Atlantic/Stanley", + "Australia/Adelaide", + "Australia/Brisbane", + "Australia/Broken_Hill", + "Australia/Darwin", + "Australia/Eucla", + "Australia/Hobart", + "Australia/Lindeman", + "Australia/Lord_Howe", + "Australia/Melbourne", + "Australia/Perth", + "Australia/Sydney", + "Canada/Atlantic", + "Canada/Central", + "Canada/Eastern", + "Canada/Mountain", + "Canada/Newfoundland", + "Canada/Pacific", + "Europe/Amsterdam", + "Europe/Andorra", + "Europe/Astrakhan", + "Europe/Athens", + "Europe/Belgrade", + "Europe/Berlin", + "Europe/Bratislava", + "Europe/Brussels", + "Europe/Bucharest", + "Europe/Budapest", + "Europe/Busingen", + "Europe/Chisinau", + "Europe/Copenhagen", + "Europe/Dublin", + "Europe/Gibraltar", + "Europe/Guernsey", + "Europe/Helsinki", + "Europe/Isle_of_Man", + "Europe/Istanbul", + "Europe/Jersey", + "Europe/Kaliningrad", + "Europe/Kiev", + "Europe/Kirov", + "Europe/Lisbon", + "Europe/Ljubljana", + "Europe/London", + "Europe/Luxembourg", + "Europe/Madrid", + "Europe/Malta", + "Europe/Mariehamn", + "Europe/Minsk", + "Europe/Monaco", + "Europe/Moscow", + "Europe/Oslo", + "Europe/Paris", + "Europe/Podgorica", + "Europe/Prague", + "Europe/Riga", + "Europe/Rome", + "Europe/Samara", + "Europe/San_Marino", + "Europe/Sarajevo", + "Europe/Saratov", + "Europe/Simferopol", + "Europe/Skopje", + "Europe/Sofia", + "Europe/Stockholm", + "Europe/Tallinn", + "Europe/Tirane", + "Europe/Ulyanovsk", + "Europe/Uzhgorod", + "Europe/Vaduz", + "Europe/Vatican", + "Europe/Vienna", + "Europe/Vilnius", + "Europe/Volgograd", + "Europe/Warsaw", + "Europe/Zagreb", + "Europe/Zaporozhye", + "Europe/Zurich", + "GMT", + "Indian/Antananarivo", + "Indian/Chagos", + "Indian/Christmas", + "Indian/Cocos", + "Indian/Comoro", + "Indian/Kerguelen", + "Indian/Mahe", + "Indian/Maldives", + "Indian/Mauritius", + "Indian/Mayotte", + "Indian/Reunion", + "Pacific/Apia", + "Pacific/Auckland", + "Pacific/Bougainville", + "Pacific/Chatham", + "Pacific/Chuuk", + "Pacific/Easter", + "Pacific/Efate", + "Pacific/Fakaofo", + "Pacific/Fiji", + "Pacific/Funafuti", + "Pacific/Galapagos", + "Pacific/Gambier", + "Pacific/Guadalcanal", + "Pacific/Guam", + "Pacific/Honolulu", + "Pacific/Kanton", + "Pacific/Kiritimati", + "Pacific/Kosrae", + "Pacific/Kwajalein", + "Pacific/Majuro", + "Pacific/Marquesas", + "Pacific/Midway", + "Pacific/Nauru", + "Pacific/Niue", + "Pacific/Norfolk", + "Pacific/Noumea", + "Pacific/Pago_Pago", + "Pacific/Palau", + "Pacific/Pitcairn", + "Pacific/Pohnpei", + "Pacific/Port_Moresby", + "Pacific/Rarotonga", + "Pacific/Saipan", + "Pacific/Tahiti", + "Pacific/Tarawa", + "Pacific/Tongatapu", + "Pacific/Wake", + "Pacific/Wallis", + "US/Alaska", + "US/Arizona", + "US/Central", + "US/Eastern", + "US/Hawaii", + "US/Mountain", + "US/Pacific", + "UTC", +] diff --git a/pykeg/core/util.py b/pykeg/core/util.py index d57bcd0ad..194da389f 100644 --- a/pykeg/core/util.py +++ b/pykeg/core/util.py @@ -3,11 +3,10 @@ # Note: imports should be limited to python stdlib, since methods here # may be used in models.py, settings.py, etc. +import importlib.util import logging import os -import pkgutil import tempfile -from builtins import object, str from collections import OrderedDict from contextlib import closing from importlib import metadata as importlib_metadata @@ -47,7 +46,7 @@ def should_upgrade(installed_verison, new_version): def get_user_agent(): - return "KegbotServer/%s" % get_version() + return f"KegbotServer/{get_version()}" def CtoF(t): @@ -58,10 +57,10 @@ def get_plugin_template_dirs(plugin_list): ret = [] for plugin in plugin_list: plugin_module = ".".join(plugin.split(".")[:-1]) - pkg = pkgutil.get_loader(plugin_module) - if not pkg: - raise ImproperlyConfigured('Cannot find plugin "%s"' % plugin) - template_dir = os.path.join(os.path.dirname(pkg.path), "templates") + spec = importlib.util.find_spec(plugin_module) + if not spec or not spec.origin: + raise ImproperlyConfigured(f'Cannot find plugin "{plugin}"') + template_dir = os.path.join(os.path.dirname(spec.origin), "templates") if os.path.isdir(template_dir): ret.append(template_dir) return ret @@ -89,14 +88,14 @@ def download_to_tempfile(url): r = requests.get(url, stream=True) ext = os.path.splitext(url)[1] fd, pathname = tempfile.mkstemp(suffix=ext) - logger.info("Downloading file %s to path %s" % (url, pathname)) + logger.info(f"Downloading file {url} to path {pathname}") with closing(os.fdopen(fd, "wb")): for chunk in r.iter_content(chunk_size=1024): if chunk: os.write(fd, chunk) return str(pathname) except requests.exceptions.RequestException as e: - raise IOError("Could not download file: {}".format(e)) + raise OSError(f"Could not download file: {e}") def get_runtime_version_info(): @@ -111,7 +110,7 @@ def get_runtime_version_info(): return OrderedDict(sorted(ret.items())) -class SuppressTaskErrors(object): +class SuppressTaskErrors: """Suppresses certain errors that occur while scheduling tasks.""" def __init__(self, logger=None): @@ -123,6 +122,6 @@ def __enter__(self): def __exit__(self, exc_type, exc_val, exc_tb): exc_info = (exc_type, exc_val, exc_tb) if isinstance(exc_val, RedisError): - self.logger.error("Error scheduling task: {}".format(exc_val), exc_info=exc_info) + self.logger.error(f"Error scheduling task: {exc_val}", exc_info=exc_info) return True return False diff --git a/pykeg/core/util_test.py b/pykeg/core/util_test.py index 738c25178..eb8736715 100644 --- a/pykeg/core/util_test.py +++ b/pykeg/core/util_test.py @@ -1,7 +1,5 @@ """Test for util module.""" -from builtins import str - from django.test import TestCase from packaging.version import Version diff --git a/pykeg/logging/logger.py b/pykeg/logging/logger.py index 784f837e5..76d83ca39 100644 --- a/pykeg/logging/logger.py +++ b/pykeg/logging/logger.py @@ -32,7 +32,6 @@ import inspect import logging import socket -from builtins import str from pykeg.core.util import get_current_request @@ -109,7 +108,7 @@ def __init__(self, name, lvl, fn, lno, msg, args, exc_info, func=None, extra=Non "line_no": self.lineno, "msg": str(msg), "args": list(args), - "time": datetime.datetime.utcnow(), + "time": datetime.datetime.now(datetime.UTC), "username": self.username, "funcname": self.funcname, "hostname": self.hostname, @@ -137,6 +136,6 @@ def makeRecord( if extra: for key in extra: if (key in ["message", "asctime"]) or (key in record.__dict__): - raise KeyError("Attempt to overwrite %r in RedisLogRecord" % key) + raise KeyError(f"Attempt to overwrite {key!r} in RedisLogRecord") record.__dict__[key] = extra[key] return record diff --git a/pykeg/notification/__init__.py b/pykeg/notification/__init__.py index 951e7589e..19ae42184 100644 --- a/pykeg/notification/__init__.py +++ b/pykeg/notification/__init__.py @@ -1,5 +1,4 @@ import logging -from builtins import str from django.conf import settings from django.core.exceptions import ImproperlyConfigured @@ -30,8 +29,8 @@ def handle_new_system_events(events): None """ backends = get_backends() - logger.info("Handling %s event(s)" % len(events)) - logger.info("Num backends: %s" % len(backends)) + logger.info(f"Handling {len(events)} event(s)") + logger.info(f"Num backends: {len(backends)}") for event in events: handle_single_event(event, backends) @@ -40,7 +39,7 @@ def handle_single_event(event, backends): from pykeg.core import models as core_models kind = event.kind - logger.info("Processing event: %s" % event.kind) + logger.info(f"Processing event: {event.kind}") for backend in backends: backend_name = backend.name() @@ -55,14 +54,14 @@ def handle_single_event(event, backends): elif kind == event.KEG_ENDED: prefs = prefs.filter(keg_ended=True) else: - logger.info("Unknown kind: %s" % kind) + logger.info(f"Unknown kind: {kind}") prefs = [] - logger.debug("Matching prefs: %s" % str(prefs)) + logger.debug(f"Matching prefs: {str(prefs)}") for matching_pref in prefs: user = matching_pref.user if not user.is_active: - logger.info("Skipping notification for inactive user %s" % user) + logger.info(f"Skipping notification for inactive user {user}") continue - logger.info("Notifying %s for event %s" % (user, kind)) + logger.info(f"Notifying {user} for event {kind}") backend.notify(event, user) diff --git a/pykeg/notification/backends/base.py b/pykeg/notification/backends/base.py index 104360da5..0a69d4b8a 100644 --- a/pykeg/notification/backends/base.py +++ b/pykeg/notification/backends/base.py @@ -1,10 +1,7 @@ """Base notification module.""" -from builtins import object - - -class BaseNotificationBackend(object): +class BaseNotificationBackend: """Base class for notification backend implementations.""" def name(self): diff --git a/pykeg/notification/backends/email.py b/pykeg/notification/backends/email.py index 18892a1a9..39d2703c8 100644 --- a/pykeg/notification/backends/email.py +++ b/pykeg/notification/backends/email.py @@ -13,11 +13,11 @@ def name(cls): return "pykeg.notification.backends.email.EmailNotificationBackend" def notify(self, event, user): - logger.info("Event %s -> user %s" % (event, user)) + logger.info(f"Event {event} -> user {user}") to_address = user.email if not to_address: - logger.warning("No e-mail address available for user %s" % user) + logger.warning(f"No e-mail address available for user {user}") return context = {} @@ -45,7 +45,7 @@ def notify(self, event, user): template_name = "notification/email_keg_volume_low.html" context["url"] = event.keg.full_url() else: - logger.info("Skipping unknown event type: %s" % event.kind) + logger.info(f"Skipping unknown event type: {event.kind}") return message = build_message(to_address, template_name, context) @@ -55,5 +55,5 @@ def notify(self, event, user): self.send_message(message) def send_message(self, message): - logger.info("Sending message: %s" % message) + logger.info(f"Sending message: {message}") message.send(fail_silently=True) diff --git a/pykeg/notification/backends/email_test.py b/pykeg/notification/backends/email_test.py index 0835ba434..07b8b2541 100644 --- a/pykeg/notification/backends/email_test.py +++ b/pykeg/notification/backends/email_test.py @@ -40,38 +40,34 @@ def test_keg_tapped(self): msg = mail.outbox[0] self.assertEqual( - "[My Kegbot] New keg tapped: Keg %s: Unknown by Unknown" % keg.id, msg.subject + f"[My Kegbot] New keg tapped: Keg {keg.id}: Unknown by Unknown", msg.subject ) self.assertEqual(["test@example"], msg.to) self.assertEqual("test-from@example", msg.from_email) - expected_body_plain = """A new keg of Unknown by Unknown was just tapped on My Kegbot! + expected_body_plain = f"""A new keg of Unknown by Unknown was just tapped on My Kegbot! -Track it here: http://test.example.com/kegs/%s/ +Track it here: http://test.example.com/kegs/{keg.id}/ You are receiving this e-mail because you have notifications enabled -on My Kegbot. To change your settings, visit http://test.example.com/account.""" % ( - keg.id, - ) +on My Kegbot. To change your settings, visit http://test.example.com/account.""" self.assertMultiLineEqual(expected_body_plain, msg.body) - expected_body_html = """

+ expected_body_html = f"""

A new keg of Unknown by Unknown was just tapped on My Kegbot!

-Track it here. +Track it here.

You are receiving this e-mail because you have notifications enabled on My Kegbot. To change your settings, visit http://test.example.com/account. -

""" % ( - keg.id, - ) +

""" self.assertEqual(1, len(msg.alternatives)) self.assertMultiLineEqual(expected_body_html, msg.alternatives[0][0]) @@ -94,37 +90,33 @@ def test_session_started(self): msg = mail.outbox[0] self.assertEqual( - "[My Kegbot] A new session (session %s) has started." % (drink.session.id,), msg.subject + f"[My Kegbot] A new session (session {drink.session.id}) has started.", msg.subject ) self.assertEqual(["test@example"], msg.to) self.assertEqual("test-from@example", msg.from_email) - expected_body_plain = """A new session was just kicked off on My Kegbot. + expected_body_plain = f"""A new session was just kicked off on My Kegbot. -You can follow the session here: http://test.example.com/s/%s/ +You can follow the session here: http://test.example.com/s/{drink.session.id}/ You are receiving this e-mail because you have notifications enabled -on My Kegbot. To change your settings, visit http://test.example.com/account.""" % ( - drink.session.id, - ) +on My Kegbot. To change your settings, visit http://test.example.com/account.""" self.assertMultiLineEqual(expected_body_plain, msg.body) - expected_body_html = """

+ expected_body_html = f"""

A new session was just kicked off on My Kegbot.

-You can follow the session here. +You can follow the session here.

You are receiving this e-mail because you have notifications enabled on My Kegbot. To change your settings, visit http://test.example.com/account. -

""" % ( - drink.session.id, - ) +

""" self.assertEqual(1, len(msg.alternatives)) self.assertMultiLineEqual(expected_body_html, msg.alternatives[0][0]) @@ -151,39 +143,33 @@ def test_keg_volume_low(self): msg = mail.outbox[0] self.assertEqual( - "[My Kegbot] Volume low on keg %s (Unknown by Unknown)" % keg.id, msg.subject + f"[My Kegbot] Volume low on keg {keg.id} (Unknown by Unknown)", msg.subject ) self.assertEqual(["test@example"], msg.to) self.assertEqual("test-from@example", msg.from_email) - expected_body_plain = """Keg %s (Unknown by Unknown) is 15.0%% full. + expected_body_plain = f"""Keg {keg.id} (Unknown by Unknown) is 15.0% full. -See full statistics here: http://test.example.com/kegs/%s/ +See full statistics here: http://test.example.com/kegs/{keg.id}/ You are receiving this e-mail because you have notifications enabled -on My Kegbot. To change your settings, visit http://test.example.com/account.""" % ( - keg.id, - keg.id, - ) +on My Kegbot. To change your settings, visit http://test.example.com/account.""" self.assertMultiLineEqual(expected_body_plain, msg.body) - expected_body_html = """

-Keg %s (Unknown by Unknown) is 15.0%% full. + expected_body_html = f"""

+Keg {keg.id} (Unknown by Unknown) is 15.0% full.

-See full statistics here. +See full statistics here.

You are receiving this e-mail because you have notifications enabled on My Kegbot. To change your settings, visit http://test.example.com/account. -

""" % ( - keg.id, - keg.id, - ) +

""" self.assertEqual(1, len(msg.alternatives)) self.assertMultiLineEqual(expected_body_html, msg.alternatives[0][0]) @@ -206,39 +192,33 @@ def test_keg_ended(self): self.assertEqual(1, len(mail.outbox)) msg = mail.outbox[0] - self.assertEqual("[My Kegbot] Keg ended: Keg %s: Unknown by Unknown" % keg.id, msg.subject) + self.assertEqual(f"[My Kegbot] Keg ended: Keg {keg.id}: Unknown by Unknown", msg.subject) self.assertEqual(["test@example"], msg.to) self.assertEqual("test-from@example", msg.from_email) - expected_body_plain = """Keg %s of Unknown by Unknown was just finished on My Kegbot. + expected_body_plain = f"""Keg {keg.id} of Unknown by Unknown was just finished on My Kegbot. -See final statistics here: http://test.example.com/kegs/%s/ +See final statistics here: http://test.example.com/kegs/{keg.id}/ You are receiving this e-mail because you have notifications enabled -on My Kegbot. To change your settings, visit http://test.example.com/account.""" % ( - keg.id, - keg.id, - ) +on My Kegbot. To change your settings, visit http://test.example.com/account.""" self.assertMultiLineEqual(expected_body_plain, msg.body) - expected_body_html = """

-Keg %s (Unknown by Unknown) was just finished on + expected_body_html = f"""

+Keg {keg.id} (Unknown by Unknown) was just finished on My Kegbot.

-See final statistics here. +See final statistics here.

You are receiving this e-mail because you have notifications enabled on My Kegbot. To change your settings, visit http://test.example.com/account. -

""" % ( - keg.id, - keg.id, - ) +

""" self.assertEqual(1, len(msg.alternatives)) self.assertMultiLineEqual(expected_body_html, msg.alternatives[0][0]) diff --git a/pykeg/notification/forms.py b/pykeg/notification/forms.py index 290f735f4..3e3bb85ab 100644 --- a/pykeg/notification/forms.py +++ b/pykeg/notification/forms.py @@ -1,11 +1,9 @@ -from builtins import object - from django import forms from pykeg.core import models class NotificationSettingsForm(forms.ModelForm): - class Meta(object): + class Meta: model = models.NotificationSettings exclude = ["user", "backend"] diff --git a/pykeg/notification/notification_test.py b/pykeg/notification/notification_test.py index 6241d24db..74a86d664 100644 --- a/pykeg/notification/notification_test.py +++ b/pykeg/notification/notification_test.py @@ -1,6 +1,5 @@ """Unittests for notification module.""" -from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.test import TestCase from django.test.utils import override_settings diff --git a/pykeg/plugin/datastore.py b/pykeg/plugin/datastore.py index 5ec56fd20..ea67e4346 100644 --- a/pykeg/plugin/datastore.py +++ b/pykeg/plugin/datastore.py @@ -1,13 +1,11 @@ """Data storage interface and implementations for plugins.""" -from builtins import object - from addict import Dict from pykeg.core import models -class PluginDatastore(object): +class PluginDatastore: """Interface for plugins to persist plugin-specific data.""" def __init__(self, plugin_name): @@ -27,13 +25,13 @@ def delete(self, key): def save_form(self, form, prefix): """Helper method to save a form using the specified per-field prefix.""" for field_name, value in list(form.cleaned_data.items()): - self.set("%s:%s" % (prefix, field_name), value) + self.set(f"{prefix}:{field_name}", value) def load_form(self, form_cls, prefix, form_kwargs={}): """Helper method to load a form using the specified per-field prefix.""" data = Dict() for field_name, field in list(form_cls.base_fields.items()): - initial = self.get("%s:%s" % (prefix, field_name)) + initial = self.get(f"{prefix}:{field_name}") if initial is not None: data[field_name] = field.to_python(initial) else: @@ -72,12 +70,12 @@ def delete(self, key): class InMemoryDatastore(PluginDatastore): def __init__(self, *args, **kwargs): - super(InMemoryDatastore, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.data = {} def _keyname(self, key): """Returns the datastore-namespaced key name.""" - return "{}:{}".format(self.plugin_name, key) + return f"{self.plugin_name}:{key}" def set(self, key, value): if value is None: diff --git a/pykeg/plugin/plugin.py b/pykeg/plugin/plugin.py index d736acd89..ec7143770 100644 --- a/pykeg/plugin/plugin.py +++ b/pykeg/plugin/plugin.py @@ -1,12 +1,11 @@ """Plugin interface for extending the Kegbot frontend.""" import logging -from builtins import object from pykeg.plugin.datastore import ModelDatastore -class Plugin(object): +class Plugin: """Interface class for plugins.""" NAME = None diff --git a/pykeg/plugin/util.py b/pykeg/plugin/util.py index fa8cb79f2..81dae99c0 100644 --- a/pykeg/plugin/util.py +++ b/pykeg/plugin/util.py @@ -3,8 +3,8 @@ from importlib import import_module from django.conf import settings -from django.conf.urls import url from django.core.exceptions import ImproperlyConfigured +from django.urls import re_path from django.utils import timezone from .plugin import Plugin @@ -20,7 +20,7 @@ def get_plugin_class(name): module = import_module(module_path) cls = getattr(module, member_name) except (ValueError, ImportError, AttributeError) as e: - raise ImproperlyConfigured("Could not import plugin %s: %s" % (name, e)) + raise ImproperlyConfigured(f"Could not import plugin {name}: {e}") if not issubclass(cls, Plugin): raise ImproperlyConfigured("%s does not subclass plugin.Plugin", name) @@ -38,7 +38,7 @@ def get_plugins(): cls = get_plugin_class(name) plugin_obj = cls(plugin_registry=_CACHED_PLUGINS) short_name = plugin_obj.get_short_name() - assert short_name not in plugins, "Multiple plugins named {}".format(short_name) + assert short_name not in plugins, f"Multiple plugins named {short_name}" plugins[short_name] = plugin_obj _CACHED_PLUGINS = plugins return _CACHED_PLUGINS @@ -61,9 +61,9 @@ def get_account_urls(): def _to_urls(urllist, short_name): urls = [] for regex, fn, viewname in urllist: - regex = "plugin/%s/%s" % (short_name, regex) - viewname = "plugin-%s-%s" % (short_name, viewname) - urls.append(url(regex, fn, name=viewname)) + regex = f"plugin/{short_name}/{regex}" + viewname = f"plugin-{short_name}-{viewname}" + urls.append(re_path(regex, fn, name=viewname)) return urls diff --git a/pykeg/proto/exceptions.py b/pykeg/proto/exceptions.py index 614fad528..7227d16c9 100644 --- a/pykeg/proto/exceptions.py +++ b/pykeg/proto/exceptions.py @@ -1,46 +1,62 @@ class Error(Exception): - """An error occurred.""" - HTTP_CODE = 400 - def __init__(self, message=None): - self.message = message - - def Message(self): - if self.message: - return self.message - m = self.__class__.__doc__ - m = m.split('\n', 1)[0] - return m + """An error occurred.""" + + HTTP_CODE = 400 + + def __init__(self, message=None): + self.message = message + + def Message(self): + if self.message: + return self.message + m = self.__class__.__doc__ + m = m.split('\n', 1)[0] + return m + class NotFoundError(Error): - """The requested object could not be found.""" - HTTP_CODE = 404 + """The requested object could not be found.""" + + HTTP_CODE = 404 + class RequestError(Error): - """There was an error fufilling the request.""" + """There was an error fufilling the request.""" + class ServerError(Error): - """The server had a problem fulfilling your request.""" - HTTP_CODE = 500 + """The server had a problem fulfilling your request.""" + + HTTP_CODE = 500 + class BadRequestError(Error): - """The request was incomplete or malformed.""" - HTTP_CODE = 400 + """The request was incomplete or malformed.""" + + HTTP_CODE = 400 + class NoAuthTokenError(Error): - """An api_key is required.""" - HTTP_CODE = 401 + """An api_key is required.""" + + HTTP_CODE = 401 + class BadApiKeyError(Error): - """The api_key given is invalid.""" - HTTP_CODE = 401 + """The api_key given is invalid.""" + + HTTP_CODE = 401 + class PermissionDeniedError(Error): - """The api_key given does not have permission for this resource.""" - HTTP_CODE = 401 + """The api_key given does not have permission for this resource.""" + + HTTP_CODE = 401 + MAP_NAME_TO_EXCEPTION = dict((c.__name__, c) for c in Error.__subclasses__()) -def ErrorCodeToException(code, message=ServerError): - cls = MAP_NAME_TO_EXCEPTION.get(code, Error) - return cls(message) +def ErrorCodeToException(code, message=ServerError): + cls = MAP_NAME_TO_EXCEPTION.get(code, Error) + return cls(message) diff --git a/pykeg/proto/kbapi.py b/pykeg/proto/kbapi.py index 51d31c9d6..fbff98164 100644 --- a/pykeg/proto/kbapi.py +++ b/pykeg/proto/kbapi.py @@ -1,219 +1,31 @@ -"""Kegweb API client.""" - -import datetime -from builtins import object - -import gflags -import requests - -from .exceptions import * -from .util import AttrDict - -gflags.DEFINE_float( - "api_timeout", - 10.0, - "Socket timeout, in seconds, for Kegbot web API operations. " - "Note that this timeout only applies to blocking socket operations " - "(such as opening a connection) and not I/O.", +"""Kegweb API exceptions. + +These are re-exported here so server code can `from pykeg.proto import kbapi` +and reference e.g. `kbapi.NoAuthTokenError`. The old gflags-based HTTP client +lived here too; it was unused by the server and now lives in the kegbot-api +project. +""" + +from .exceptions import ( + BadApiKeyError, + BadRequestError, + Error, + ErrorCodeToException, + NoAuthTokenError, + NotFoundError, + PermissionDeniedError, + RequestError, + ServerError, ) -FLAGS = gflags.FLAGS - -_DEFAULT_URL = "http://localhost:8000/api/" -_DEFAULT_KEY = "" -try: - from pykeg import settings - - if hasattr(settings, "KEGWEB_BASE_URL"): - _DEFAULT_URL = "%s/api/" % getattr(settings, "KEGWEB_BASE_URL") - if hasattr(settings, "KEGWEB_API_KEY"): - _DEFAULT_KEY = settings.KEGWEB_API_KEY -except ImportError: - # Non-fatal if we can't load settings. - pass - -gflags.DEFINE_string("api_url", _DEFAULT_URL, "Base URL for the Kegweb HTTP api.") - -gflags.DEFINE_string("api_key", _DEFAULT_KEY, "Access key for the Kegweb HTTP api.") - -### begin common - - -def decode_response(response): - """Decodes the requests response object as a JSON response. - - For normal responses, the return value is the Python JSON-decoded 'result' - field of the response. If the response is an error, a RemoteError exception - is raised. - """ - - status_code = response.status_code - response_dict = response.json() - - if "error" in response_dict: - # Response had an error: translate to exception. - err = response_dict["error"] - code = err.get("code", status_code) - message = err.get("message", None) - e = ErrorCodeToException(code, message) - raise e - elif "object" in response_dict or "objects" in response_dict: - # Response was OK, return the result. - return AttrDict(response_dict) - else: - # WTF? - raise ValueError("Invalid response from server: missing result or error") - - -### end common - - -class Client(object): - """Kegweb RESTful API client.""" - - def __init__(self, api_url=None, api_key=None): - if api_url is None: - api_url = FLAGS.api_url - if api_key is None: - api_key = FLAGS.api_key - self._api_url = api_url - self._api_key = api_key - - def _get_url(self, endpoint): - base = self._api_url.rstrip("/") - endpoint = endpoint.strip("/") - return "%s/%s" % (base, endpoint) - - def _http_get(self, endpoint, params=None): - """Issues a GET request to the endpoint, and retuns the result. - - Keyword arguments are passed to the endpoint as GET arguments. - - For normal responses, the return value is the Python JSON-decoded 'object' - or 'objects' field of the response. If the response is an error, a - RemoteError exception is raised. - - If there was an error contacting the server, or in parsing its response, a - ServerError is raised. - """ - return self._http_request(endpoint, params=params) - - def _http_post(self, endpoint, post_data, params=None): - """Issues a POST request to the endpoint, and returns the result. - - For normal responses, the return value is the Python JSON-decoded 'object' - or 'objects' field of the response. If the response is an error, a - RemoteError exception is raised. - - If there was an error contacting the server, or in parsing its response, a - ServerError is raised. - """ - return self._http_request(endpoint, params=params, post_data=post_data) - - def _http_request(self, endpoint, params=None, post_data=None): - """Issues a POST or GET request, depending on the arguments.""" - headers = { - "X-Kegbot-Api-Key": self._api_key, - } - url = self._get_url(endpoint) - - try: - if post_data: - r = requests.post( - url, params=params, data=post_data, headers=headers, timeout=FLAGS.api_timeout - ) - else: - r = requests.get(url, params=params, headers=headers, timeout=FLAGS.api_timeout) - except requests.exceptions.RequestException as e: - raise RequestError(e) - - return decode_response(r) - - def record_drink( - self, - tap_name, - ticks, - volume_ml=None, - username=None, - pour_time=None, - duration=0, - auth_token=None, - spilled=False, - shout="", - ): - endpoint = "/taps/%s" % tap_name - post_data = { - "tap_name": tap_name, - "ticks": ticks, - } - if volume_ml is not None: - post_data["volume_ml"] = volume_ml - if username is not None: - post_data["username"] = username - if duration > 0: - post_data["duration"] = duration - if auth_token is not None: - post_data["auth_token"] = auth_token - if spilled: - post_data["spilled"] = spilled - if shout: - post_data["shout"] = shout - if pour_time: - post_data["pour_time"] = int(pour_time.strftime("%s")) - post_data["now"] = int(datetime.datetime.now().strftime("%s")) - return self._http_post(endpoint, post_data=post_data).object - - def cancel_drink(self, seqn, spilled=False): - endpoint = "/cancel-drink" - post_data = { - "id": seqn, - "spilled": spilled, - } - return self._http_post(endpoint, post_data=post_data).object - - def log_sensor_reading(self, sensor_name, temperature, when=None): - endpoint = "/thermo-sensors/%s" % (sensor_name,) - post_data = { - "temp_c": float(temperature), - } - # TODO(mikey): include post data - return self._http_post(endpoint, post_data=post_data).object - - def status(self): - """Gets complete system status.""" - return self._http_get("status").object - - def taps(self): - """Gets the status of all taps.""" - return self._http_get("taps").objects - - def get_token(self, auth_device, token_value): - url = "auth-tokens/%s/%s" % (auth_device, token_value) - try: - return self._http_get(url).object - except ServerError as e: - raise NotFoundError(e) - - def drinks(self): - """Gets a list of all drinks.""" - return self._http_get("drinks").objects - - def sound_events(self): - """Gets a list of all sound events.""" - return self._http_get("sound-events").objects - - def create_controller(self, controller_name, model_name="unknown", serial_number="unknown"): - post_data = { - "name": controller_name, - "model_name": model_name, - "serial_number": serial_number, - } - return self._http_post("controllers", post_data=post_data).object - - def create_flow_meter(self, controller_id, port_name, ticks_per_ml=2.2): - post_data = { - "port_name": port_name, - "controller": controller_id, - "ticks_per_ml": ticks_per_ml, - } - return self._http_post("flow-meters", post_data=post_data).object +__all__ = [ + "BadApiKeyError", + "BadRequestError", + "Error", + "ErrorCodeToException", + "NoAuthTokenError", + "NotFoundError", + "PermissionDeniedError", + "RequestError", + "ServerError", +] diff --git a/pykeg/proto/protolib.py b/pykeg/proto/protolib.py index a96e874f7..f0b4ea864 100644 --- a/pykeg/proto/protolib.py +++ b/pykeg/proto/protolib.py @@ -32,7 +32,7 @@ def ToProto(obj, full=False): elif kind in _CONVERSION_MAP: return _CONVERSION_MAP[kind](obj, full) else: - raise ValueError("Unknown object type: %s" % kind) + raise ValueError(f"Unknown object type: {kind}") def ToDict(obj, full=False): @@ -326,7 +326,7 @@ def KegTapToProto(tap, full=False): ret.meter.MergeFrom(ToProto(meter)) else: # TODO(mikey): Remove compatibility. - ret.meter_name = "unknown.%s" % tap.id + ret.meter_name = f"unknown.{tap.id}" ret.ml_per_tick = 0 toggle = tap.current_toggle() diff --git a/pykeg/proto/protoutil.py b/pykeg/proto/protoutil.py index 0df9b9628..ac1e8e13b 100644 --- a/pykeg/proto/protoutil.py +++ b/pykeg/proto/protoutil.py @@ -2,15 +2,13 @@ import datetime -TIME_ZONE = "UTC" - def ProtoMessageToDict(message): ret = {} # if not message.IsInitialized(): # raise ValueError, 'Message not initialized' for descriptor, value in message.ListFields(): - if descriptor.label == descriptor.LABEL_REPEATED: + if descriptor.is_repeated: if descriptor.type == descriptor.TYPE_MESSAGE: ret[descriptor.name] = [ProtoMessageToDict(v) for v in value] else: @@ -26,27 +24,27 @@ def ProtoMessageToDict(message): def DictToProtoMessage(values, out_message): for name, field in out_message.DESCRIPTOR.fields_by_name.items(): if name not in values: - if field.label == field.LABEL_REQUIRED: + if field.is_required: raise ValueError(f"Missing required field {name}") continue value = values.get(name) if field.type == field.TYPE_MESSAGE: inner_message = getattr(out_message, name) - if field.label == field.LABEL_REPEATED: + if field.is_repeated: for subval in value: DictToProtoMessage(subval, inner_message.add()) else: DictToProtoMessage(value, inner_message) else: - if field.label == field.LABEL_REPEATED: + if field.is_repeated: out = getattr(out_message, name) for v in value: if isinstance(v, datetime.datetime): - v = util.datetime_to_iso8601str(v, TIME_ZONE) + v = v.isoformat() out.append(v) else: if isinstance(value, datetime.datetime): - value = util.datetime_to_iso8601str(value, TIME_ZONE) + value = value.isoformat() setattr(out_message, name, value) return out_message diff --git a/pykeg/proto/util.py b/pykeg/proto/util.py index 005f51e89..418ffb950 100644 --- a/pykeg/proto/util.py +++ b/pykeg/proto/util.py @@ -1,25 +1,25 @@ class AttrDict(dict): - """Dict that exposes items as attributes. + """Dict that exposes items as attributes. - Source: https://stackoverflow.com/a/48806603 - """ + Source: https://stackoverflow.com/a/48806603 + """ - def __init__(self, mapping=None): - super(AttrDict, self).__init__() - if mapping is not None: - for key, value in mapping.items(): - self.__setitem__(key, value) + def __init__(self, mapping=None): + super().__init__() + if mapping is not None: + for key, value in mapping.items(): + self.__setitem__(key, value) - def __setitem__(self, key, value): - if isinstance(value, dict): - value = AttrDict(value) - super(AttrDict, self).__setitem__(key, value) - self.__dict__[key] = value # for code completion in editors + def __setitem__(self, key, value): + if isinstance(value, dict): + value = AttrDict(value) + super().__setitem__(key, value) + self.__dict__[key] = value # for code completion in editors - def __getattr__(self, item): - try: - return self.__getitem__(item) - except KeyError: - raise AttributeError(item) + def __getattr__(self, item): + try: + return self.__getitem__(item) + except KeyError: + raise AttributeError(item) - __setattr__ = __setitem__ + __setattr__ = __setitem__ diff --git a/pykeg/settings.py b/pykeg/settings.py index c92b95e20..d9fb5f68d 100644 --- a/pykeg/settings.py +++ b/pykeg/settings.py @@ -9,7 +9,6 @@ import os import dj_database_url -import dj_email_url from pykeg import config from pykeg.config import ENV_PRODUCTION, ENV_TEST @@ -48,9 +47,8 @@ "django.contrib.sessions", "django.contrib.staticfiles", "crispy_forms", - "bootstrap_pagination", + "crispy_bootstrap4", "imagekit", - "gunicorn", "corsheaders", "rest_framework", "django_rq", @@ -73,13 +71,27 @@ # During tests, whitenoise's manifest will not be available and # errors will be thrown while trying to access static files. # Use the default static backend to work around this. - STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage" + _STATICFILES_BACKEND = "django.contrib.staticfiles.storage.StaticFilesStorage" else: - STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" + _STATICFILES_BACKEND = "whitenoise.storage.CompressedManifestStaticFilesStorage" + +# Storage backends (Django 5.1+ replaced DEFAULT_FILE_STORAGE / STATICFILES_STORAGE). +STORAGES = { + "default": { + "BACKEND": "pykeg.web.kegweb.kbstorage.KegbotFileSystemStorage", + }, + "staticfiles": { + "BACKEND": _STATICFILES_BACKEND, + }, +} + +# crispy-forms 2.x template pack (Bootstrap 4). +CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap4" +CRISPY_TEMPLATE_PACK = "bootstrap4" # Default session serialization. -SESSION_SERIALIZER = "django.contrib.sessions.serializers.PickleSerializer" +SESSION_SERIALIZER = "django.contrib.sessions.serializers.JSONSerializer" # Kegweb specific stuff @@ -163,6 +175,19 @@ }, } +# When running the test suite without an explicit REDIS_URL, back every redis +# cache with an in-process fakeredis so the suite needs no redis server. CI sets +# REDIS_URL and continues to exercise a real redis. +if config.IS_RUNNING_PYTEST and "REDIS_URL" not in os.environ: + import fakeredis + + for _cache in CACHES.values(): + if _cache.get("BACKEND") == "django_redis.cache.RedisCache": + _cache["LOCATION"] = "redis://localhost:6379/0" + _cache["OPTIONS"]["CONNECTION_POOL_KWARGS"] = { + "connection_class": fakeredis.FakeRedisConnection + } + INTERNAL_IPS = ("127.0.0.1",) # Set to true if the database admin module should be enabled. @@ -185,6 +210,11 @@ }, } +# django-rq defers enqueue until the DB transaction commits. Under pytest the +# suite runs inside TestCase (which rolls back and never fires on_commit), so +# enqueue immediately there to keep synchronous (ASYNC=False) jobs running inline. +RQ = {"COMMIT_MODE": "auto" if config.IS_RUNNING_PYTEST else "on_db_commit"} + # logging LOGGING = { @@ -267,10 +297,9 @@ # Imagekit IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND = "imagekit.imagecache.NonValidatingImageCacheBackend" -# Storage -DEFAULT_FILE_STORAGE = "pykeg.web.kegweb.kbstorage.KegbotFileSystemStorage" +# Storage is configured via STORAGES above (Django 5.1+). -from pykeg.core.util import get_plugin_template_dirs +from pykeg.core.util import get_plugin_template_dirs # noqa: E402 (needs KEGBOT_PLUGINS above) TEMPLATES = [ { @@ -317,7 +346,8 @@ "http://127.0.0.1:1234", ] CSRF_TRUSTED_ORIGINS = [ - "localhost:1234", + "http://localhost:1234", + "http://127.0.0.1:1234", ] CORS_ALLOW_CREDENTIALS = True SESSION_COOKIE_SAMESITE = None diff --git a/pykeg/test/plugin.py b/pykeg/test/plugin.py index 84c9ca2ed..86069d431 100644 --- a/pykeg/test/plugin.py +++ b/pykeg/test/plugin.py @@ -4,7 +4,6 @@ import tempfile import pytest -from django.conf import settings logger = logging.getLogger("pykeg.test.plugin") TEMP_DATA_DIR = None @@ -15,10 +14,10 @@ def pytest_load_initial_conftests(early_config, parser, args): global TEMP_DATA_DIR TEMP_DATA_DIR = tempfile.mkdtemp() os.environ["KEGBOT_DATA_DIR"] = TEMP_DATA_DIR - logger.info("setting KEGBOT_DATA_DIR to {}".format(TEMP_DATA_DIR)) + logger.info(f"setting KEGBOT_DATA_DIR to {TEMP_DATA_DIR}") @pytest.hookimpl def pytest_sessionfinish(session, exitstatus): - logger.info("removing KEGBOT_DATA_DIR {}".format(TEMP_DATA_DIR)) + logger.info(f"removing KEGBOT_DATA_DIR {TEMP_DATA_DIR}") shutil.rmtree(TEMP_DATA_DIR) diff --git a/pykeg/util/bugreport.py b/pykeg/util/bugreport.py index d30c5b9c6..adda6bb76 100644 --- a/pykeg/util/bugreport.py +++ b/pykeg/util/bugreport.py @@ -34,7 +34,7 @@ def writelog(fd, log): method = request_info.get("method") path = request_info.get("request_path") addr = request_info.get("addr") - writeline(fd, " * Request: {} {} ({})".format(method, path, addr)) + writeline(fd, f" * Request: {method} {path} ({addr})") traceback = log.get("traceback") if traceback: @@ -54,13 +54,13 @@ def get_output(cmd): .strip() ) except subprocess.CalledProcessError as e: - return "ERR ({})".format(e) + return f"ERR ({e})" def bugreport(fd): now = datetime.datetime.now() - writeline(fd, "Kegbot Server {} Bugreport".format(get_version())) - writeline(fd, "Generated {}".format(isodate.datetime_isoformat(now))) + writeline(fd, f"Kegbot Server {get_version()} Bugreport") + writeline(fd, f"Generated {isodate.datetime_isoformat(now)}") fd.write(SEPARATOR) writeline(fd, "## System info\n") @@ -101,7 +101,7 @@ def bugreport(fd): continue writelog(fd, log) except redis.RedisError as e: - writeline(fd, "ERR ({})".format(e)) + writeline(fd, f"ERR ({e})") writeline(fd, "\n") fd.write(SEPARATOR) diff --git a/pykeg/util/dbstatus.py b/pykeg/util/dbstatus.py index 09896b89c..807f2cdb6 100644 --- a/pykeg/util/dbstatus.py +++ b/pykeg/util/dbstatus.py @@ -16,7 +16,7 @@ class NeedMigration(DatabaseStatusError): """Thrown when a migration is needed.""" def __init__(self, migration_name): - super(NeedMigration, self).__init__('Migration "{}" not applied'.format(migration_name)) + super().__init__(f'Migration "{migration_name}" not applied') self.migration_name = migration_name diff --git a/pykeg/util/email_test.py b/pykeg/util/email_test.py index d31f52084..91109f076 100644 --- a/pykeg/util/email_test.py +++ b/pykeg/util/email_test.py @@ -1,7 +1,6 @@ """Test for email util module.""" import time -from builtins import str from django.test import TestCase diff --git a/pykeg/util/kbjson.py b/pykeg/util/kbjson.py index 7cace1dcf..6dd32ea54 100644 --- a/pykeg/util/kbjson.py +++ b/pykeg/util/kbjson.py @@ -6,7 +6,6 @@ import datetime import json -import types import isodate from addict import Dict @@ -32,7 +31,7 @@ def _ToAttrDict(obj): format used in JSONEncoder. If it does not parse, the value will be left as a string. """ - if type(obj) == dict: + if type(obj) is dict: # Try to convert any "time" or "date" fields into datetime objects. If the # format doesn't match, just leave it alone. for k, v in list(obj.items()): diff --git a/pykeg/util/runner.py b/pykeg/util/runner.py index 71e0b5f5b..1b33f825b 100644 --- a/pykeg/util/runner.py +++ b/pykeg/util/runner.py @@ -49,7 +49,7 @@ def is_running(self): return self.running def add_command(self, command_name, command): - logger.debug('Adding command "{}": {}'.format(command_name, command)) + logger.debug(f'Adding command "{command_name}": {command}') if command_name in self.commands: raise ValueError("Command already installed") self.commands[command_name] = command @@ -60,7 +60,7 @@ def run(self): self.running = True self.print_startup_line() - self.logger.info("Starting commands from pid={}".format(os.getpid())) + self.logger.info(f"Starting commands from pid={os.getpid()}") dev_null_name = getattr(os, "devnull", "/dev/null") dev_null = os.open(dev_null_name, os.O_RDWR) @@ -69,7 +69,7 @@ def run(self): if sys.argv[0]: d = os.path.dirname(sys.argv[0]) if d not in path.split(":"): - path = "{}:{}".format(d, path) + path = f"{d}:{path}" user = os.environ.get("USER", "") if not user: @@ -82,27 +82,25 @@ def run(self): env["PATH"] = path env["USER"] = user - self.logger.debug("env={}".format(repr(env))) + self.logger.debug(f"env={repr(env)}") for command_name, command in list(self.commands.items()): proc = self._launch_command(command_name, command, dev_null, env) - self.logger.info("Started {} (pid={})".format(command_name, proc.pid)) + self.logger.info(f"Started {command_name} (pid={proc.pid})") self.watched_procs[command_name] = proc self.watch_commands() def watch_commands(self): - self.logger.info("Watching {} processes.".format(len(self.commands))) + self.logger.info(f"Watching {len(self.commands)} processes.") while True: abort = False for command_name, proc in list(self.watched_procs.items()): - self.logger.debug("Pinging {} (pid={})".format(command_name, proc.pid)) + self.logger.debug(f"Pinging {command_name} (pid={proc.pid})") proc.poll() if proc.returncode is not None: self.logger.info( - 'Process "{}" exited with returncode {}'.format( - command_name, proc.returncode - ) + f'Process "{command_name}" exited with returncode {proc.returncode}' ) abort = True if abort: @@ -114,16 +112,16 @@ def abort(self): self.logger.info("Abort called, killing remaining processes ...") for command_name, proc in list(self.watched_procs.items()): if proc.returncode is None: - self.logger.info("Killing {} (pid={})".format(command_name, proc.pid)) + self.logger.info(f"Killing {command_name} (pid={proc.pid})") os.killpg(proc.pid, signal.SIGTERM) for command_name, proc in list(self.watched_procs.items()): - self.logger.info("Waiting for {} to exit (pid={}) ...".format(command_name, proc.pid)) + self.logger.info(f"Waiting for {command_name} to exit (pid={proc.pid}) ...") proc.wait() self.logger.info("... done.") self.logger.info("All processes exited.") def _launch_command(self, command_name, command, dev_null, env=None): - self.logger.info("Launching command: {}: {}".format(command_name, command)) + self.logger.info(f"Launching command: {command_name}: {command}") def preexec(): # Set session id. diff --git a/pykeg/util/units.py b/pykeg/util/units.py index 25ba104e9..792f6a601 100644 --- a/pykeg/util/units.py +++ b/pykeg/util/units.py @@ -1,5 +1,3 @@ -import types -from builtins import object from enum import Enum @@ -20,7 +18,7 @@ class UNITS(Enum): Hogshead = 238480.9434 -class Quantity(object): +class Quantity: def __init__(self, amount, units=UNITS.Milliliter, from_units=None): self._units = units self._amount = self.convert(amount, units, from_units) if from_units else amount @@ -29,10 +27,10 @@ def __init__(self, amount, units=UNITS.Milliliter, from_units=None): def fn(unit=unit): return self.ConvertTo(unit)._amount - setattr(self, "In{}s".format(unit.name), fn) + setattr(self, f"In{unit.name}s", fn) def __str__(self): - return "{} {}".format(self._amount, self._units.name) + return f"{self._amount} {self._units.name}" def __add__(self, other, subtract=False): val = 0 diff --git a/pykeg/web/account/views.py b/pykeg/web/account/views.py index 19b3a4f1c..378e334d6 100644 --- a/pykeg/web/account/views.py +++ b/pykeg/web/account/views.py @@ -8,7 +8,6 @@ from django.contrib.auth.decorators import login_required from django.http import Http404 from django.shortcuts import redirect, render -from django.urls import reverse from django.views.decorators.cache import never_cache from django.views.decorators.http import require_POST @@ -106,7 +105,7 @@ def notifications(request): ) message.send() messages.success( - request, "An e-mail confirmation has been sent to {}".format(new_email) + request, f"An e-mail confirmation has been sent to {new_email}" ) else: @@ -185,7 +184,7 @@ def regenerate_api_key(request): def plugin_settings(request, plugin_name): plugin = request.plugins.get(plugin_name, None) if not plugin: - raise Http404('Plugin "%s" not loaded' % plugin_name) + raise Http404(f'Plugin "{plugin_name}" not loaded') view = plugin.get_user_settings_view() if not view: diff --git a/pykeg/web/api/api_test.py b/pykeg/web/api/api_test.py index ae6f0015f..62bcd799a 100644 --- a/pykeg/web/api/api_test.py +++ b/pykeg/web/api/api_test.py @@ -1,7 +1,5 @@ """Unittests for pykeg.web.api""" -from builtins import str - from django.core import mail from django.test import TestCase from django.test.utils import override_settings @@ -21,15 +19,15 @@ def create_site(): class BaseApiTestCase(TestCase): def get(self, subpath, data={}, follow=False, **extra): - response = self.client.get("/api/%s" % subpath, data=data, follow=follow, **extra) + response = self.client.get(f"/api/{subpath}", data=data, follow=follow, **extra) return response, kbjson.loads(response.content) def post(self, subpath, data={}, follow=False, **extra): - response = self.client.post("/api/%s" % subpath, data=data, follow=follow, **extra) + response = self.client.post(f"/api/{subpath}", data=data, follow=follow, **extra) return response, kbjson.loads(response.content) def delete(self, subpath, data={}, follow=False, **extra): - response = self.client.delete("/api/%s" % subpath, data=data, follow=follow, **extra) + response = self.client.delete(f"/api/{subpath}", data=data, follow=follow, **extra) return response, kbjson.loads(response.content) @@ -82,10 +80,10 @@ def test_defaults(self): self.assertEqual("kegboard.flow1", taps[1].meter_name) for tap in taps: - response1, data1 = self.get("taps/%s" % tap.meter_name) + response1, data1 = self.get(f"taps/{tap.meter_name}") self.assertEqual(data1.meta.result, "ok") - response2, data2 = self.get("taps/%s" % tap.id) + response2, data2 = self.get(f"taps/{tap.id}") self.assertEqual(data2.meta.result, "ok") self.assertEqual(data1, data2) @@ -337,8 +335,8 @@ def test_controller_data(self): def test_add_remove_meters(self): response, data = self.get(f"taps/{self.tap.id}", HTTP_X_KEGBOT_API_KEY=self.apikey.key) self.assertEqual(data.meta.result, "ok") - meters = models.FlowMeter.objects.all() - self.assertEqual(data.object.meter.id, meters[0].id) + meter = models.FlowMeter.objects.get(tap=self.tap) + self.assertEqual(data.object.meter.id, meter.id) original_data = data response, data = self.post( @@ -350,18 +348,17 @@ def test_add_remove_meters(self): response, data = self.post( f"taps/{self.tap.id}/connect-meter", HTTP_X_KEGBOT_API_KEY=self.apikey.key, - data={"meter": meters[0].id}, + data={"meter": meter.id}, ) self.assertEqual(data.meta.result, "ok") - meters = models.FlowMeter.objects.all() - self.assertEqual(data.object.meter.id, meters[0].id) + self.assertEqual(data.object.meter.id, meter.id) self.assertEqual(original_data, data) def test_add_remove_toggles(self): response, data = self.get(f"taps/{self.tap.id}", HTTP_X_KEGBOT_API_KEY=self.apikey.key) self.assertEqual(data.meta.result, "ok") - toggles = models.FlowToggle.objects.all() - self.assertEqual(data.object.toggle.id, toggles[0].id) + toggle = models.FlowToggle.objects.get(tap=self.tap) + self.assertEqual(data.object.toggle.id, toggle.id) response, data = self.post( f"taps/{self.tap.id}/disconnect-toggle", HTTP_X_KEGBOT_API_KEY=self.apikey.key @@ -372,12 +369,12 @@ def test_add_remove_toggles(self): response, data = self.post( f"taps/{self.tap.id}/connect-toggle", HTTP_X_KEGBOT_API_KEY=self.apikey.key, - data={"toggle": toggles[0].id}, + data={"toggle": toggle.id}, ) response, data = self.get(f"taps/{self.tap.id}", HTTP_X_KEGBOT_API_KEY=self.apikey.key) self.assertEqual(data.meta.result, "ok") self.assertIsNotNone(data.object.get("toggle")) - self.assertEqual(data.object.toggle.id, toggles[0].id) + self.assertEqual(data.object.toggle.id, toggle.id) def test_get_version(self): response, data = self.get("version") diff --git a/pykeg/web/api/devicelink.py b/pykeg/web/api/devicelink.py index 548259ab5..f2e4d2d65 100644 --- a/pykeg/web/api/devicelink.py +++ b/pykeg/web/api/devicelink.py @@ -24,7 +24,6 @@ """ import random -from builtins import range from django.core.cache import cache @@ -56,7 +55,7 @@ class LinkExpiredException(Exception): def _build_code(size=DEFAULT_CODE_SIZE): code = "".join(random.choice(CODE_LETTERS) for i in range(size)) mid = size // 2 - code = "{}-{}".format(code[:mid], code[mid:]) + code = f"{code[:mid]}-{code[mid:]}" return code diff --git a/pykeg/web/api/forms.py b/pykeg/web/api/forms.py index 96237d488..fc93b807b 100644 --- a/pykeg/web/api/forms.py +++ b/pykeg/web/api/forms.py @@ -1,5 +1,3 @@ -from builtins import object - from django import forms from pykeg.core import models @@ -40,7 +38,7 @@ class ThermoPostForm(forms.Form): class CreateKegTapForm(forms.ModelForm): - class Meta(object): + class Meta: model = models.KegTap fields = ("name", "notes") diff --git a/pykeg/web/api/middleware.py b/pykeg/web/api/middleware.py index b6cd77a13..0e0c70f65 100644 --- a/pykeg/web/api/middleware.py +++ b/pykeg/web/api/middleware.py @@ -1,5 +1,4 @@ import logging -from builtins import object from django.conf import settings from django.http import HttpResponse @@ -25,7 +24,7 @@ WHITELISTED_API_PATHS += getattr(settings, "KEGBOT_EXTRA_WHITELISTED_API_PATHS", ()) -class ApiRequestMiddleware(object): +class ApiRequestMiddleware: def __init__(self, get_response): self.get_response = get_response @@ -83,4 +82,4 @@ def process_exception(self, request, exception): def cache_key(request): - return "api:%s" % request.get_full_path() + return f"api:{request.get_full_path()}" diff --git a/pykeg/web/api/util.py b/pykeg/web/api/util.py index b77a80737..cc2a078d5 100644 --- a/pykeg/web/api/util.py +++ b/pykeg/web/api/util.py @@ -3,7 +3,6 @@ import logging import sys import traceback -import types from addict import Dict from django.conf import settings @@ -75,7 +74,7 @@ def to_json_error(e, exc_info): else: code = "ServerError" http_code = 500 - message = "An internal error occurred: %s" % str(e) + message = f"An internal error occurred: {str(e)}" result = {"error": {"code": code, "message": message}} if settings.DEBUG: result["error"]["traceback"] = "".join(traceback.format_exception(*exc_info)) @@ -91,10 +90,10 @@ def build_response(request, result_data, response_code=200): json_str = kbjson.dumps(result_data, indent=indent) if callback and validate_jsonp.is_valid_jsonp_callback_value(callback): - json_str = "%s(%s);" % (callback, json_str) + json_str = f"{callback}({json_str});" if format == "html" or (settings.DEBUG and debug): - html = "
%s
" % json_str + html = f"
{json_str}
" return HttpResponse(html, content_type="text/html", status=response_code) else: return HttpResponse(json_str, content_type="application/json", status=response_code) @@ -128,7 +127,7 @@ def wrap_exception(request, exception): exc_info = sys.exc_info() LOGGER.error( - "%s: %s" % (exception.__class__.__name__, exception), + f"{exception.__class__.__name__}: {exception}", exc_info=exc_info, extra={ "status_code": 500, diff --git a/pykeg/web/api/validate_jsonp.py b/pykeg/web/api/validate_jsonp.py index bf79e6a5e..c45c7d6dd 100644 --- a/pykeg/web/api/validate_jsonp.py +++ b/pykeg/web/api/validate_jsonp.py @@ -1,12 +1,9 @@ -# -*- coding: utf-8 -*- - # Placed into the Public Domain by tav # origin: https://raw.github.com/tav/scripts/master/validate_jsonp.py """Validate Javascript Identifiers for use as JSON-P callback parameters.""" import re -from builtins import chr, str from unicodedata import category # ------------------------------------------------------------------------------ @@ -116,7 +113,6 @@ def is_valid_javascript_identifier(identifier, escape=r"\u", ucd_cat=category): return False if escape in identifier: - new = [] add_char = new.append split_id = identifier.split(escape) diff --git a/pykeg/web/api/views.py b/pykeg/web/api/views.py index f7355b1c7..87f3c0fb5 100644 --- a/pykeg/web/api/views.py +++ b/pykeg/web/api/views.py @@ -2,7 +2,6 @@ import datetime import logging -from builtins import str from functools import wraps from django.contrib.auth import login as auth_login @@ -350,7 +349,7 @@ def apply_since(request, query): try: since = int(since_str) return query.filter(id__gt=since) - except (ValueError, TypeError): + except ValueError, TypeError: pass return query @@ -799,7 +798,7 @@ def link_device_status(request, code): def default_handler(request): - raise Http404("Not an API endpoint: %s" % request.path[:100]) + raise Http404(f"Not an API endpoint: {request.path[:100]}") def get_tap_from_meter_name_or_404(meter_name_or_id): diff --git a/pykeg/web/auth/__init__.py b/pykeg/web/auth/__init__.py index dc7b51981..fdada9c98 100644 --- a/pykeg/web/auth/__init__.py +++ b/pykeg/web/auth/__init__.py @@ -1,7 +1,5 @@ """Kegbot authentication backend interface.""" -from builtins import object - from django.contrib import auth from django.core.exceptions import ImproperlyConfigured @@ -10,7 +8,7 @@ def get_auth_backend(): """Returns the sole authentication backend.""" backends = auth.get_backends() if len(backends) != 1: - raise ImproperlyConfigured("Expected exactly 1 backend (found {})".format(len(backends))) + raise ImproperlyConfigured(f"Expected exactly 1 backend (found {len(backends)})") return backends[0] @@ -22,8 +20,7 @@ class UserExistsException(AuthException): """Registration error indicating this username/email is taken.""" -class AuthBackend(object): - +class AuthBackend: # Django methods def authenticate(self, **credentials): diff --git a/pykeg/web/auth/local.py b/pykeg/web/auth/local.py index c7ca8e79a..894ccac1c 100644 --- a/pykeg/web/auth/local.py +++ b/pykeg/web/auth/local.py @@ -1,7 +1,6 @@ """Local Django authentication backend.""" import uuid -from builtins import str from django.contrib.auth.backends import ModelBackend from django.db import IntegrityError diff --git a/pykeg/web/charts/charts.py b/pykeg/web/charts/charts.py index 380876a87..4ea01d69c 100644 --- a/pykeg/web/charts/charts.py +++ b/pykeg/web/charts/charts.py @@ -155,7 +155,7 @@ def chart_users_by_volume(stats, *args, **kwargs): if not username: username = "Guest" volume, units = format_volume(volume, kwargs) - label = "%s (%.1f %s)" % (username, volume, units) + label = f"{username} ({volume:.1f} {units})" data.append((label, volume)) other_vol = 0 @@ -166,7 +166,7 @@ def chart_users_by_volume(stats, *args, **kwargs): data.reverse() if other_vol: - label = "%s (%.1f)" % ("all others", other_vol) + label = "{} ({:.1f})".format("all others", other_vol) data.append((label, other_vol)) res = { diff --git a/pykeg/web/context_processors.py b/pykeg/web/context_processors.py index 8c0914e33..c55fb13c3 100644 --- a/pykeg/web/context_processors.py +++ b/pykeg/web/context_processors.py @@ -15,11 +15,11 @@ def kbsite(request): sso_login_url = getattr(settings, "SSO_LOGIN_URL", "") if sso_login_url: - sso_login_url = "{}?{}".format(sso_login_url, redir) + sso_login_url = f"{sso_login_url}?{redir}" sso_logout_url = getattr(settings, "SSO_LOGOUT_URL", "") if sso_logout_url: - sso_logout_url = "{}?{}".format(sso_logout_url, redir) + sso_logout_url = f"{sso_logout_url}?{redir}" ret = { "DEBUG": settings.DEBUG, diff --git a/pykeg/web/gunicorn_conf.py b/pykeg/web/gunicorn_conf.py deleted file mode 100644 index 14dbe2099..000000000 --- a/pykeg/web/gunicorn_conf.py +++ /dev/null @@ -1,11 +0,0 @@ -import multiprocessing -import os - -if os.getenv("KEGBOT_IN_HEROKU") and os.getenv("PORT"): - # Necessary on Heroku, which doesn't respect EXPOSE :-\ - bind = "0.0.0.0:{}".format(int(os.getenv("PORT"))) -else: - bind = "0.0.0.0:8000" - -worker_class = "gevent" -workers = multiprocessing.cpu_count() * 2 + 1 diff --git a/pykeg/web/kbregistration/forms.py b/pykeg/web/kbregistration/forms.py index 5a22cbb07..5d665819c 100644 --- a/pykeg/web/kbregistration/forms.py +++ b/pykeg/web/kbregistration/forms.py @@ -1,5 +1,4 @@ import urllib.parse -from builtins import object from django import forms from django.conf import settings @@ -22,7 +21,7 @@ class KegbotRegistrationForm(forms.ModelForm): - class Meta(object): + class Meta: model = models.User fields = ("email", "username") @@ -30,7 +29,7 @@ class Meta(object): password2 = forms.CharField(widget=forms.PasswordInput, label=_("Password (again)")) def clean(self): - super(KegbotRegistrationForm, self).clean() + super().clean() if "password1" in self.cleaned_data and "password2" in self.cleaned_data: if self.cleaned_data["password1"] != self.cleaned_data["password2"]: raise forms.ValidationError(_("The two password fields didn't match.")) diff --git a/pykeg/web/kegadmin/forms.py b/pykeg/web/kegadmin/forms.py index 61864d23f..3a50cc127 100644 --- a/pykeg/web/kegadmin/forms.py +++ b/pykeg/web/kegadmin/forms.py @@ -1,5 +1,3 @@ -from builtins import object, str - from crispy_forms.bootstrap import FormActions from crispy_forms.helper import FormHelper from crispy_forms.layout import HTML, Field, Layout, Submit @@ -118,14 +116,14 @@ class TapForm(forms.ModelForm): class FlowMeterModelChoiceField(forms.ModelChoiceField): def label_from_instance(self, meter): if meter.tap: - return "{} (connected to {})".format(meter, meter.tap.name) + return f"{meter} (connected to {meter.tap.name})" else: return str(meter) class FlowToggleModelChoiceField(forms.ModelChoiceField): def label_from_instance(self, toggle): if toggle.tap: - return "{} (connected to {})".format(toggle, toggle.tap.name) + return f"{toggle} (connected to {toggle.tap.name})" else: return str(toggle) @@ -133,7 +131,7 @@ class ThermoSensorModelChoiceField(forms.ModelChoiceField): def label_from_instance(self, sensor): last_log = sensor.LastLog() if last_log: - return "{} (Last report: {})".format(sensor, naturaltime(last_log.time)) + return f"{sensor} (Last report: {naturaltime(last_log.time)})" else: return str(sensor) @@ -158,12 +156,12 @@ def label_from_instance(self, sensor): help_text="Optional sensor monitoring the temperature at this tap.", ) - class Meta(object): + class Meta: model = models.KegTap fields = ("name", "notes", "temperature_sensor", "sort_order") def __init__(self, *args, **kwargs): - super(TapForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) if self.instance: self.fields["meter"].initial = self.instance.current_meter() @@ -173,7 +171,7 @@ def __init__(self, *args, **kwargs): def save(self, commit=True): if not commit: raise ValueError("TapForm does not support commit=False") - tap = super(TapForm, self).save(commit=True) + tap = super().save(commit=True) tap.connect_meter(self.cleaned_data["meter"]) tap.connect_toggle(self.cleaned_data["toggle"]) return tap @@ -312,7 +310,7 @@ def save(self): class EditKegForm(forms.ModelForm): - class Meta(object): + class Meta: model = models.Keg fields = ( "type", @@ -347,7 +345,7 @@ class Meta(object): class GeneralSiteSettingsForm(forms.ModelForm): - class Meta(object): + class Meta: model = models.KegbotSite fields = ( "title", @@ -374,7 +372,7 @@ class Meta(object): class LocationSiteSettingsForm(forms.ModelForm): guest_image = forms.ImageField(required=False, help_text='Custom image for the "guest" user.') - class Meta(object): + class Meta: model = models.KegbotSite fields = ( "volume_display_units", @@ -395,7 +393,7 @@ class Meta(object): class AdvancedSiteSettingsForm(forms.ModelForm): - class Meta(object): + class Meta: model = models.KegbotSite fields = ( "session_timeout_minutes", @@ -416,7 +414,7 @@ class Meta(object): class BeverageForm(forms.ModelForm): - class Meta(object): + class Meta: model = models.Beverage fields = ( "name", @@ -460,7 +458,7 @@ class Meta(object): class BeverageProducerForm(forms.ModelForm): - class Meta(object): + class Meta: model = models.BeverageProducer fields = ( "name", @@ -492,7 +490,7 @@ class FindUserForm(forms.Form): class UserForm(forms.ModelForm): - class Meta(object): + class Meta: model = models.User fields = ( "username", @@ -515,14 +513,14 @@ class Meta(object): class UserProfileForm(forms.ModelForm): - class Meta(object): + class Meta: model = models.User fields = ("username", "display_name", "email") new_mugshot = forms.ImageField(required=False) def save(self, *args, **kwargs): - user = super(UserProfileForm, self).save(*args, **kwargs) + user = super().save(*args, **kwargs) image = self.cleaned_data.get("new_mugshot") if image: pic = models.Picture.objects.create(user=user) @@ -534,7 +532,7 @@ def save(self, *args, **kwargs): class TokenForm(forms.ModelForm): - class Meta(object): + class Meta: model = models.AuthenticationToken fields = ( "nice_name", @@ -579,7 +577,7 @@ class DeleteTokenForm(forms.Form): class AddTokenForm(forms.ModelForm): - class Meta(object): + class Meta: model = models.AuthenticationToken fields = ( "auth_device", @@ -740,19 +738,19 @@ def clean_address(self): class NewFlowMeterForm(forms.ModelForm): - class Meta(object): + class Meta: model = models.FlowMeter fields = ("port_name", "ticks_per_ml", "controller") class UpdateFlowMeterForm(forms.ModelForm): - class Meta(object): + class Meta: model = models.FlowMeter fields = ("ticks_per_ml",) class AddFlowMeterForm(forms.ModelForm): - class Meta(object): + class Meta: model = models.FlowMeter fields = ("port_name", "ticks_per_ml", "controller") @@ -769,13 +767,13 @@ class Meta(object): class FlowToggleForm(forms.ModelForm): - class Meta(object): + class Meta: model = models.FlowToggle fields = ("port_name", "controller") class AddFlowToggleForm(forms.ModelForm): - class Meta(object): + class Meta: model = models.FlowToggle fields = ("port_name", "controller") @@ -801,7 +799,7 @@ class DeleteControllerForm(forms.Form): class ControllerForm(forms.ModelForm): - class Meta(object): + class Meta: model = models.Controller fields = ("name", "model_name", "serial_number") diff --git a/pykeg/web/kegadmin/templates/kegadmin/beer_type_list.html b/pykeg/web/kegadmin/templates/kegadmin/beer_type_list.html index ef340e222..c12b8fab8 100644 --- a/pykeg/web/kegadmin/templates/kegadmin/beer_type_list.html +++ b/pykeg/web/kegadmin/templates/kegadmin/beer_type_list.html @@ -1,7 +1,6 @@ {% extends "kegadmin/base.html" %} {% load kegweblib %} {% load crispy_forms_tags %} -{% load bootstrap_pagination %} {% block title %}Kegbot Admin: Beer Types | {{ block.super }}{% endblock %} {% block pagetitle %}Kegbot Admin: Beer Types{% endblock %} @@ -35,6 +34,6 @@ -{% bootstrap_paginate beverages %} +{% include "kegweb/_pagination.html" with page=beverages %} {% endblock %} diff --git a/pykeg/web/kegadmin/templates/kegadmin/brewer_list.html b/pykeg/web/kegadmin/templates/kegadmin/brewer_list.html index 8f9576430..ec3d073ea 100644 --- a/pykeg/web/kegadmin/templates/kegadmin/brewer_list.html +++ b/pykeg/web/kegadmin/templates/kegadmin/brewer_list.html @@ -1,7 +1,6 @@ {% extends "kegadmin/base.html" %} {% load kegweblib %} {% load crispy_forms_tags %} -{% load bootstrap_pagination %} {% block title %}Kegbot Admin: Brewers | {{ block.super }}{% endblock %} {% block pagetitle %}Kegbot Admin: Brewers{% endblock %} @@ -38,6 +37,6 @@ -{% bootstrap_paginate brewers %} +{% include "kegweb/_pagination.html" with page=brewers %} {% endblock %} diff --git a/pykeg/web/kegadmin/templates/kegadmin/drink_list.html b/pykeg/web/kegadmin/templates/kegadmin/drink_list.html index d8de9b84f..801d7acb6 100644 --- a/pykeg/web/kegadmin/templates/kegadmin/drink_list.html +++ b/pykeg/web/kegadmin/templates/kegadmin/drink_list.html @@ -1,7 +1,6 @@ {% extends "kegadmin/base.html" %} {% load kegweblib %} {% load crispy_forms_tags %} -{% load bootstrap_pagination %} {% block title %}Kegbot Admin: Drinks | {{ block.super }}{% endblock %} {% block pagetitle %}Kegbot Admin: Drinks{% endblock %} @@ -9,7 +8,7 @@ {% block kegadmin-main %} {% crispy delete_drinks_form %} -{% bootstrap_paginate drinks %} +{% include "kegweb/_pagination.html" with page=drinks %} {% endblock %} diff --git a/pykeg/web/kegadmin/templates/kegadmin/keg_list.html b/pykeg/web/kegadmin/templates/kegadmin/keg_list.html index a4435101c..4242d03c3 100644 --- a/pykeg/web/kegadmin/templates/kegadmin/keg_list.html +++ b/pykeg/web/kegadmin/templates/kegadmin/keg_list.html @@ -1,7 +1,6 @@ {% extends "kegadmin/base.html" %} {% load kegweblib %} {% load crispy_forms_tags %} -{% load bootstrap_pagination %} {% block title %}Kegbot Admin: Kegs | {{ block.super }}{% endblock %} {% block pagetitle %}Kegbot Admin: Kegs{% endblock %} @@ -36,6 +35,6 @@
{{ keg.type.name }}
{% endfor %} -{% bootstrap_paginate kegs %} +{% include "kegweb/_pagination.html" with page=kegs %} {% endblock %} diff --git a/pykeg/web/kegadmin/templates/kegadmin/token_list.html b/pykeg/web/kegadmin/templates/kegadmin/token_list.html index b520da0c1..caea7de9d 100644 --- a/pykeg/web/kegadmin/templates/kegadmin/token_list.html +++ b/pykeg/web/kegadmin/templates/kegadmin/token_list.html @@ -1,7 +1,6 @@ {% extends "kegadmin/base.html" %} {% load kegweblib %} {% load crispy_forms_tags %} -{% load bootstrap_pagination %} {% block title %}Kegbot Admin: Users | {{ block.super }}{% endblock %} {% block pagetitle %}Kegbot Admin: Users{% endblock %} @@ -40,6 +39,6 @@ -{% bootstrap_paginate tokens %} +{% include "kegweb/_pagination.html" with page=tokens %} {% endblock %} diff --git a/pykeg/web/kegadmin/templates/kegadmin/user_list.html b/pykeg/web/kegadmin/templates/kegadmin/user_list.html index e0f468b32..b3dd47d6e 100644 --- a/pykeg/web/kegadmin/templates/kegadmin/user_list.html +++ b/pykeg/web/kegadmin/templates/kegadmin/user_list.html @@ -1,7 +1,6 @@ {% extends "kegadmin/base.html" %} {% load kegweblib %} {% load crispy_forms_tags %} -{% load bootstrap_pagination %} {% block title %}Kegbot Admin: Users | {{ block.super }}{% endblock %} {% block pagetitle %}Kegbot Admin: Users{% endblock %} @@ -50,6 +49,6 @@ -{% bootstrap_paginate users %} +{% include "kegweb/_pagination.html" with page=users %} {% endblock %} diff --git a/pykeg/web/kegadmin/views.py b/pykeg/web/kegadmin/views.py index 0d23a73b6..1e8aab60b 100644 --- a/pykeg/web/kegadmin/views.py +++ b/pykeg/web/kegadmin/views.py @@ -11,7 +11,7 @@ import redis from django.conf import settings from django.contrib import messages -from django.core.files.storage import get_storage_class +from django.core.files.storage import default_storage from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator from django.db.models import Q from django.http import Http404, HttpResponse @@ -21,7 +21,6 @@ from pykeg.backup import backup from pykeg.core import models, tasks -from pykeg.core.util import get_runtime_version_info from pykeg.logging.handlers import RedisListHandler from pykeg.util import kbjson from pykeg.util.email import build_message @@ -137,7 +136,7 @@ def email(request): context["settings_url"] = context["site_url"] + "/account" message = build_message(address, "notification/email_test.html", context) message.send(fail_silently=True) - messages.success(request, "E-mail successfully sent to %s" % address) + messages.success(request, f"E-mail successfully sent to {address}") context["email_configured"] = email_configured @@ -148,7 +147,7 @@ def email(request): def export(request): context = {} backups = [] - storage = get_storage_class()() + storage = default_storage if request.method == "POST": if "package_backup" in request.POST: @@ -346,7 +345,7 @@ def tap_detail(request, tap_id): end_keg_form = forms.EndKegForm(request.POST) if end_keg_form.is_valid(): old_keg = tap.end_current_keg() - messages.success(request, "Keg %s was ended." % old_keg.id) + messages.success(request, f"Keg {old_keg.id} was ended.") elif "submit_record_drink" in request.POST: record_drink_form = forms.RecordDrinkForm(request.POST) @@ -354,7 +353,7 @@ def tap_detail(request, tap_id): user = record_drink_form.cleaned_data.get("user") volume_ml = record_drink_form.cleaned_data.get("volume_ml") d = models.Drink.record_drink(tap, ticks=0, username=user, volume_ml=volume_ml) - messages.success(request, "Drink %s recorded." % d.id) + messages.success(request, f"Drink {d.id} recorded.") else: messages.error(request, "Please enter a valid volume and user.") @@ -493,7 +492,7 @@ def user_list(request): user = models.User.objects.get(username=username) return redirect("kegadmin-edit-user", user.id) except models.User.DoesNotExist: - messages.error(request, 'User "%s" does not exist.' % username) + messages.error(request, f'User "{username}" does not exist.') users = models.User.objects.exclude(username="guest").order_by("-id") paginator = Paginator(users, 25) @@ -520,7 +519,7 @@ def add_user(request): instance = form.save(commit=False) instance.set_password(form.cleaned_data.get("password")) instance.save() - messages.success(request, 'User "%s" created.' % instance.username) + messages.success(request, f'User "{instance.username}" created.') return redirect("kegadmin-users") context["form"] = form return render(request, "kegadmin/add_user.html", context=context) @@ -539,7 +538,7 @@ def user_detail(request, user_id): else: edit_user.is_active = True edit_user.save() - messages.success(request, "User %s was enabled." % edit_user.username) + messages.success(request, f"User {edit_user.username} was enabled.") elif "submit_disable" in request.POST: if edit_user.is_guest(): @@ -549,7 +548,7 @@ def user_detail(request, user_id): else: edit_user.is_active = False edit_user.save() - messages.success(request, "User %s was disabled." % edit_user.username) + messages.success(request, f"User {edit_user.username} was disabled.") elif "submit_add_staff" in request.POST: if edit_user.is_staff: @@ -557,7 +556,7 @@ def user_detail(request, user_id): else: edit_user.is_staff = True edit_user.save() - messages.success(request, "User %s staff status enabled." % edit_user.username) + messages.success(request, f"User {edit_user.username} staff status enabled.") elif "submit_remove_staff" in request.POST: if edit_user.is_guest(): @@ -567,13 +566,13 @@ def user_detail(request, user_id): else: edit_user.is_staff = False edit_user.save() - messages.success(request, "User %s staff status disabled." % edit_user.username) + messages.success(request, f"User {edit_user.username} staff status disabled.") elif "submit_update_profile" in request.POST: profile_form = forms.UserProfileForm(request.POST, request.FILES, instance=edit_user) if profile_form.is_valid(): profile_form.save() - messages.success(request, "User %s e-profile updated" % edit_user.username) + messages.success(request, f"User {edit_user.username} e-profile updated") else: messages.error(request, "Unknown form submitted.") @@ -640,7 +639,7 @@ def drink_edit(request, drink_id): old_keg = drink.keg if form.is_valid(): drink.cancel_drink() - messages.success(request, "Drink %s was cancelled." % drink_id) + messages.success(request, f"Drink {drink_id} was cancelled.") return redirect(old_keg.get_absolute_url()) else: messages.error(request, "Invalid request") @@ -651,7 +650,7 @@ def drink_edit(request, drink_id): old_keg = drink.keg if form.is_valid(): drink.cancel_drink(spilled=True) - messages.success(request, "Drink %s was spilled." % drink_id) + messages.success(request, f"Drink {drink_id} was spilled.") return redirect(old_keg.get_absolute_url()) else: messages.error(request, "Invalid request") @@ -663,7 +662,7 @@ def drink_edit(request, drink_id): new_user = form.cleaned_data["user"] try: drink.reassign(new_user) - messages.success(request, "Drink %s was reassigned." % drink_id) + messages.success(request, f"Drink {drink_id} was reassigned.") except models.User.DoesNotExist: messages.error(request, "No such user") else: @@ -678,7 +677,7 @@ def drink_edit(request, drink_id): messages.warning(request, "Drink volume unchanged.") else: drink.set_volume(volume_ml) - messages.success(request, "Drink %s was updated." % drink_id) + messages.success(request, f"Drink {drink_id} was updated.") else: messages.error(request, "Please provide a valid volume.") return redirect(drink.get_absolute_url()) @@ -968,7 +967,7 @@ def autocomplete_token(request): def plugin_settings(request, plugin_name): plugin = request.plugins.get(plugin_name, None) if not plugin: - raise Http404('Plugin "%s" not loaded' % plugin_name) + raise Http404(f'Plugin "{plugin_name}" not loaded') view = plugin.get_admin_settings_view() if not view: @@ -1004,11 +1003,11 @@ def link_device(request): result = re.match("^([A-Z1-9]{3})-?([A-Z1-9]{3})$", code.upper()) if not result: messages.error('Link code is not in the form of "XXX-XXX"') - code = "{}-{}".format(result.group(1), result.group(2)) + code = f"{result.group(1)}-{result.group(2)}" try: status = devicelink.confirm_link(code) name = status.get("name", "New device") - messages.success(request, "{} linked!".format(name)) + messages.success(request, f"{name} linked!") except devicelink.LinkExpiredException: messages.error(request, "Code incorrect or expired.") return redirect("kegadmin-link-device") diff --git a/pykeg/web/kegweb/kbstorage.py b/pykeg/web/kegweb/kbstorage.py index c71c3e5d5..354863063 100644 --- a/pykeg/web/kegweb/kbstorage.py +++ b/pykeg/web/kegweb/kbstorage.py @@ -25,7 +25,7 @@ def url(self, name): base_url = get_base_url() if not self.base_url.startswith(base_url): self.base_url = urllib.parse.urljoin(base_url, self.base_url) - return super(KegbotFileSystemStorage, self).url(name) + return super().url(name) if S3BotoStorage: @@ -35,4 +35,4 @@ class S3StaticStorage(S3BotoStorage): def __init__(self, *args, **kwargs): kwargs["bucket"] = S3_STATIC_BUCKET - super(S3StaticStorage, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) diff --git a/pykeg/web/kegweb/kegweb_test.py b/pykeg/web/kegweb/kegweb_test.py index 518b4c6af..396403b21 100644 --- a/pykeg/web/kegweb/kegweb_test.py +++ b/pykeg/web/kegweb/kegweb_test.py @@ -36,11 +36,11 @@ def testBasicEndpoints(self): d = models.Drink.record_drink("kegboard.flow0", ticks=100) drink_id = d.id - response = self.client.get("/d/%s" % drink_id, follow=True) - self.assertRedirects(response, "/drinks/%s/" % drink_id, status_code=301) + response = self.client.get(f"/d/{drink_id}", follow=True) + self.assertRedirects(response, f"/drinks/{drink_id}/", status_code=301) session_id = d.session.id - response = self.client.get("/s/%s" % session_id, follow=True) + response = self.client.get(f"/s/{session_id}", follow=True) self.assertRedirects(response, d.session.get_absolute_url(), status_code=301) def testShout(self): @@ -71,8 +71,8 @@ def test_privacy(self): "/kegs/": "Keg List", "/stats/": "System Stats", "/sessions/": "All Sessions", - "/kegs/{}/".format(keg.id): "Keg {}".format(keg.id), - "/drinks/{}/".format(d.id): "Drink {}".format(d.id), + f"/kegs/{keg.id}/": f"Keg {keg.id}", + f"/drinks/{d.id}/": f"Drink {d.id}", } def test_urls(expect_fail, urls=urls): diff --git a/pykeg/web/kegweb/templates/kegweb/_pagination.html b/pykeg/web/kegweb/templates/kegweb/_pagination.html new file mode 100644 index 000000000..66a8abe91 --- /dev/null +++ b/pykeg/web/kegweb/templates/kegweb/_pagination.html @@ -0,0 +1,33 @@ +{% comment %} +Bootstrap 3 pagination for a Django Paginator Page, passed in as `page`. +Usage: {% include "kegweb/_pagination.html" with page=page_obj %} +{% endcomment %} +{% load paginator_tags %} +{% if page.paginator.num_pages > 1 %} +
    + + {% if page.has_previous %} + « + {% else %} + + {% endif %} + + {% elided_page_range page as page_numbers %} + {% for num in page_numbers %} + {% if num == page.paginator.ELLIPSIS %} +
  • {{ num }}
  • + {% elif num == page.number %} +
  • {{ num }}
  • + {% else %} +
  • {{ num }}
  • + {% endif %} + {% endfor %} + + {% if page.has_next %} + » + {% else %} + + {% endif %} + +
+{% endif %} diff --git a/pykeg/web/kegweb/templates/kegweb/drinker_sessions.html b/pykeg/web/kegweb/templates/kegweb/drinker_sessions.html index 732a986e2..56b6827b0 100644 --- a/pykeg/web/kegweb/templates/kegweb/drinker_sessions.html +++ b/pykeg/web/kegweb/templates/kegweb/drinker_sessions.html @@ -1,6 +1,5 @@ {% extends "base.html" %} {% load kegweblib %} -{% load bootstrap_pagination %} {% block title %}Drinker Sessions: {{ drinker.username }} | {{ block.super }}{% endblock %} {% block pagetitle %}Drinker Sessions: {{ drinker.username }}{% endblock %} @@ -24,7 +23,7 @@ {% include "kegweb/keg-session.html" %} {% endwith %} {% endfor %} - {% bootstrap_paginate chunks %} + {% include "kegweb/_pagination.html" with page=chunks %} {% endif %} diff --git a/pykeg/web/kegweb/templates/kegweb/drinkingsession_archive_base.html b/pykeg/web/kegweb/templates/kegweb/drinkingsession_archive_base.html index 36dd8b1ae..f9e65978f 100644 --- a/pykeg/web/kegweb/templates/kegweb/drinkingsession_archive_base.html +++ b/pykeg/web/kegweb/templates/kegweb/drinkingsession_archive_base.html @@ -1,6 +1,5 @@ {% extends "base.html" %} {% load kegweblib %} -{% load bootstrap_pagination %} {% block content %}
@@ -24,7 +23,7 @@

All Sessions

-{% bootstrap_paginate page_obj range=10 %} +{% include "kegweb/_pagination.html" with page=page_obj %}
diff --git a/pykeg/web/kegweb/templates/kegweb/keg_list.html b/pykeg/web/kegweb/templates/kegweb/keg_list.html index f381c9a8b..4a92bbdcd 100644 --- a/pykeg/web/kegweb/templates/kegweb/keg_list.html +++ b/pykeg/web/kegweb/templates/kegweb/keg_list.html @@ -1,6 +1,5 @@ {% extends "base.html" %} {% load kegweblib %} -{% load bootstrap_pagination %} {% block title %}Keg List | {{ block.super }}{% endblock %} {% block pagetitle %}Keg List{% endblock %} @@ -15,6 +14,6 @@
{% endfor %} -{% bootstrap_paginate page_obj range=10 %} +{% include "kegweb/_pagination.html" with page=page_obj %} {% endblock %} diff --git a/pykeg/web/kegweb/templates/kegweb/keg_sessions.html b/pykeg/web/kegweb/templates/kegweb/keg_sessions.html index 31a8d9635..8fc7b075a 100644 --- a/pykeg/web/kegweb/templates/kegweb/keg_sessions.html +++ b/pykeg/web/kegweb/templates/kegweb/keg_sessions.html @@ -1,6 +1,5 @@ {% extends "base.html" %} {% load kegweblib %} -{% load bootstrap_pagination %} {% block title %}Keg {{ keg.id }}: Sessions | {{ block.super }}{% endblock %} {% block pagetitle %}Keg {{ keg.id }}: Sessions{% endblock %} @@ -12,7 +11,7 @@ {% for session in sessions %} {% include "kegweb/keg-session.html" %} {% endfor %} - {% bootstrap_paginate sessions %} + {% include "kegweb/_pagination.html" with page=sessions %} {% endif %} diff --git a/pykeg/web/kegweb/templatetags/kegweblib.py b/pykeg/web/kegweb/templatetags/kegweblib.py index bdb4dac76..b1e2210b1 100644 --- a/pykeg/web/kegweb/templatetags/kegweblib.py +++ b/pykeg/web/kegweb/templatetags/kegweblib.py @@ -1,6 +1,5 @@ -from builtins import str +import zoneinfo -import pytz from django.conf import settings from django.template import ( Library, @@ -86,7 +85,7 @@ def navitem(parser, token): """{% navitem [exact] %}""" tokens = token.split_contents() if len(tokens) < 3: - raise TemplateSyntaxError("%s requires at least 3 tokens" % tokens[0]) + raise TemplateSyntaxError(f"{tokens[0]} requires at least 3 tokens") return NavitemNode(*tokens[1:]) @@ -114,7 +113,7 @@ def render(self, context): res = '<li class="active">' else: res = "<li>" - res += '<a href="%s">%s</a></li>' % (urlbase, title) + res += f'<a href="{urlbase}">{title}</a></li>' return res @@ -126,7 +125,7 @@ def timeago(parser, token): """{% timeago <timestamp> %}""" tokens = token.contents.split() if len(tokens) != 2: - raise TemplateSyntaxError("%s requires 2 tokens" % tokens[0]) + raise TemplateSyntaxError(f"{tokens[0]} requires 2 tokens") return TimeagoNode(tokens[1]) @@ -141,14 +140,14 @@ def render(self, context): # Try to set time zone information. if settings.TIME_ZONE and not settings.USE_TZ: try: - tz = pytz.timezone(settings.TIME_ZONE) - ts = tz.localize(ts) - except pytz.UnknownTimeZoneError: + tz = zoneinfo.ZoneInfo(settings.TIME_ZONE) + ts = ts.replace(tzinfo=tz) + except zoneinfo.ZoneInfoNotFoundError: pass iso = ts.isoformat() alt = timezone.localtime(ts).strftime("%A, %B %d, %Y %I:%M%p") - return '<abbr class="timeago" title="%s">%s</abbr>' % (iso, alt) + return f'<abbr class="timeago" title="{iso}">{alt}</abbr>' # temperature @@ -159,7 +158,7 @@ def temperature_tag(parser, token): """{% temperature <temp_c> %}""" tokens = token.contents.split() if len(tokens) < 2: - raise TemplateSyntaxError("%s requires at least 2 tokens" % tokens[0]) + raise TemplateSyntaxError(f"{tokens[0]} requires at least 2 tokens") return TemperatureNode(tokens[1]) @@ -173,7 +172,7 @@ def render(self, context): v = Variable(self.varname) try: amount = v.resolve(context) - except (VariableDoesNotExist, ValueError): + except VariableDoesNotExist, ValueError: raise amount = "unknown" @@ -194,7 +193,7 @@ def volumetag(parser, token): """{% volume <amount> %}""" tokens = token.contents.split() if len(tokens) < 2: - raise TemplateSyntaxError("%s requires at least 2 tokens" % tokens[0]) + raise TemplateSyntaxError(f"{tokens[0]} requires at least 2 tokens") return VolumeNode(tokens[1], tokens[2:]) @@ -213,7 +212,7 @@ def render(self, context): tv = Variable(self._volume_varname) try: num = float(tv.resolve(context)) - except (VariableDoesNotExist, ValueError): + except VariableDoesNotExist, ValueError: num = "unknown" unit = "mL" make_badge = "badge" in self._extra_args @@ -226,7 +225,7 @@ def format(cls, amount, units, make_badge=False): ctx = { "units": units, "amount": amount, - "title": "%s %s" % (amount, units), + "title": f"{amount} {units}", "extra_css": "badge " if make_badge else "", } return cls.TEMPLATE % ctx @@ -240,7 +239,7 @@ def drinker_name_tag(parser, token): """{% drinker_name <drink_or_user_obj> [nolink] %}""" tokens = token.contents.split() if len(tokens) < 2: - raise TemplateSyntaxError("%s requires at least 2 tokens" % tokens[0]) + raise TemplateSyntaxError(f"{tokens[0]} requires at least 2 tokens") return DrinkerNameNode(tokens[1], tokens[2:]) @@ -253,7 +252,7 @@ def render(self, context): obj = Variable(self._varname) try: obj = obj.resolve(context) - except (VariableDoesNotExist, ValueError): + except VariableDoesNotExist, ValueError: obj = None user = None @@ -266,7 +265,7 @@ def render(self, context): if "nolink" in self._extra_args: return user.get_full_name() else: - return '<a href="%s">%s</a>' % ( + return '<a href="{}">{}</a>'.format( reverse("kb-drinker", args=[user.username]), user.get_full_name(), ) @@ -324,7 +323,7 @@ def __init__(self, charttype, width, height, args): self._height = height self._args = args - self._chart_fn = getattr(charts, "chart_%s" % (self._charttype,), None) + self._chart_fn = getattr(charts, f"chart_{self._charttype}", None) def _get_chart_id(self, context): # TODO(mikey): Is there a better way to store _CHART_ID? @@ -344,7 +343,7 @@ def show_error(self, error_str): def render(self, context): if not self._chart_fn: - return self.show_error("Unknown chart type: %s" % self._charttype) + return self.show_error(f"Unknown chart type: {self._charttype}") chart_id = self._get_chart_id(context) obj = Variable(self._args[0]).resolve(context) @@ -363,7 +362,7 @@ def render(self, context): "chart": { "borderColor": "#eeeeff", "borderWidth": 0, - "renderTo": "chart-%s-container" % chart_id, + "renderTo": f"chart-{chart_id}-container", }, "credits": { "enabled": False, @@ -422,5 +421,5 @@ def volume(text, fmt="pints"): elif fmt == "halfbarrels": res = vol.InHalfBarrelKegs() else: - raise TemplateSyntaxError("Unknown volume format: %s" % fmt) + raise TemplateSyntaxError(f"Unknown volume format: {fmt}") return float(res) diff --git a/pykeg/web/kegweb/templatetags/paginator_tags.py b/pykeg/web/kegweb/templatetags/paginator_tags.py new file mode 100644 index 000000000..b7f497f25 --- /dev/null +++ b/pykeg/web/kegweb/templatetags/paginator_tags.py @@ -0,0 +1,17 @@ +"""Template helpers for rendering Django Paginator pages. + +Replaces the unmaintained django-bootstrap-pagination package with Django's +own Paginator (see kegweb/_pagination.html for the Bootstrap 3 markup). +""" + +from django import template + +register = template.Library() + + +@register.simple_tag +def elided_page_range(page, on_each_side=2, on_ends=1): + """Return a windowed page range (with ELLIPSIS markers) for a Page.""" + return page.paginator.get_elided_page_range( + page.number, on_each_side=on_each_side, on_ends=on_ends + ) diff --git a/pykeg/web/kegweb/views.py b/pykeg/web/kegweb/views.py index f0de4645d..bfb48bf8b 100644 --- a/pykeg/web/kegweb/views.py +++ b/pykeg/web/kegweb/views.py @@ -1,7 +1,5 @@ """Kegweb main views.""" -from builtins import range, str - from django.contrib import messages from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator from django.shortcuts import get_object_or_404, redirect, render @@ -262,7 +260,7 @@ class SessionDateDetailView(DateDetailView): def get_context_data(self, **kwargs): """Adds `stats` to the context.""" - ret = super(SessionDateDetailView, self).get_context_data(**kwargs) + ret = super().get_context_data(**kwargs) stats = ret[self.context_object_name].get_stats() ret["stats"] = stats ret["kegs"] = [models.Keg.objects.get(pk=pk) for pk in stats.get("keg_ids", [])] diff --git a/pykeg/web/middleware.py b/pykeg/web/middleware.py index ed4ea5540..7f06cf8a5 100644 --- a/pykeg/web/middleware.py +++ b/pykeg/web/middleware.py @@ -1,11 +1,11 @@ import logging from django.conf import settings +from django.db import connection from django.http import HttpResponse from django.shortcuts import render from django.utils import timezone -from pykeg import config from pykeg.core import models from pykeg.core.util import get_version_object, must_upgrade, set_current_request from pykeg.plugin import util as plugin_util @@ -105,8 +105,13 @@ def __call__(self, request): # Skip all checks if we're in the setup wizard. if request.path.startswith("/setup"): - request.session = {} - request.session["_auth_user_backend"] = None + # On a fresh install the session table doesn't exist yet, so a real + # session read/write would crash; stub it out until migrations (run + # in the wizard's first step) create the table. Once it exists, keep + # the real session so the wizard can log the new admin in. + if "django_session" not in connection.introspection.table_names(): + request.session = {} + request.session["_auth_user_backend"] = None return self.get_response(request) # First confirm the database is working. @@ -212,6 +217,4 @@ def process_view(self, request, view_func, view_args, view_kwargs): return render(request, "kegweb/members_only.html", status=401) return None - return HttpResponse( - "Server misconfigured, unknown privacy setting:%s" % privacy, status=500 - ) + return HttpResponse(f"Server misconfigured, unknown privacy setting:{privacy}", status=500) diff --git a/pykeg/web/setup_wizard/forms.py b/pykeg/web/setup_wizard/forms.py index b0092e2a5..6968c2f5c 100644 --- a/pykeg/web/setup_wizard/forms.py +++ b/pykeg/web/setup_wizard/forms.py @@ -1,5 +1,3 @@ -from builtins import object - from crispy_forms.bootstrap import FormActions from crispy_forms.helper import FormHelper from crispy_forms.layout import Field, Layout, Submit @@ -9,7 +7,7 @@ class MiniSiteSettingsForm(forms.ModelForm): - class Meta(object): + class Meta: model = models.KegbotSite fields = ( "title", diff --git a/pykeg/web/setup_wizard/setup_wizard_tests.py b/pykeg/web/setup_wizard/setup_wizard_tests.py index a541eaa0a..84017e8c1 100644 --- a/pykeg/web/setup_wizard/setup_wizard_tests.py +++ b/pykeg/web/setup_wizard/setup_wizard_tests.py @@ -3,8 +3,9 @@ from django.core.cache import cache from django.test import TransactionTestCase from django.test.utils import override_settings +from django.urls import reverse -from pykeg.core import defaults +from pykeg.core import defaults, models class SetupWizardTestCase(TransactionTestCase): @@ -32,6 +33,27 @@ def test_settings_debug_true(self): response = self.client.get("/setup/") self.assertEqual(response.status_code, 200) + @override_settings(DEBUG=True) + def test_admin_step_logs_in_new_admin(self): + """The admin-user step creates the admin and logs them in.""" + defaults.set_defaults() # site exists but is not yet set up + + response = self.client.post( + reverse("setup_admin"), + { + "username": "wizadmin", + "email": "admin@example.com", + "password": "s3cret-pass", + "confirm_password": "s3cret-pass", + }, + ) + self.assertRedirects(response, reverse("setup_finish"), fetch_redirect_response=False) + + admin = models.User.objects.get(username="wizadmin") + self.assertTrue(admin.is_superuser) + # The freshly-created admin should now be authenticated. + self.assertEqual(str(admin.pk), self.client.session.get("_auth_user_id")) + def test_setup_not_shown(self): """Verify wizard is not shown on set-up site.""" site = defaults.set_defaults() diff --git a/pykeg/web/setup_wizard/views.py b/pykeg/web/setup_wizard/views.py index e83129693..b39fdb4b7 100644 --- a/pykeg/web/setup_wizard/views.py +++ b/pykeg/web/setup_wizard/views.py @@ -4,7 +4,7 @@ from django.conf import settings from django.contrib import messages -from django.contrib.auth import authenticate +from django.contrib.auth import authenticate, login from django.core import management from django.http import Http404 from django.shortcuts import redirect, render @@ -168,10 +168,15 @@ def admin(request): form = AdminUserForm(request.POST) if form.is_valid(): form.save() + # Log the freshly-created admin in so setup continues as them. By + # this step the session table exists (migrated in the first step), + # so IsSetupMiddleware leaves the real session in place. user = authenticate( username=form.cleaned_data.get("username"), password=form.cleaned_data.get("password"), ) + if user is not None: + login(request, user) return redirect("setup_finish") context["form"] = form return render(request, "setup_wizard/admin.html", context=context) diff --git a/pyproject.toml b/pyproject.toml index 7f5e618d6..1cd80f43a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,72 +1,89 @@ -[tool.black] -line-length = 100 -target_version = ['py310'] -include = 'pykeg/.*\.pyi?$' -exclude = ''' -( - .*/migrations/.* -) -''' - -[tool.poetry] +[project] name = "kegbot" -version = "1.3.0" +version = "2.0.0.dev0" description = "Kegbot Server" -authors = ["The Kegbot Project contributors"] -repository = "https://github.com/Kegbot/kegbot-server" +authors = [{ name = "The Kegbot Project contributors" }] license = "MIT" -packages = [ - { include = "pykeg", from = "." } +requires-python = ">=3.14,<4" +dependencies = [ + "addict", + "coloredlogs", + "dj-database-url", + "dj-email-url", + "django-cors-headers>=4.9,<5", + "django-crispy-forms>=2.4,<3", + "crispy-bootstrap4>=2024.10", + "django-imagekit", + "django-redis>=5.4", + "django-registration>=5.1", + "django-rq>=3,<5", + "djangorestframework>=3.16,<4", + "Django>=5.2,<5.3", + "freezegun>=1.2.1,<2", + "isodate", + "mysqlclient", + "packaging", + "Pillow", + "protobuf>=5,<7", + "psycopg2", + "PyYAML", + "redis", + "requests", + "rq>=2,<3", + "waitress>=3,<4", + "whitenoise", +] + +[project.urls] +repository = "https://github.com/Kegbot/kegbot-server" + +[dependency-groups] +docs = [ + "furo", + "sphinx", + "sphinx-autobuild", + "sphinx-issues", ] +dev = [ + "fakeredis", + "pre-commit", + "pytest", + "pytest-django", + "requests-mock", + "ruff", + "vcrpy", +] + +[tool.uv] +package = true -[tool.poetry.dependencies] -python = "^3.10.4" -coloredlogs = "*" -dj-database-url = "*" -dj-email-url = "*" -django-bootstrap-pagination = "*" -django-crispy-forms = "*" -django-imagekit = "*" -django-redis = "*" -django-registration = "*" -gunicorn = "*" -httplib2 = "*" -isodate = "*" -oauthlib = "*" -pilkit = "*" -protobuf = "3.20.0" -python-gflags = "*" -pytz = "*" -redis = "*" -requests-oauthlib = "*" -requests = "*" -whitenoise = "*" -Django = "3.2.15" -Pillow = "*" -PyYAML = "*" -mysqlclient = "*" -addict = "*" -gevent = "*" -psycopg2 = "*" -future = "^0.18.2" -freezegun = "^1.2.1" -djangorestframework = "^3.13.1" -django-cors-headers = "^3.13.0" -rq = "^1.10.1" -django-rq = "^2.5.1" +# mysqlclient/psycopg2 are source-only (need system libs); keep them in the +# lock for CI/Docker but skip locally where those libs are absent, e.g.: +# uv sync --no-install-package mysqlclient --no-install-package psycopg2 -[tool.poetry.dev-dependencies] -black = "*" -flake8 = "*" -pytest = "*" -pytest-django = "*" -requests-mock = "*" -sphinx-issues = "*" -sphinx = "*" -vcrpy = "*" -furo = {git = "https://github.com/Kegbot/furo", rev = "kegbot/kegbot"} -sphinx-autobuild = "^2021.3.14" +[tool.ruff] +line-length = 100 +target-version = "py314" +exclude = [ + ".git", + ".venv", + "build", + "dist", + "migrations", + # Generated protobuf code (regenerated from kegbot-api .proto sources). + "pykeg/proto/*_pb2.py", +] + +[tool.ruff.format] +quote-style = "preserve" + +[tool.ruff.lint] +select = ["E", "F", "I", "UP"] +ignore = ["E501"] [build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["pykeg"] diff --git a/setup.cfg b/setup.cfg index 8146c2452..6bbf076eb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,19 +1,6 @@ -[flake8] -exclude=build,.git,migrations,settings.py -ignore=E128,E265,E266,E501,W601 -max-line-length=100 - [tool:pytest] addopts = -p pykeg.test.plugin -ra --reuse-db --lfnf=all testpaths = pykeg/ python_files = tests.py test_*.py *_tests.py *_test.py DJANGO_SETTINGS_MODULE = pykeg.settings REUSE_DB = 1 - -[tool:isort] -multi_line_output=3 -include_trailing_comma=True -force_grid_wrap=0 -use_parentheses=True -line_length=88 -skip_glob=*migrations* diff --git a/testdata/legacy-backup-compose.yml b/testdata/legacy-backup-compose.yml new file mode 100644 index 000000000..dc4081dcd --- /dev/null +++ b/testdata/legacy-backup-compose.yml @@ -0,0 +1,72 @@ +# Generates the frozen v1.3 backup fixtures under testdata/legacy-backups/. +# Run via `bin/generate-legacy-backups.sh` from the repo root. +# +# The generate services run the real server image (built from the local +# checkout) against pinned database images, so the fixtures are +# reproducible regardless of what is installed on the host. + +services: + mysql: + image: mysql:8 + environment: + MYSQL_ROOT_PASSWORD: changeme + MYSQL_DATABASE: kegbot + healthcheck: + # Ping over TCP: during init the image runs a socket-only temporary + # server that would pass a plain socket ping too early. + test: ["CMD", "mysqladmin", "ping", "-h127.0.0.1", "--silent"] + interval: 5s + timeout: 5s + retries: 30 + + postgres: + image: postgres:16 + environment: + POSTGRES_PASSWORD: changeme + POSTGRES_DB: kegbot + healthcheck: + test: ["CMD", "pg_isready", "-U", "postgres"] + interval: 5s + timeout: 5s + retries: 30 + + redis: + image: redis + + generate-mysql: + build: + context: .. + entrypoint: [] + working_dir: /app + command: ["bin/build-legacy-backup.sh", "testdata/legacy-backups/kegbot-v1.3-mysql.zip"] + environment: + DATABASE_URL: mysql://root:changeme@mysql/kegbot + REDIS_URL: redis://redis:6379/0 + KEGBOT_SECRET_KEY: changeme + volumes: + - ../bin:/app/bin:ro + - ../testdata:/app/testdata + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_started + + generate-postgres: + build: + context: .. + entrypoint: [] + working_dir: /app + command: ["bin/build-legacy-backup.sh", "testdata/legacy-backups/kegbot-v1.3-postgres.zip"] + environment: + DATABASE_URL: postgres://postgres:changeme@postgres/kegbot + REDIS_URL: redis://redis:6379/0 + KEGBOT_SECRET_KEY: changeme + volumes: + - ../bin:/app/bin:ro + - ../testdata:/app/testdata + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_started diff --git a/testdata/legacy-backups/kegbot-v1.3-mysql.zip b/testdata/legacy-backups/kegbot-v1.3-mysql.zip new file mode 100644 index 000000000..11b15fdb4 Binary files /dev/null and b/testdata/legacy-backups/kegbot-v1.3-mysql.zip differ diff --git a/testdata/legacy-backups/kegbot-v1.3-postgres.zip b/testdata/legacy-backups/kegbot-v1.3-postgres.zip new file mode 100644 index 000000000..471ff2a7f Binary files /dev/null and b/testdata/legacy-backups/kegbot-v1.3-postgres.zip differ diff --git a/uv.lock b/uv.lock new file mode 100644 index 000000000..38e7cc24a --- /dev/null +++ b/uv.lock @@ -0,0 +1,1328 @@ +version = 1 +revision = 3 +requires-python = ">=3.14, <4" + +[[package]] +name = "accessible-pygments" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899, upload-time = "2024-05-10T11:23:10.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" }, +] + +[[package]] +name = "addict" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/ef/fd7649da8af11d93979831e8f1f8097e85e82d5bfeabc8c68b39175d8e75/addict-2.4.0.tar.gz", hash = "sha256:b3b2210e0e067a281f5646c8c5db92e99b7231ea8b0eb5f74dbdf9e259d4e494", size = 9186, upload-time = "2020-11-21T16:21:31.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/00/b08f23b7d7e1e14ce01419a467b583edbb93c6cdb8654e54a9cc579cd61f/addict-2.4.0-py3-none-any.whl", hash = "sha256:249bb56bbfd3cdc2a004ea0ff4c2b6ddc84d53bc2194761636eb314d5cfa5dfc", size = 3832, upload-time = "2020-11-21T16:21:29.588Z" }, +] + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, +] + +[[package]] +name = "anyio" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, +] + +[[package]] +name = "asgiref" +version = "3.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/26/3b59f2bdae5f640389becb1f673cded775287f5fc4f816309d9ca9a3f93d/asgiref-3.12.1.tar.gz", hash = "sha256:59dcb51c272ad209d59bed5708a64a333083e86017d7fcdd67498eeab7784340", size = 42378, upload-time = "2026-07-14T09:56:18.087Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/1b/54f4ad77cd8a584fa70746c47df988e002cf1ee1eba43364d46f87803647/asgiref-3.12.1-py3-none-any.whl", hash = "sha256:fe386d1c2bff7259ea95929266d12a8cf9a8b5a1c2598402967d8792e7a7c094", size = 25478, upload-time = "2026-07-14T09:56:16.926Z" }, +] + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" }, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, + { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, + { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, + { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, + { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coloredlogs" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "humanfriendly" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520, upload-time = "2021-06-11T10:22:45.202Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload-time = "2021-06-11T10:22:42.561Z" }, +] + +[[package]] +name = "confusable-homoglyphs" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/10/1358fca1ee2d97d4f2877df9ffbe6d124da666fef3b2f75e771a4c1afee6/confusable_homoglyphs-3.3.1.tar.gz", hash = "sha256:b995001c9b2e1b4cea0cf5f3840a7c79188a8cbbad053d693572bd8c1c1ec460", size = 325480, upload-time = "2024-01-30T10:10:27.47Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/6e/c0fcbb7d341a46cf4241a6aa9e6a737734f0657521fc1bcd074953fe4eea/confusable_homoglyphs-3.3.1-py2.py3-none-any.whl", hash = "sha256:84c92cb79dc7f55aa290d0762b2349abd8dee4c16fbe6f99eac978d394e2e6a1", size = 144755, upload-time = "2024-01-30T10:10:24.857Z" }, +] + +[[package]] +name = "crispy-bootstrap4" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "django-crispy-forms" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/cc/638d36595da9fbb2c9d0be98bf6007442a65a521b614cf4645d04311b061/crispy_bootstrap4-2026.2.tar.gz", hash = "sha256:66f8f14bf9c2c16ed94243236ed253a94e5a625afa1ee64022ce29db98c6cd85", size = 34645, upload-time = "2026-02-11T22:45:05.422Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/6d/b90d601ea2449cc6b35b4b08be90fb1f6ca1baf2be383ed195f7bfa91a32/crispy_bootstrap4-2026.2-py3-none-any.whl", hash = "sha256:4b2b99dfe3e3cacb548702159462110901bd38792b650b770e50c62284ac2227", size = 23178, upload-time = "2026-02-11T22:45:04.108Z" }, +] + +[[package]] +name = "croniter" +version = "6.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/57/2e2a65aee2a70483cb28e2b7e15a072d00a523207593b44400d4717bb100/croniter-6.2.4.tar.gz", hash = "sha256:fc124f751b1b04805c2a04b061898b436b45ab2320b045e1e052ea895de65189", size = 166267, upload-time = "2026-07-10T09:52:59.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/ba/d678e5bd329646ca51d3c92addbc77804e86d21f4b6b6a027218e6abb010/croniter-6.2.4-py3-none-any.whl", hash = "sha256:8ef3d544107a5c05a150a2d78f8bf5a8eb9c5c4d93405a736b824109574e3f4d", size = 46677, upload-time = "2026-07-10T09:52:58.425Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/02/bd72be9134d25ed783ecbbc38a539ffaefbf90c78418c7fb7229600dbac7/distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed", size = 615141, upload-time = "2026-06-12T08:04:52.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", size = 470628, upload-time = "2026-06-12T08:04:50.506Z" }, +] + +[[package]] +name = "dj-database-url" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/48/51f398a47c197f584b3445de886986ddc40de18bdb6e168f325a8d2c7bca/dj_database_url-2.2.0.tar.gz", hash = "sha256:9f9b05058ddf888f1e6f840048b8d705ff9395e3b52a07165daa3d8b9360551b", size = 10874, upload-time = "2024-05-28T12:43:28.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/9a/13f173c716d07283661e821f7e1624d0904835151b4f099687455dbef81e/dj_database_url-2.2.0-py3-none-any.whl", hash = "sha256:3e792567b0aa9a4884860af05fe2aa4968071ad351e033b6db632f97ac6db9de", size = 7764, upload-time = "2024-05-28T12:44:25.836Z" }, +] + +[[package]] +name = "dj-email-url" +version = "1.0.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/14/ef/8eb478accd9b0369d46a98d1b43027ee0c254096149265c78e6b2e2fa3b0/dj-email-url-1.0.6.tar.gz", hash = "sha256:55ffe3329e48f54f8a75aa36ece08f365e09d61f8a209773ef09a1d4760e699a", size = 15590, upload-time = "2022-09-24T11:04:50.281Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/f9/fcb9745099d821f9a26092d3d6f4df8f10049885045c3a93ff726d2e40a6/dj_email_url-1.0.6-py2.py3-none-any.whl", hash = "sha256:cbd08327fbb08b104eac160fb4703f375532e4c0243eb230f5b960daee7a96db", size = 6296, upload-time = "2022-09-24T11:04:47.191Z" }, +] + +[[package]] +name = "django" +version = "5.2.16" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "sqlparse" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/26/889449d521ae508b26de715954faecd8bcf3f740affb81b2d146a83b42a5/django-5.2.16.tar.gz", hash = "sha256:59ea02020c3136fce14bef0bbece21a10a4febef5eed1c51c22ae468efa22200", size = 10890894, upload-time = "2026-07-07T13:52:17.005Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/13/1e5e3e4c15dcecb04281b3cb2a46a4670e1cef131068e202f6040df19224/django-5.2.16-py3-none-any.whl", hash = "sha256:04f354bf9d807a86ad1a8392fe3808d362358a8eafc322848e0e43e59b24371d", size = 8311943, upload-time = "2026-07-07T13:52:11.223Z" }, +] + +[[package]] +name = "django-appconf" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/a2/e58bec8d7941b914af52a67c35b5709eceed2caa2848f28437f1666ed668/django_appconf-1.2.0.tar.gz", hash = "sha256:15a88d60dd942d6059f467412fe4581db632ef03018a3c183fb415d6fc9e5cec", size = 16127, upload-time = "2025-11-08T15:46:27.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/e6/4c34d94dfb74bbcbc489606e61f1924933de30d22c593dd1f429f35fbd7f/django_appconf-1.2.0-py3-none-any.whl", hash = "sha256:b81bce5ef0ceb9d84df48dfb623a32235d941c78cc5e45dbb6947f154ea277f4", size = 6500, upload-time = "2025-11-08T15:46:25.957Z" }, +] + +[[package]] +name = "django-cors-headers" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/39/55822b15b7ec87410f34cd16ce04065ff390e50f9e29f31d6d116fc80456/django_cors_headers-4.9.0.tar.gz", hash = "sha256:fe5d7cb59fdc2c8c646ce84b727ac2bca8912a247e6e68e1fb507372178e59e8", size = 21458, upload-time = "2025-09-18T10:40:52.326Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/d8/19ed1e47badf477d17fb177c1c19b5a21da0fd2d9f093f23be3fb86c5fab/django_cors_headers-4.9.0-py3-none-any.whl", hash = "sha256:15c7f20727f90044dcee2216a9fd7303741a864865f0c3657e28b7056f61b449", size = 12809, upload-time = "2025-09-18T10:40:50.843Z" }, +] + +[[package]] +name = "django-crispy-forms" +version = "2.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/f5/b79e3ed7cae871d5d71bf3448d627e1fabab769358bb90808bec056556fe/django_crispy_forms-2.7.tar.gz", hash = "sha256:4c59bed60417375cba26cebb2c67ab350b655934670270b1c89dbcd7e60f1b4c", size = 1097842, upload-time = "2026-07-29T11:12:12.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/44/50335d09c4deb70affee0c54feef07ba458df015e32e45490615d9dc5b6b/django_crispy_forms-2.7-py3-none-any.whl", hash = "sha256:42a7ecb05ac3fd050d006dfe7aeceb7f318c30e5b5124ff619e2be252f36f096", size = 31478, upload-time = "2026-07-29T11:12:11.052Z" }, +] + +[[package]] +name = "django-imagekit" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django-appconf" }, + { name = "pilkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/5f/229c58eefe2a9051bc17152179c46002b519371001ad15097a50226e1306/django_imagekit-6.1.0.tar.gz", hash = "sha256:60fa411eaf5d2d137e1d18d17b3004a6d17b482ca46b3e4b5121a5338a8fc9ff", size = 65550, upload-time = "2026-02-22T18:17:03.951Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/4f/9a3b70941982efb80fc59880a471cf26818879a00f930d80d82342cf63c1/django_imagekit-6.1.0-py3-none-any.whl", hash = "sha256:b735620d101821ae0789480b3945addfeb900f0d7653c7f377e0dc1ed63d7c39", size = 41061, upload-time = "2026-02-22T18:17:02.622Z" }, +] + +[[package]] +name = "django-redis" +version = "5.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "redis" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/9d/2272742fdd9d0a9f0b28cd995b0539430c9467a2192e4de2cea9ea6ad38c/django-redis-5.4.0.tar.gz", hash = "sha256:6a02abaa34b0fea8bf9b707d2c363ab6adc7409950b2db93602e6cb292818c42", size = 52567, upload-time = "2023-10-01T20:22:01.221Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/f1/63caad7c9222c26a62082f4f777de26389233b7574629996098bf6d25a4d/django_redis-5.4.0-py3-none-any.whl", hash = "sha256:ebc88df7da810732e2af9987f7f426c96204bf89319df4c6da6ca9a2942edd5b", size = 31119, upload-time = "2023-10-01T20:21:33.009Z" }, +] + +[[package]] +name = "django-registration" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "confusable-homoglyphs" }, + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/a0/f6e6d0a59b94eb4ab14983853334cb25401d677c5a3799aaca6819acf2eb/django_registration-5.2.1.tar.gz", hash = "sha256:06864f9da0bc4d7b073fb2da98d95d12428357ab0bc46e33f79c26707ec06bbe", size = 94187, upload-time = "2025-04-07T05:54:48.558Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/5d/aa2f82b3c809db66eb986963db1d4b89de190b4eac2e3021a725112e3b13/django_registration-5.2.1-py3-none-any.whl", hash = "sha256:7079e2364b15fc6bef18b81ae94c5e947b556abdbbfb19c9bcca14feafde6a3d", size = 105277, upload-time = "2025-04-07T05:54:46.981Z" }, +] + +[[package]] +name = "django-rq" +version = "4.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "redis" }, + { name = "rq" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/9d/7989e0e4df4ea4af1c95a1a6b751bdfe68d1ac0b15465fc1a0bcb679d551/django_rq-4.1.1.tar.gz", hash = "sha256:6976b8f50d22d844d350b2f0a26b209889bd60ece257b9888d4e5b7d17f622c3", size = 48776, upload-time = "2026-07-04T15:11:07.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/32/512dfa80fce6390deb390da9492bcbabc4824b3f4574890e68daac13b2d4/django_rq-4.1.1-py3-none-any.whl", hash = "sha256:d10a14eaab06aebd223f0a7e5194d129ea126ac9236125150b2ff86e30f27dea", size = 73412, upload-time = "2026-07-04T15:11:06.068Z" }, +] + +[[package]] +name = "djangorestframework" +version = "3.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/d7/c016e69fac19ff8afdc89db9d31d9ae43ae031e4d1993b20aca179b8301a/djangorestframework-3.17.1.tar.gz", hash = "sha256:a6def5f447fe78ff853bff1d47a3c59bf38f5434b031780b351b0c73a62db1a5", size = 905742, upload-time = "2026-03-24T16:58:33.705Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/e1/2c516bdc83652b1a60c6119366ac2c0607b479ed05cd6093f916ca8928f8/djangorestframework-3.17.1-py3-none-any.whl", hash = "sha256:c3c74dd3e83a5a3efc37b3c18d92bd6f86a6791c7b7d4dff62bb068500e76457", size = 898844, upload-time = "2026-03-24T16:58:31.845Z" }, +] + +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + +[[package]] +name = "fakeredis" +version = "2.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "redis" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/34/73256495885f68b125b6a95f6a8448e103adf34825746ab8905db52183a0/fakeredis-2.37.0.tar.gz", hash = "sha256:7461f124dcba04a80691d72270b3d1d5cd100ef14dc068c76db825940f3ed799", size = 236339, upload-time = "2026-07-22T20:04:53.753Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/e2/964e6ef372770dd7c32f9738b50ff4924f1d3cccd665b568680e4bcb0167/fakeredis-2.37.0-py3-none-any.whl", hash = "sha256:657a2a695a1123be0c13f98db409371497bd94c29d260dd76a9fc7ce1a633745", size = 151526, upload-time = "2026-07-22T20:04:51.979Z" }, +] + +[[package]] +name = "filelock" +version = "3.32.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/57/3ba6e6cb097f85b855b00163d169f35365f44277df044dcf96d55b8f62a3/filelock-3.32.2.tar.gz", hash = "sha256:c33351e1f49cae33414acbc6d56784e6ecee82514ec90795da1161fc4836b5b8", size = 217172, upload-time = "2026-07-29T22:46:04.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/e8/72f8cef9fdfeffe06213fe8508039396ee48daa0e3259457ed766173bfd6/filelock-3.32.2-py3-none-any.whl", hash = "sha256:87dd94cf281e586d135fa51132b8e3d9a598b316e90377a288663c9321036c82", size = 98830, upload-time = "2026-07-29T22:46:03.52Z" }, +] + +[[package]] +name = "freezegun" +version = "1.5.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/dd/23e2f4e357f8fd3bdff613c1fe4466d21bfb00a6177f238079b17f7b1c84/freezegun-1.5.5.tar.gz", hash = "sha256:ac7742a6cc6c25a2c35e9292dfd554b897b517d2dec26891a2e8debf205cb94a", size = 35914, upload-time = "2025-08-09T10:39:08.338Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/2e/b41d8a1a917d6581fc27a35d05561037b048e47df50f27f8ac9c7e27a710/freezegun-1.5.5-py3-none-any.whl", hash = "sha256:cd557f4a75cf074e84bc374249b9dd491eaeacd61376b9eb3c423282211619d2", size = 19266, upload-time = "2025-08-09T10:39:06.636Z" }, +] + +[[package]] +name = "furo" +version = "2025.12.19" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "accessible-pygments" }, + { name = "beautifulsoup4" }, + { name = "pygments" }, + { name = "sphinx" }, + { name = "sphinx-basic-ng" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/20/5f5ad4da6a5a27c80f2ed2ee9aee3f9e36c66e56e21c00fde467b2f8f88f/furo-2025.12.19.tar.gz", hash = "sha256:188d1f942037d8b37cd3985b955839fea62baa1730087dc29d157677c857e2a7", size = 1661473, upload-time = "2025-12-19T17:34:40.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/b2/50e9b292b5cac13e9e81272c7171301abc753a60460d21505b606e15cf21/furo-2025.12.19-py3-none-any.whl", hash = "sha256:bb0ead5309f9500130665a26bee87693c41ce4dbdff864dbfb6b0dae4673d24f", size = 339262, upload-time = "2025-12-19T17:34:38.905Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "humanfriendly" +version = "10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyreadline3", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload-time = "2021-09-17T21:40:43.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794, upload-time = "2021-09-17T21:40:39.897Z" }, +] + +[[package]] +name = "identify" +version = "2.6.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "imagesize" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "isodate" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "kegbot" +version = "2.0.0.dev0" +source = { editable = "." } +dependencies = [ + { name = "addict" }, + { name = "coloredlogs" }, + { name = "crispy-bootstrap4" }, + { name = "dj-database-url" }, + { name = "dj-email-url" }, + { name = "django" }, + { name = "django-cors-headers" }, + { name = "django-crispy-forms" }, + { name = "django-imagekit" }, + { name = "django-redis" }, + { name = "django-registration" }, + { name = "django-rq" }, + { name = "djangorestframework" }, + { name = "freezegun" }, + { name = "isodate" }, + { name = "mysqlclient" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "protobuf" }, + { name = "psycopg2" }, + { name = "pyyaml" }, + { name = "redis" }, + { name = "requests" }, + { name = "rq" }, + { name = "waitress" }, + { name = "whitenoise" }, +] + +[package.dev-dependencies] +dev = [ + { name = "fakeredis" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-django" }, + { name = "requests-mock" }, + { name = "ruff" }, + { name = "vcrpy" }, +] +docs = [ + { name = "furo" }, + { name = "sphinx" }, + { name = "sphinx-autobuild" }, + { name = "sphinx-issues" }, +] + +[package.metadata] +requires-dist = [ + { name = "addict" }, + { name = "coloredlogs" }, + { name = "crispy-bootstrap4", specifier = ">=2024.10" }, + { name = "dj-database-url" }, + { name = "dj-email-url" }, + { name = "django", specifier = ">=5.2,<5.3" }, + { name = "django-cors-headers", specifier = ">=4.9,<5" }, + { name = "django-crispy-forms", specifier = ">=2.4,<3" }, + { name = "django-imagekit" }, + { name = "django-redis", specifier = ">=5.4" }, + { name = "django-registration", specifier = ">=5.1" }, + { name = "django-rq", specifier = ">=3,<5" }, + { name = "djangorestframework", specifier = ">=3.16,<4" }, + { name = "freezegun", specifier = ">=1.2.1,<2" }, + { name = "isodate" }, + { name = "mysqlclient" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "protobuf", specifier = ">=5,<7" }, + { name = "psycopg2" }, + { name = "pyyaml" }, + { name = "redis" }, + { name = "requests" }, + { name = "rq", specifier = ">=2,<3" }, + { name = "waitress", specifier = ">=3,<4" }, + { name = "whitenoise" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "fakeredis" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-django" }, + { name = "requests-mock" }, + { name = "ruff" }, + { name = "vcrpy" }, +] +docs = [ + { name = "furo" }, + { name = "sphinx" }, + { name = "sphinx-autobuild" }, + { name = "sphinx-issues" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mysqlclient" +version = "2.2.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/b0/9df076488cb2e536d40ce6dbd4273c1f20a386e31ffe6e7cb613902b3c2a/mysqlclient-2.2.8.tar.gz", hash = "sha256:8ed20c5615a915da451bb308c7d0306648a4fd9a2809ba95c992690006306199", size = 92287, upload-time = "2026-02-10T10:58:37.405Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/44/6ae21b4e8490eb5167d04fa7452c462ffd72a502b96bf531477ef044a380/mysqlclient-2.2.8-cp314-cp314-win_amd64.whl", hash = "sha256:9bed7c8d3b629bdc09e17fb628d5b3b0a5fd1f12b09432b464b9126c727bedc0", size = 211644, upload-time = "2026-02-10T10:58:45.022Z" }, + { url = "https://files.pythonhosted.org/packages/66/e5/037d55623be9f681236e04abe12e1290847c06bd48270c3f19ac33493cbf/mysqlclient-2.2.8-cp314-cp314t-win_amd64.whl", hash = "sha256:260cce0e81446c83bf0a389e0fae38d68547d9f8fc0833bc733014e10ce28a99", size = 213067, upload-time = "2026-02-10T10:58:43.389Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pilkit" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/a5/bbe12d2c9dc06e29224c45a2cd7aa0ce923648588b6a15aadfee150bbd0c/pilkit-3.0.tar.gz", hash = "sha256:f6719e8cc0482e5447f5cb94f18b949d8e604ea9673a9b019c74d41b779e4eab", size = 402342, upload-time = "2023-09-27T22:47:18.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/ec/877b84b82cbcba6203e39d068cecfdbcfb61de69260cb851ea11be9b67f3/pilkit-3.0-py3-none-any.whl", hash = "sha256:fe1707b0411a1d0cbf9ad3986779fa5a346cec4582a188740924aa39f504d117", size = 20073, upload-time = "2023-09-27T22:47:16.285Z" }, +] + +[[package]] +name = "pillow" +version = "12.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" }, + { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" }, + { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" }, + { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" }, + { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" }, + { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" }, + { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" }, + { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" }, + { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" }, + { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" }, + { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" }, + { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" }, + { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" }, + { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" }, + { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" }, + { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" }, + { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" }, + { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" }, + { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" }, + { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" }, + { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" }, + { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" }, + { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/9b/560e4be8e26f6fd133a03630a8df0c663b9e8d61b4ade152b72005aec83b/platformdirs-4.11.0.tar.gz", hash = "sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0", size = 31953, upload-time = "2026-07-21T13:09:36.565Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/68/d8d58938dfb1370b266a1a729e6d77a985be23689a0496498ee17b2cbf90/platformdirs-4.11.0-py3-none-any.whl", hash = "sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74", size = 23247, upload-time = "2026-07-21T13:09:35.422Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/3a/ddb78f32a0814e66b18a099377a106a2dcdce92d86a034d69d65df9b256e/pre_commit-4.6.1.tar.gz", hash = "sha256:03e809865c7d178b9979d06c761fcbfe6808fdaded8581a745bb110e52050421", size = 198646, upload-time = "2026-07-21T20:56:58.225Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/49/bc925106abcdac498074f2cbe6137e94e09f418dd2b7775df5b577dc0313/pre_commit-4.6.1-py2.py3-none-any.whl", hash = "sha256:0e3b2942510d1fb34eec167a3ec57331bf8442122f1153a9fb8b58f5c49b2717", size = 226186, upload-time = "2026-07-21T20:56:57.064Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, +] + +[[package]] +name = "psycopg2" +version = "2.9.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/bc/f66df707ed1aec949fbf24e4460e4f4277a7ba23cdadb3965bb1f634ddb9/psycopg2-2.9.12.tar.gz", hash = "sha256:1dedb1c7a1d8552c4a6044c6b1c41a52e6a8e2d144af83eccac758076b1b7c15", size = 379683, upload-time = "2026-04-20T23:36:11.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/4a/a3566f77501c21a6c2a1fc234dbe5dff74a86e3f150ef070e4ffb835e7f9/psycopg2-2.9.12-cp314-cp314-win_amd64.whl", hash = "sha256:a73d5513bfe929c56555006c7a9cc7ae6e4276aa99dd2b1e2544eb8bb54f8b23", size = 2848588, upload-time = "2026-04-20T23:33:25.983Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyreadline3" +version = "3.5.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/6d/f94028646d7bbe6d9d873c47ee7c246f2d29129d253f0d96cb6fcab70733/pyreadline3-3.5.6.tar.gz", hash = "sha256:61e53218b99656091ddb077df9e71f25850e72e030b6183b39c9b7e6e4f4a9bf", size = 100368, upload-time = "2026-05-14T17:55:04.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/5e/35c856e186b74678c24927847ad9895a51f1bc02a0c6126477a6c6040064/pyreadline3-3.5.6-py3-none-any.whl", hash = "sha256:8449b734232e42a5dcd74048e39b60db2839a4c38cf3ae2bf7707d58b5389c0d", size = 85243, upload-time = "2026-05-14T17:55:03.262Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-django" +version = "4.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/2b/db9a193df89e5660137f5428063bcc2ced7ad790003b26974adf5c5ceb3b/pytest_django-4.12.0.tar.gz", hash = "sha256:df94ec819a83c8979c8f6de13d9cdfbe76e8c21d39473cfe2b40c9fc9be3c758", size = 91156, upload-time = "2026-02-14T18:40:49.235Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/a5/41d091f697c09609e7ef1d5d61925494e0454ebf51de7de05f0f0a728f1d/pytest_django-4.12.0-py3-none-any.whl", hash = "sha256:3ff300c49f8350ba2953b90297d23bf5f589db69545f56f1ec5f8cff5da83e85", size = 26123, upload-time = "2026-02-14T18:40:47.381Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-discovery" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/b7/1581a8103855c43567776aa34135e5ec3c597346c23bfd10c7eb5e0b10a4/python_discovery-1.5.1.tar.gz", hash = "sha256:e2ea8b884cd1701f386eda8cf327b87743f1dc21b7f784470799537d95635384", size = 77200, upload-time = "2026-07-31T22:06:02.48Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/07/a89b539750a159d5101c4eb9fc84e2961f65cefbd5e0b7440b284471c0b0/python_discovery-1.5.1-py3-none-any.whl", hash = "sha256:ac07f44cade589d954e9d6a1e1468539fdddd2cf676beb51da73e0f156b7c932", size = 35752, upload-time = "2026-07-31T22:06:01.116Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "redis" +version = "8.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/99/604f0b666d4c616d891cf77ebb9db6bb21601344c051aebf1b72b9ff915f/redis-8.1.0.tar.gz", hash = "sha256:6e1a19beef9225c83efd689c7e6b7da2d5215b1f42cd13b7fc3714d0a09c7b25", size = 5254356, upload-time = "2026-07-30T08:51:00.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/9d/c5731f6e3608663d4d3656fd8d3aecee8b509c3082818f5a13eae925baea/redis-8.1.0-py3-none-any.whl", hash = "sha256:a4fe1aac3d3b3cc791d4b3d5931c5a956045dc951ee74d1c913ee3ac4d2ee9fb", size = 560618, upload-time = "2026-07-30T08:50:58.497Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "requests-mock" +version = "1.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/32/587625f91f9a0a3d84688bf9cfc4b2480a7e8ec327cefd0ff2ac891fd2cf/requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401", size = 60901, upload-time = "2024-03-29T03:54:29.446Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/ec/889fbc557727da0c34a33850950310240f2040f3b1955175fdb2b36a8910/requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563", size = 27695, upload-time = "2024-03-29T03:54:27.64Z" }, +] + +[[package]] +name = "roman-numerals" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, +] + +[[package]] +name = "rq" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "croniter" }, + { name = "redis" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/a8/7bf65cda593feb5888214a4d1e64aae6fc5eb0bbbb74df922c916099a3e5/rq-2.10.0.tar.gz", hash = "sha256:2d8c533dd27500fedabec06295f18db595966e4f22744e6988fe31155b8f7a21", size = 754610, upload-time = "2026-06-20T03:11:45.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/d3/f8c59750a1c98f99d225954eb2c3dd89f2d66b666c448b4b0dff79ab29ed/rq-2.10.0-py3-none-any.whl", hash = "sha256:1f072e0ff79771f3d3a810170df4c4836dca9ce9f31330a9fc25e25277620ec8", size = 124665, upload-time = "2026-06-20T03:11:47.524Z" }, +] + +[[package]] +name = "ruff" +version = "0.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/25/7113f6d5498888c5fb7db34081cba7d5971c4cb1bfb26819966eee68f003/ruff-0.16.1.tar.gz", hash = "sha256:fedad7c801dabd3fb9741d76aca39246e6ddd9ca446a015875207bf19f1e6bc7", size = 4877500, upload-time = "2026-07-30T19:37:01.379Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/bd/694da69368e0973de65df2ddc73ab18d43c469d5963d9b150911de6bc513/ruff-0.16.1-py3-none-linux_armv6l.whl", hash = "sha256:58edb313b88f0c5460a26adf5f39a37a3be789494a15e3e411e35fa78b89f9a0", size = 10839126, upload-time = "2026-07-30T19:36:13.697Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f0/b626e5d5bd0dd9576263658ef12885e2288afd1029a48e26ffed65ec1ac1/ruff-0.16.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fde5a99e2f97479af66edd6622c6d5a2a7592c77cf4153d9e4428f5eeb55b60c", size = 11070253, upload-time = "2026-07-30T19:36:17.14Z" }, + { url = "https://files.pythonhosted.org/packages/83/63/f40acfb6b35b88623e71684942b552c3edd96035f5d98f313815f7b277de/ruff-0.16.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e0d4c20532fca4f7fa609369161d968dd28f65d83dabbd61d8e9c7edbf7001f6", size = 10561425, upload-time = "2026-07-30T19:36:20.04Z" }, + { url = "https://files.pythonhosted.org/packages/aa/dd/14ec0e9c2b4d315547dd38765004b4863e354e1b52cb308272215d9f6f6d/ruff-0.16.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30affbcedf59ad5703d9c91f82266e02b47739f797e1a7b6e158e5526a6dae38", size = 10948879, upload-time = "2026-07-30T19:36:22.476Z" }, + { url = "https://files.pythonhosted.org/packages/33/e9/9d870cbae575030fdef595f04b4b97573c525b5497cce4f4498cf2f85446/ruff-0.16.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24e9c631573cbca9d20f1283f8f479b2afa4a8503504822bd71a293889f16743", size = 10643691, upload-time = "2026-07-30T19:36:24.914Z" }, + { url = "https://files.pythonhosted.org/packages/c4/09/12743d544e2173f53ecd27217c65f90d2bc0f8424a66a60339e56bbc0457/ruff-0.16.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b41bdd48fb420987a9b5212e4957c26ad4abce401fa9ea9d4d85843727945f4f", size = 11435354, upload-time = "2026-07-30T19:36:28.447Z" }, + { url = "https://files.pythonhosted.org/packages/7f/89/a1652b2daee52083c9554a6333b678a8b01d0400f976827bb87857f9449a/ruff-0.16.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b0d1e1393b7648079e13669de1c1f4fde06d4583e84d8fd5c1551e0a77a2aa75", size = 12259033, upload-time = "2026-07-30T19:36:31.326Z" }, + { url = "https://files.pythonhosted.org/packages/16/96/ecdcb8c54ee7b123b487f807eb014e6e019155a0b81dfb669acd52f28ce3/ruff-0.16.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07bf434b1c95f4e093be4532068ef4fcf00924eb2ade8796075980902d6fd54a", size = 11667981, upload-time = "2026-07-30T19:36:34.394Z" }, + { url = "https://files.pythonhosted.org/packages/cd/90/c52e12e0d862e9572f2a33aa227409143520abe53111e9a6babbac7b4af8/ruff-0.16.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39897739f112253ee4fdd2e8aa9a4f9ded99fb2be367d5f31dfa4ded6025584c", size = 11468183, upload-time = "2026-07-30T19:36:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6b/4ffb7ad1d83eb16cf8cbb3c8815d3f11c88460fd162d4b372a2059be1c2a/ruff-0.16.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:82ae3c0c0d74daf17b968a10b7b3bb3ef297ab7de0c1f749646b25e690ccb150", size = 11470071, upload-time = "2026-07-30T19:36:39.91Z" }, + { url = "https://files.pythonhosted.org/packages/9c/72/32ae7db4c0b5e32ab611787caa19d1546800676d79f7483b7100a3561bf4/ruff-0.16.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4d5f2ed10f8242d83fc08d521301089364e3375375705356f20c0e31606ef3ef", size = 10919503, upload-time = "2026-07-30T19:36:42.65Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ca/3d901ba6ad6fc38da39c3448fc6c59ac945679293a17c3ceb6d6c1cba13e/ruff-0.16.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a4665b309891f83f3e3c25447935f1213e9abbd4b5640af7a1f2def9f8d413c1", size = 10649861, upload-time = "2026-07-30T19:36:45.18Z" }, + { url = "https://files.pythonhosted.org/packages/92/79/894ef1ced26552d5f8c9cf6d85b0687840e1128c55aeab7b9c2d54a0d880/ruff-0.16.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26e9ca5c9bc3971f20d3cf18a957f52ffd6a5f6564ff15c4912a144dcac22494", size = 11148137, upload-time = "2026-07-30T19:36:47.936Z" }, + { url = "https://files.pythonhosted.org/packages/2d/69/3609a09fa1cb46cc28b762363e440a354204e5dff01bd0c8d7437874d6b9/ruff-0.16.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:67e1e1e3fa4f0c82f0e36d4cd61e661f6e7a6196cb1aa92fe0828fa7b8f257cd", size = 11559211, upload-time = "2026-07-30T19:36:50.448Z" }, + { url = "https://files.pythonhosted.org/packages/fc/8a/fb22af2fd78a736e241fabf67e30ce1799a64244026377a49e133af90762/ruff-0.16.1-py3-none-win32.whl", hash = "sha256:d31765e131295b8445caf301e3e8a85b34d1b9b211b4109b7ba457888b051806", size = 10838258, upload-time = "2026-07-30T19:36:53.298Z" }, + { url = "https://files.pythonhosted.org/packages/d4/35/e57fd9fb5d423961df087a00b12d42c0a830288dc2f3b45ecca299158b4f/ruff-0.16.1-py3-none-win_amd64.whl", hash = "sha256:09b05e8b90c2cb06ad63464350e7a45e8e44a2dfe52072ebfba6666ca8d3f596", size = 11961111, upload-time = "2026-07-30T19:36:56.107Z" }, + { url = "https://files.pythonhosted.org/packages/cb/46/240ea004bf6dc4feb40e9832f2205a476a47dd5b8a3f8211a5fc5f95e20e/ruff-0.16.1-py3-none-win_arm64.whl", hash = "sha256:dbaadaac38c70239f056d306b7476f246b0bf000fa6b3876402acbf5b227eaf8", size = 11309414, upload-time = "2026-07-30T19:36:58.79Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "snowballstemmer" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/f8/0a71edf031f03c40db17503cb8ca78a69a171254e568e7db241b0ab57ea1/snowballstemmer-3.1.1.tar.gz", hash = "sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260", size = 123314, upload-time = "2026-06-03T00:56:40.194Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl", hash = "sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752", size = 104164, upload-time = "2026-06-03T00:56:38.614Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/38/e12680bbe6b4f8f3d17adcaf38d26850aa756c85cf4a80e79fc12a018fe8/soupsieve-2.9.1.tar.gz", hash = "sha256:c33e6605bbc71dd628b00c632d58ae607c22bade247e52553928f83bbb75b4ba", size = 122261, upload-time = "2026-07-21T16:57:17.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/2c/437fe806897c2d6cfdc3ee43a18da8bf8e568530a4ae9bac781541ca9896/soupsieve-2.9.1-py3-none-any.whl", hash = "sha256:4f4477399246b7a0c720a88ca2454b11cd6bb9ae4c9d170140786e916776c14c", size = 37404, upload-time = "2026-07-21T16:57:16.421Z" }, +] + +[[package]] +name = "sphinx" +version = "9.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, +] + +[[package]] +name = "sphinx-autobuild" +version = "2025.8.25" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, + { name = "sphinx" }, + { name = "starlette" }, + { name = "uvicorn" }, + { name = "watchfiles" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/3c/a59a3a453d4133777f7ed2e83c80b7dc817d43c74b74298ca0af869662ad/sphinx_autobuild-2025.8.25.tar.gz", hash = "sha256:9cf5aab32853c8c31af572e4fecdc09c997e2b8be5a07daf2a389e270e85b213", size = 15200, upload-time = "2025-08-25T18:44:55.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/20/56411b52f917696995f5ad27d2ea7e9492c84a043c5b49a3a3173573cd93/sphinx_autobuild-2025.8.25-py3-none-any.whl", hash = "sha256:b750ac7d5a18603e4665294323fd20f6dcc0a984117026d1986704fa68f0379a", size = 12535, upload-time = "2025-08-25T18:44:54.164Z" }, +] + +[[package]] +name = "sphinx-basic-ng" +version = "1.0.0b2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/0b/a866924ded68efec7a1759587a4e478aec7559d8165fac8b2ad1c0e774d6/sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9", size = 20736, upload-time = "2023-07-08T18:40:54.166Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/dd/018ce05c532a22007ac58d4f45232514cd9d6dd0ee1dc374e309db830983/sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b", size = 22496, upload-time = "2023-07-08T18:40:52.659Z" }, +] + +[[package]] +name = "sphinx-issues" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/4e/c9a46b19c90d4d3cdcbcaf0e4d392b09e5b41ec75b134bc5a1aa192be94f/sphinx_issues-6.0.0.tar.gz", hash = "sha256:f40f2c71cecb2068c7f06a53825778b674d58d8395b4ea60551a36164d2988e3", size = 15230, upload-time = "2026-03-13T17:23:32.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/8a/28333d222ac31539aa0d818db0790077e5a6a408f933b4bf64f7d3440ffc/sphinx_issues-6.0.0-py3-none-any.whl", hash = "sha256:c7ed2915059526d02022733985a7712d4b2b64a707e16b98294ed9758e64df4f", size = 8362, upload-time = "2026-03-13T17:23:31.031Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + +[[package]] +name = "sqlparse" +version = "0.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" }, +] + +[[package]] +name = "starlette" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.52.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/03/18/ccce41535dee1be77735592bd19965f3972c82e07ee703d324709496b716/uvicorn-0.52.1.tar.gz", hash = "sha256:112ec661814189acbccd3f7b86460147cc065fc92c0821afa78918780e4354dd", size = 100571, upload-time = "2026-08-01T18:19:30.732Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/d5/68e6e9bca63c0badf67002890a46d3784c958de45b65e1275ec583ca1f06/uvicorn-0.52.1-py3-none-any.whl", hash = "sha256:e4403f9d93188cf9d1088e9f40e3acd12630e2df8675316704379a7fc20fff6a", size = 79859, upload-time = "2026-08-01T18:19:29.294Z" }, +] + +[[package]] +name = "vcrpy" +version = "8.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/d5/8a1f8eb603e2d35fbb0ecd1e309d0c5c18a0ecfc8c0a8f04088bbc8f833b/vcrpy-8.3.0.tar.gz", hash = "sha256:46d64e77e8d95e5c76c7d9a94ff05d8b38b2ae4e1d4869eb0235024b6fcb5212", size = 96117, upload-time = "2026-07-04T14:27:01.608Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/77/cb4219be91508399cbcb6143bad89462cfb16f6c638458f454a5d46ac95a/vcrpy-8.3.0-py3-none-any.whl", hash = "sha256:bd66e6143746778157f00e2a922527a8d96b2fdc350be8988a45a29c843815b9", size = 46530, upload-time = "2026-07-04T14:27:00.546Z" }, +] + +[[package]] +name = "virtualenv" +version = "21.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ea/fa/18004e5cb15541ad2a68ff219c755233b012b12d4ec8663d06a258082bec/virtualenv-21.7.1.tar.gz", hash = "sha256:d0dbfaa5483487baea28d7210ef8d24c9d1bd0f10f449eeb215568825a9b334e", size = 5525237, upload-time = "2026-07-30T15:40:36.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/a7/ded126c19495158a05c7202b3389139839d4cf78d622d453867778e0f7a8/virtualenv-21.7.1-py3-none-any.whl", hash = "sha256:6394973f990536e34c05157179146c020284c42fe01da1dfeb0ba16c345280d9", size = 5504576, upload-time = "2026-07-30T15:40:34.512Z" }, +] + +[[package]] +name = "waitress" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/cb/04ddb054f45faa306a230769e868c28b8065ea196891f09004ebace5b184/waitress-3.0.2.tar.gz", hash = "sha256:682aaaf2af0c44ada4abfb70ded36393f0e307f4ab9456a215ce0020baefc31f", size = 179901, upload-time = "2024-11-16T20:02:35.195Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/57/a27182528c90ef38d82b636a11f606b0cbb0e17588ed205435f8affe3368/waitress-3.0.2-py3-none-any.whl", hash = "sha256:c56d67fd6e87c2ee598b76abdd4e96cfad1f24cacdea5078d382b1f9d7b5ed2e", size = 56232, upload-time = "2024-11-16T20:02:33.858Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" }, + { url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" }, + { url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" }, + { url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" }, + { url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" }, + { url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" }, + { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" }, + { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" }, + { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" }, + { url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" }, + { url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" }, + { url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" }, + { url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" }, + { url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" }, + { url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" }, + { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" }, + { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" }, + { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" }, + { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" }, +] + +[[package]] +name = "websockets" +version = "17.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/96/e01084f83a64bcb3a27994bd0cb0db68ff29d9c6707fae37ec19b18ba990/websockets-17.0.1.tar.gz", hash = "sha256:5baa9bc0dfbae8c507e51c8cf1b6d4628086f7a87bbd3a9952bd5f035451f1cc", size = 183298, upload-time = "2026-07-31T11:31:27.665Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/dc/cadab608924ac605647031472fb1f8792d7d4ea07565ba1899ec42028e0d/websockets-17.0.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:53b90c00bc6201ab6695c7ff51a04d0e425514c37515e9eeecd2c1b978ac6c0e", size = 212640, upload-time = "2026-07-31T11:30:22.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/7a/b034d13ca181211bbd58bb50835cb196a7784cd505b5a2079d4d03374f9f/websockets-17.0.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5f33a649bfcb8312524173cc4bbafa7dbb236e18eee9aa31a1d324ca0ddda28c", size = 210332, upload-time = "2026-07-31T11:30:23.608Z" }, + { url = "https://files.pythonhosted.org/packages/2f/4d/943ede39b53744768edf1ed84a3f9401527388228a3d6c1249c02c3d6bd7/websockets-17.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cddc675ec31bca65473321f9a9794e488b43b3b8de5d02c8ef4810c5d5792163", size = 210546, upload-time = "2026-07-31T11:30:24.932Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/88dc159d2ae66743c669443246243f28d873b0c5e58271b8cc1ca0440334/websockets-17.0.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b3ff0ad440ad52dda64138f16895f66403f40192365e39b1010e889f289746b0", size = 219928, upload-time = "2026-07-31T11:30:26.221Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f2/ff27eaefa15851a5cf7f004ab827a022bf2d6632cb520f89cb100db7e84b/websockets-17.0.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:72d7f2a5aeb4e82daa4ee18f125b4277f427033359be5c745ad709608446cc2c", size = 220279, upload-time = "2026-07-31T11:30:27.49Z" }, + { url = "https://files.pythonhosted.org/packages/19/2e/a5166149f363d2449c1cb2dde6486a245521979509d53b87a09f3e79662b/websockets-17.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6fd88365da261c53d3e943fb37e0d0721b9cde119f6b2e3fc84369b6ab234d63", size = 221525, upload-time = "2026-07-31T11:30:28.872Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/f46931269b3ff3bde65d27c65ddb22f9bb8ce92ac2c6c4df0910128f6219/websockets-17.0.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ab9f962a5b64a5c3c845d556b7dc4e6fb683f7b67179f8205e814bb2e0213ffe", size = 222897, upload-time = "2026-07-31T11:30:30.164Z" }, + { url = "https://files.pythonhosted.org/packages/42/f4/deccf3439f35df953ec35e13fe07986821c5f1ab5785d69614283bdb9034/websockets-17.0.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8c07f145d0b9e90cbd96035f31fb79199aef4da1872854e36ebeb258e3d57594", size = 222129, upload-time = "2026-07-31T11:30:31.489Z" }, + { url = "https://files.pythonhosted.org/packages/35/a5/e1b57a59da92ade37fd021567a17b518ea8267b28e5530075844cdb525fe/websockets-17.0.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9f7747d3daa41a11f25f7cca5dc988fc51da97b311bed4c9d843860f79779283", size = 220875, upload-time = "2026-07-31T11:30:32.805Z" }, + { url = "https://files.pythonhosted.org/packages/f0/30/e7d0889c790a854156de424575fd67af79ddbaed9ff3157ae863dfd1c1dc/websockets-17.0.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2abb1ba0a5133b7d2ef3c1c9f4b0c1e8a101012dce0b594ab2b2888d9a64820e", size = 218160, upload-time = "2026-07-31T11:30:34.512Z" }, + { url = "https://files.pythonhosted.org/packages/09/2e/43db785d6ed9ae7594fae7b62bbc9cb4dfee2b015e06a1005f1e5ce283b6/websockets-17.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f3fd9a1f87f8f0f3f8e9f9bd0195f7516562d13f5b178db8c5784d1f60b60bed", size = 220951, upload-time = "2026-07-31T11:30:35.806Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f5/4ac3cab3d5e8a830657a822f64a8910e3803229c6783e59c3fd9a3487427/websockets-17.0.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2bc14b481e05e331811108daa1aeb41a5e237a5564ef2f02ec5a356a0f102f78", size = 219460, upload-time = "2026-07-31T11:30:37.273Z" }, + { url = "https://files.pythonhosted.org/packages/03/0e/c3a4020673ffc17c82cf1a467835038a196a555d3b4f2a50f0f063cf8ccc/websockets-17.0.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:57d2ee9b24b404ce75f3814f92073c0ed88106c950148d2427fe8d25ca254d1f", size = 220248, upload-time = "2026-07-31T11:30:38.527Z" }, + { url = "https://files.pythonhosted.org/packages/7d/87/e47a6a278cc1dfade38444c893ce18322943c25d4b780a74450d9d164be1/websockets-17.0.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1b363bfd72a52c0658a3154a4cff219f15a474b35a235057d38853bf151acce7", size = 221421, upload-time = "2026-07-31T11:30:39.879Z" }, + { url = "https://files.pythonhosted.org/packages/da/8f/473d5fc4e3836e375b0233c6ef26777e6e5e3f7bfc84ccd524eae4090ed5/websockets-17.0.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:10b1587c599fa0f2c89154587c80e0fda98ade6c9fa8c0260a2823fb1800b685", size = 218975, upload-time = "2026-07-31T11:30:41.192Z" }, + { url = "https://files.pythonhosted.org/packages/d4/b9/819ec2dcdf69031d7e9cab11247f3a6ff9bbc8c7c53ada1dbcb9055b227b/websockets-17.0.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d7d72843691f50b91127c50688df10cb72ec6f4c4b1d7e2c11ab33b16acf8e51", size = 219925, upload-time = "2026-07-31T11:30:42.524Z" }, + { url = "https://files.pythonhosted.org/packages/85/b9/6c0da301f6118502e079cf92f4e864adf28e56b3f8c0f6085076ced7b876/websockets-17.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90973a3a00f23afdfd1c9b06fb84289bf0220f247ef8a62501a1967c7af54f7b", size = 220218, upload-time = "2026-07-31T11:30:44.04Z" }, + { url = "https://files.pythonhosted.org/packages/55/f9/cba32dc9dd856565263d6272f594255bd0e2781deb8cd982c026a54760ad/websockets-17.0.1-cp314-cp314-win32.whl", hash = "sha256:599b03beb77633bffc095334338fad79cafc2b01fbd58953838130a9ae967d7b", size = 212626, upload-time = "2026-07-31T11:30:45.579Z" }, + { url = "https://files.pythonhosted.org/packages/fa/95/91cdd8c192287d7ea741f37cf7d64fdc1a14410f06f73805e428a1a590af/websockets-17.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:81ce19c6046ace11da7001781be7317bb1dc389f399af4b2ed962190f76f9add", size = 212969, upload-time = "2026-07-31T11:30:46.983Z" }, + { url = "https://files.pythonhosted.org/packages/8e/fd/8c98a1e431960661c5769ab1a4dd66494e87ab02d791cc79e51e0d9a289f/websockets-17.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:efe0ae052a8d023b87198921e8a7ce1dc7768816bcd2fbc20df171ac73a04891", size = 212850, upload-time = "2026-07-31T11:30:48.304Z" }, + { url = "https://files.pythonhosted.org/packages/13/c1/142f5186ee7dc3beee0426b998a79e223e067b7689afcaa95890d64aa800/websockets-17.0.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ab56439c9f74c52770690c7b2f616b3bf775cb3920453ee355ac765c032d8bbf", size = 212967, upload-time = "2026-07-31T11:30:49.688Z" }, + { url = "https://files.pythonhosted.org/packages/02/de/4b03ed316c9dee180365286c298219809ff247be39beeaaf9958b21167ab/websockets-17.0.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:20a92f78ac8250984ed459faa9ca48c285adbfc0038ddc3fdac6046990a9c9ed", size = 210504, upload-time = "2026-07-31T11:30:50.987Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d8/ad2b3e8f867e1e8cac3077e2f33ffb60b71bd763d6cfc71bd916f113c3bf/websockets-17.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6a434e59962a4fb9016bea327e1d14d6cd67670ecfb8942b4f4a0c24036634ce", size = 210702, upload-time = "2026-07-31T11:30:52.261Z" }, + { url = "https://files.pythonhosted.org/packages/48/18/7a77a82ce9d6f831c07b176da3942f7e71acd0f115f3ecdb1d00a040eb01/websockets-17.0.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2503c7e2a5049a12d5dac917a46d5d52591283a766165b8176bb167560421b38", size = 220290, upload-time = "2026-07-31T11:30:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/f5/af/43c3e3c3ea7ba4693c2181743f3221957df28bededadcbd9fc8a0661bde0/websockets-17.0.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:28012a54510fe8301bb893ef143cec30a2780a2d3bc20b7bbdf4379d7a63945d", size = 220573, upload-time = "2026-07-31T11:30:54.966Z" }, + { url = "https://files.pythonhosted.org/packages/1f/bd/ed48eca15725743ee7e2dc172e15c61de29e85ec98dace7b14257f366836/websockets-17.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22bd00f8bae2bccdb5dbe41e20f58ba44ca9fff0b4b561aaf39099c35da762ed", size = 221747, upload-time = "2026-07-31T11:30:56.762Z" }, + { url = "https://files.pythonhosted.org/packages/99/50/838deb7937a8225c4925dd4a977eafea473fabf444178a99de0bc7e92bb0/websockets-17.0.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e98ec9ec61cce5bc4b8b218322ad090b0994eb060bb04da704c62ef0a3d864e6", size = 223891, upload-time = "2026-07-31T11:30:58.127Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e9/657fb70c6eb6bcd01adfa5d2b06496e9911e1c1a8813d353b8c00f7591cd/websockets-17.0.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8e387adb0c692c6b5571bdeafc8ac9d1901ea30f10309134780b16ecd35e6605", size = 222317, upload-time = "2026-07-31T11:30:59.416Z" }, + { url = "https://files.pythonhosted.org/packages/07/4c/82cb722afa5428fed981331210c4c07600570db01bb1620579f655b5adaf/websockets-17.0.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd1470d2c53fe53269bf5619da7725d30dd9b9693f1689f7a85eab8dea734442", size = 221047, upload-time = "2026-07-31T11:31:00.757Z" }, + { url = "https://files.pythonhosted.org/packages/90/84/bd6d67d6bc65f0de0cb50de55dab42f256a9876a351c4736522eb168fda0/websockets-17.0.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:884af729b8ab50486acd94d9768c2b60914bf39b579ebba0a5cb73bfdfd61fd2", size = 218626, upload-time = "2026-07-31T11:31:02.48Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/6a372c8553976f0d8f97f5115826ed47e34b3be6b8bf0d0249af249a7416/websockets-17.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a60fa1a25cca1bcc2bf87b8d6be37a741f0a3239fb5e9cfb7a37173b68ffcf87", size = 221299, upload-time = "2026-07-31T11:31:03.795Z" }, + { url = "https://files.pythonhosted.org/packages/bc/31/f966e8472337974f74d788b3ef6c6f3b8b9a5f201efd16a843c91d269fa5/websockets-17.0.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:e8208f2729cba030ff872a92064c97584eeb9502f53d32a05a0f05d5a17ca6c6", size = 219789, upload-time = "2026-07-31T11:31:05.08Z" }, + { url = "https://files.pythonhosted.org/packages/57/34/404e83a6cc7b0efcac810b7041bffd72ff76900e6fd0aa45a26c92fb2ffe/websockets-17.0.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:54cdcaa56f5d3eafd57058f0fa4a3de93a310b43a3c4699f06efc4c0bd054a5a", size = 220678, upload-time = "2026-07-31T11:31:06.635Z" }, + { url = "https://files.pythonhosted.org/packages/e5/70/8946188c2a68d67251859b589a3634918cf7867bf0b891347a5ecaa43d30/websockets-17.0.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4d41c0a1d47a478bc432b3b9068097bee1ce0c5b19327ea6f75c2ab34ab1f2fb", size = 221697, upload-time = "2026-07-31T11:31:08.023Z" }, + { url = "https://files.pythonhosted.org/packages/04/16/ee73fc2083a2938ac6209f4ec804960496835b20a0068dbcfe8424957c04/websockets-17.0.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f991247276797d0c61ab7770bc9791eadc16f683b4d83517f624932adc1a8bab", size = 219390, upload-time = "2026-07-31T11:31:09.378Z" }, + { url = "https://files.pythonhosted.org/packages/94/6a/d5f88033c69932af6cdaa72da62516ade47c257e3bf69f4c0ba5f40e12a2/websockets-17.0.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:733e3cc7171fa1b899edbe725ef9382d0e960657dc1fd933f3281ae910c01dab", size = 220161, upload-time = "2026-07-31T11:31:10.915Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/6183dd2c0370287ecf4afe0bb33aca364208e5e7b0e1a286adcaecc0c78b/websockets-17.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:810cb3fb5fa6e447216f4e82d9a85cb8aed0929ae3538153ddfe8a6e3121a58d", size = 220591, upload-time = "2026-07-31T11:31:12.289Z" }, + { url = "https://files.pythonhosted.org/packages/26/93/70f6516d85b9744f7eac224c4b1b9ef4e84133f80b53be02080cb1c3e663/websockets-17.0.1-cp314-cp314t-win32.whl", hash = "sha256:17ac37716c0244e82c9e384c41653c090b1864c6610224ca3857e7f7b58fce10", size = 212755, upload-time = "2026-07-31T11:31:13.883Z" }, + { url = "https://files.pythonhosted.org/packages/f1/2c/9d9c1da5a7ea9af307b386d25f64d1dead4729644198d2b92e36db5dfd41/websockets-17.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:bb31f42ea095ea826463c770829aa188a86c9a5c976b1467cbbf583c811de833", size = 213094, upload-time = "2026-07-31T11:31:15.367Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/a585e7573e128070605d003b5544729bcd58d9756c7e99d550818ca4b916/websockets-17.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:dbfae8e75b342e31fc6fd1a8bbb393b7cbb91d6cfd581650300a94381e7b7e2b", size = 213009, upload-time = "2026-07-31T11:31:16.776Z" }, + { url = "https://files.pythonhosted.org/packages/09/ce/3929538b2b9918f5eee623fbf3346893973191f6df93f19bbda097bd7bb7/websockets-17.0.1-py3-none-any.whl", hash = "sha256:c6be9cba65c65cc76dfa3d4619e359ff02a4476c74e179b215236c11a0b32345", size = 206718, upload-time = "2026-07-31T11:31:26.037Z" }, +] + +[[package]] +name = "whitenoise" +version = "6.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/2a/55b3f3a4ec326cd077c1c3defeee656b9298372a69229134d930151acd01/whitenoise-6.12.0.tar.gz", hash = "sha256:f723ebb76a112e98816ff80fcea0a6c9b8ecde835f8ddda25df7a30a3c2db6ad", size = 26841, upload-time = "2026-02-27T00:05:42.028Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/eb/d5583a11486211f3ebd4b385545ae787f32363d453c19fffd81106c9c138/whitenoise-6.12.0-py3-none-any.whl", hash = "sha256:fc5e8c572e33ebf24795b47b6a7da8da3c00cff2349f5b04c02f28d0cc5a3cc2", size = 20302, upload-time = "2026-02-27T00:05:40.086Z" }, +] + +[[package]] +name = "wrapt" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/b0/c1f5a970721f06b85c0cd5142e0ff8fe067708abd779b0c4f4be7d61d09f/wrapt-2.3.0.tar.gz", hash = "sha256:681a2d0eefd721998f90642762b8e75c2159ec531b20ad5e437245ea7b06a107", size = 131509, upload-time = "2026-07-28T06:06:14.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/10/b073beaea89bc0d3670a75ff51139430a54b6af7ba7796507730634536dd/wrapt-2.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ea52a0d0f08c584943d5764be0e84efa912c8da23c23e1e285ff2f5641c18fcc", size = 81978, upload-time = "2026-07-28T06:05:21.133Z" }, + { url = "https://files.pythonhosted.org/packages/b3/31/0916d9cebf848ed3f1a0c1888faee421747df77331e4db2bc527a9a85988/wrapt-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd85b0aa88efdb189d6ae2f35f4526943a8f091c38599c9c31478241c819e6a1", size = 82518, upload-time = "2026-07-28T06:05:22.562Z" }, + { url = "https://files.pythonhosted.org/packages/f5/73/31c1bf0f3384062751c2094dadb314916d70aa9b6bfd26d994b4a7b393fa/wrapt-2.3.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:141ed6211286a9660d8d6702de598b43f0934b4f0eda16393f100a80f501d945", size = 170187, upload-time = "2026-07-28T06:05:23.904Z" }, + { url = "https://files.pythonhosted.org/packages/ed/25/fce087d54b79b8905f3c3c9dd5f454bbd8d8acb80b960c4a6aee5b4659b3/wrapt-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e49885a62ec4ee854d1b9e6371fda6afd219917225752abf729a3f36d4df9a5", size = 169288, upload-time = "2026-07-28T06:05:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/c7/30/0d09e6dddc6b7a7230ac77f50254b5980ab4fcd22976f72f8cc8a0404458/wrapt-2.3.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d6159c9b2fefec02314e1332dbbbfaf960e369dfd26bcf7f8b258b5732065b3", size = 160932, upload-time = "2026-07-28T06:05:27.022Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ca/0913af0d2ec0c43865d32d615f518fea66c13c5c930e489e9b0de248e9a8/wrapt-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24da48596326ef8e448cfa837b454f638713d3531262375f00e5a9681682fc07", size = 169017, upload-time = "2026-07-28T06:05:28.501Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f2/3d1e47ea81b822210f5df1bf942fd90780a75c055243d569b664529dea88/wrapt-2.3.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cd3a2edf0427013736b8127955cec62608c56e53ea47e82812ea32059cda407f", size = 159065, upload-time = "2026-07-28T06:05:30.01Z" }, + { url = "https://files.pythonhosted.org/packages/43/a5/ef2066ced8e5fca204e2b361e9708e36555b40949c583d997ea3b590817d/wrapt-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4fa0df3bff4e7ce45759f33fd39335fe2f60477bb9ecf7b8aa41e7d07ee36a23", size = 168821, upload-time = "2026-07-28T06:05:31.649Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e1/016104650d4e572fa91506eb396b3dd8efbccc9284fdc1c9479c3d21db28/wrapt-2.3.0-cp314-cp314-win32.whl", hash = "sha256:2935d5454b3f179a29b12cf390ee47246740ba2c3a7545b1b46ba31a5f2a4a0b", size = 78700, upload-time = "2026-07-28T06:05:33.391Z" }, + { url = "https://files.pythonhosted.org/packages/3d/97/6fdc20a9f2ca304748b3f0819cbf377d55260562777bf0b615431bc3c181/wrapt-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:cc2cea812e5cb179a796b766747e7d3b21088760d8deb95676d482b8c8e6fa7d", size = 81422, upload-time = "2026-07-28T06:05:34.774Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a4/9cbd53bf05746bea2c392af39cb052427a8ec95cbd494d930733d8f44681/wrapt-2.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:22cc5c0a717bd4da87018ae0bffd4c19c6fb679d3ff357216ba566ab26c76cab", size = 80639, upload-time = "2026-07-28T06:05:36.228Z" }, + { url = "https://files.pythonhosted.org/packages/43/bb/6c5e4a0f66ea0d2b2dd267e8dd05a0014eea56840b3c8595d40b0a5d1f91/wrapt-2.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a6b5984cd65dd639546f0eb4b8eacf1c31cb2fe9fb5c27bffe240987cdb2cf84", size = 84030, upload-time = "2026-07-28T06:05:37.714Z" }, + { url = "https://files.pythonhosted.org/packages/6a/eb/a1aedf03283bc9cbf8a1783995ddc54e3c5a86878f19002d2c428494f4c5/wrapt-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c88abcf53daef80e01a75c7530e727fa6e2c1888fe83e3dcdba4c96216a1f5c7", size = 84419, upload-time = "2026-07-28T06:05:39.131Z" }, + { url = "https://files.pythonhosted.org/packages/63/61/50d511c0dc5105563849e86daa3e16ac7feef699f79fb05af45ea70107d5/wrapt-2.3.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:85de890ff968196e92dd1ae73a9fb8970495e7650a457b1c9ef0ac3dd550bce2", size = 207171, upload-time = "2026-07-28T06:05:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/3f/59/9b538cf7795217e810699d16bc88b96a830d9b5c403eb2ec2db6b5f2ae81/wrapt-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50f416b74d092bb9f41b424e90dd457f365f7ba4b11de62a23679769a21bd85c", size = 214329, upload-time = "2026-07-28T06:05:42.287Z" }, + { url = "https://files.pythonhosted.org/packages/b3/28/9935d62b1499e5c8b3d191e99ba4eb31ca237a0b699142011a837e9dc7ea/wrapt-2.3.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39febbee6d77301d31da6996b152ce52452da7c7ef72aba10c2fa976dff9c295", size = 199079, upload-time = "2026-07-28T06:05:43.958Z" }, + { url = "https://files.pythonhosted.org/packages/2b/01/4446b80fa2ffa47a3449b250d004ba1c1937f07f64a179608fec735df866/wrapt-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:93513bec052c6cd987f9f580c3df068c8bc4ebae6543736be3ca7ec5959cafcd", size = 209992, upload-time = "2026-07-28T06:05:45.677Z" }, + { url = "https://files.pythonhosted.org/packages/d4/07/56f26c9f9979586a021e8148747004aba4498f49458c90b0502969b904e1/wrapt-2.3.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:729126e667da34d251b8ebf8a45ef0c5ddadc21542b3d6e1abf4259ece6508df", size = 196334, upload-time = "2026-07-28T06:05:47.608Z" }, + { url = "https://files.pythonhosted.org/packages/8b/41/6d7bcc895b0f28b2250e10908f060687b9165429dcd7f22ddb3d4c031b74/wrapt-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:626b69db2021aa01671ec7bbc9740e558522bd44c18cf2ce69bf3d666a014109", size = 202644, upload-time = "2026-07-28T06:05:49.183Z" }, + { url = "https://files.pythonhosted.org/packages/cd/25/7860927edba06b758b8852a6f02e832be715563c67a6795d94350bc81099/wrapt-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:629d73378082c00a8173031f9fb30a3ac6abbc894a5bfdfae71fabc60642d501", size = 79685, upload-time = "2026-07-28T06:05:50.976Z" }, + { url = "https://files.pythonhosted.org/packages/c4/0f/270bafe92fde3b069a39bc01e39ee79340895b335640df861d43d2a51885/wrapt-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:42869085687f0aefd57c0f636c3f9354f8ffb321a8ba9cb52d19beb796e561c5", size = 83104, upload-time = "2026-07-28T06:05:52.405Z" }, + { url = "https://files.pythonhosted.org/packages/55/b3/af176d79a8515a8a720eccdad9a96f6e31a30abf2865430c8c42adf2fd13/wrapt-2.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b1e5aa486e269b00ed35e64771c7d0ab8096cfd2643405ca8cd60ebedc099a51", size = 81774, upload-time = "2026-07-28T06:05:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/00/39/3daf9f47be208606586de4568ba6713db53ebc8fd7a575aea1fe57983b69/wrapt-2.3.0-py3-none-any.whl", hash = "sha256:d8c7ed08477429752b8c44991f40ad7838b18332a160698740a6bfbc10d998a2", size = 61866, upload-time = "2026-07-28T06:06:12.9Z" }, +]