From ddc34bf8bbce38c4498354337651004e9339e038 Mon Sep 17 00:00:00 2001 From: Shane <6071159+smashedr@users.noreply.github.com> Date: Tue, 21 Jul 2026 17:41:21 -0700 Subject: [PATCH 1/2] Update All The Things --- .github/actionlint.yaml | 6 + .github/workflows/ci.yaml | 110 ++++ .github/workflows/lint.yaml | 131 +++++ .github/yamllint.yaml | 9 + .gitignore | 21 +- .prettierignore | 4 + .prettierrc.json | 19 + .python-version | 1 + Jenkinsfile | 104 ---- README.md | 15 +- app/Dockerfile | 32 +- app/docker-entrypoint.sh | 12 +- app/home/admin.py | 1 + app/home/apps.py | 4 +- app/home/models.py | 10 +- app/home/templatetags/home_tags.py | 19 +- app/home/urls.py | 17 +- app/home/views.py | 228 ++++---- app/manage.py | 5 +- app/myapp/settings.py | 191 ++++--- app/myapp/urls.py | 24 +- app/myapp/views.py | 21 +- app/myapp/wsgi.py | 2 +- app/oauth/admin.py | 23 +- app/oauth/forms.py | 7 +- app/oauth/models.py | 4 +- app/oauth/urls.py | 9 +- app/oauth/views.py | 138 ++--- app/requirements.txt | 10 - app/setup.cfg | 6 - docker-compose-stack.yaml | 68 +++ docker-compose-swarm.yaml | 134 ++--- docker-compose.yaml | 67 +-- nginx/Dockerfile | 4 + nginx/nginx.conf | 16 +- pyproject.toml | 102 ++++ settings.env.example | 50 ++ uv.lock | 833 +++++++++++++++++++++++++++++ 38 files changed, 1817 insertions(+), 640 deletions(-) create mode 100644 .github/actionlint.yaml create mode 100644 .github/workflows/ci.yaml create mode 100644 .github/workflows/lint.yaml create mode 100644 .github/yamllint.yaml create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 .python-version delete mode 100644 Jenkinsfile delete mode 100644 app/requirements.txt delete mode 100644 app/setup.cfg create mode 100644 docker-compose-stack.yaml create mode 100644 pyproject.toml create mode 100644 settings.env.example create mode 100644 uv.lock diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 0000000..447aaa1 --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,6 @@ +paths: + .github/workflows/**/*.{yml,yaml}: + ignore: + - '"inputs" section is alias node but mapping node is expected' + - '"paths" section must be sequence node but got alias node with "" tag' + - '"paths-ignore" section must be sequence node but got alias node with "" tag' diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..15024bd --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,110 @@ +name: "CI" + +on: + workflow_dispatch: + inputs: + build: + description: "Run Build" + type: boolean + default: true + deploy: + description: "Run Deploy" + type: boolean + default: true + cleanup: + description: "Run Cleanup" + type: boolean + default: true + env: + description: "Environment to Deploy" + required: true + default: "dev" + type: choice + options: + - dev + - prod + + push: + branches-ignore: + - master + paths: + - ".github/workflows/ci.yaml" + - "app/**" + - "nginx/**" + - "docker-compose-swarm.yaml" + - "pyproject.toml" + - "uv.lock" + +env: + config-path: shane-red-api + stack-name: ${{ inputs.env || 'dev' }}_${{ github.repository_owner }}-${{ github.event.repository.name }} + env-name: ${{ inputs.env || 'dev' }} + build: ${{ github.event_name == 'push' && true || inputs.build }} + deploy: ${{ github.event_name == 'push' && true || inputs.deploy }} + cleanup: ${{ github.event_name == 'push' && true || inputs.cleanup }} + +concurrency: + group: ${{ inputs.env || 'dev' }} + cancel-in-progress: true + +jobs: + ci: + name: "CI" + runs-on: ubuntu-latest + timeout-minutes: 15 + if: ${{ !contains(github.event.head_commit.message, '#noci') }} + concurrency: + group: ci + cancel-in-progress: true + permissions: + contents: read + packages: write + + steps: + - name: "Checkout" + uses: actions/checkout@v6 + + - name: "Debug event.json" + continue-on-error: true + run: cat "${GITHUB_EVENT_PATH}" + - name: "Debug CTX github" + continue-on-error: true + env: + GITHUB_CTX: ${{ toJSON(github) }} + run: echo "$GITHUB_CTX" + + - name: "Debug" + continue-on-error: true + run: | + echo group: ${{ inputs.env || 'dev' }} + echo github.event_name: ${{ github.event_name }} + + echo inputs.build: ${{ inputs.build }} + echo inputs.deploy: ${{ inputs.deploy }} + echo inputs.cleanup: ${{ inputs.cleanup }} + + echo env.build: ${{ env.build }} + echo env.deploy: ${{ env.deploy }} + echo env.cleanup: ${{ env.cleanup }} + + echo env.stack-name: ${{ env.stack-name }} + echo env.env-name: ${{ env.env-name }} + + - name: "Swarm Build Action" + if: ${{ env.build == 'true' }} + uses: hosted-domains/swarm-build-action@master + with: + version: ${{ env.env-name }} + username: ${{ vars.GHCR_USER }} + password: ${{ secrets.GHCR_PASS }} + + - name: "Portainer Deploy Action" + if: ${{ env.deploy == 'true' }} + uses: hosted-domains/portainer-deploy-action@master + with: + version: ${{ env.env-name }} + stack-name: ${{ env.stack-name }} + config-file: services/${{ env.config-path }}/${{ env.env-name }}.env + config-ssh-key: ${{ secrets.SERVICE_CONFIGS_KEY }} + portainer-url: ${{ secrets.PORTAINER_URL }} + portainer-token: ${{ secrets.PORTAINER_TOKEN }} diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..79059ec --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,131 @@ +name: "Lint" + +on: + workflow_dispatch: + push: + branches: [master] + paths-ignore: + - "app/static/**" + - "*.env*" + - "nginx/nginx.conf" + - ".dockerignore" + - ".gitattributes" + - ".gitignore" + - ".prettierignore" + - "uv.lock" + pull_request: + branches: [master] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + name: "Lint" + runs-on: ubuntu-latest + timeout-minutes: 5 + if: ${{ !contains(github.event.head_commit.message, '#nolint') }} + + permissions: + contents: read + + steps: + - name: "Checkout" + uses: actions/checkout@v6 + + - name: "Debug event.json" + continue-on-error: true + run: cat "${GITHUB_EVENT_PATH}" + - name: "Debug CTX github" + continue-on-error: true + env: + GITHUB_CTX: ${{ toJSON(github) }} + run: echo "$GITHUB_CTX" + + - name: "Install UV" + uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0 + + - name: "Install Python" + run: | + uv python install + + - name: "Install Project" + run: | + uv sync --locked --all-extras --dev + + - name: "Debug UV" + continue-on-error: true + run: | + uv python dir + uv python find + uv run python -V + echo "::group::uv tree" + uv tree + echo "::endgroup::" + echo "::group::uv pip list" + uv pip list + echo "::endgroup::" + + - name: "astral-sh/ruff" + if: ${{ !cancelled() }} + uses: astral-sh/ruff-action@57714a7c8a2e59f32539362ba31877a1957dded1 # v3.5.1 + with: + version: latest + + - name: "astral-sh/ty" + continue-on-error: true + if: ${{ !cancelled() }} + run: | + uv run ty check -v + + - name: "psf/black" + if: ${{ !cancelled() }} + uses: psf/black@stable + + - name: "isort" + if: ${{ !cancelled() }} + uses: isort/isort-action@24d8a7a51d33ca7f36c3f23598dafa33f7071326 # v1.1.1 + + - name: "mypy" + continue-on-error: true + if: ${{ !cancelled() }} + run: | + uvx toml-run mypy + + - name: "bandit" + if: ${{ !cancelled() }} + run: | + uvx toml-run bandit + + - name: "tombi" + if: ${{ !cancelled() }} + run: | + uv run tombi lint + + - name: "yamllint" + if: ${{ !cancelled() }} + run: | + uvx toml-run yamllint + + - name: "prettier" + if: ${{ !cancelled() }} + run: | + echo "::group::Install" + npm install prettier + echo "::endgroup::" + npx prettier --check . + + - name: "actionlint" + if: ${{ !cancelled() }} + uses: cssnr/actionlint-action@v1 + with: + shellcheck_opts: -e SC2012 + + - name: "Hadolint" + if: ${{ !cancelled() }} + uses: hadolint/hadolint-action@v3.1.0 + with: + dockerfile: Dockerfile + recursive: true + ignore: DL3013,DL3018 diff --git a/.github/yamllint.yaml b/.github/yamllint.yaml new file mode 100644 index 0000000..ed12050 --- /dev/null +++ b/.github/yamllint.yaml @@ -0,0 +1,9 @@ +extends: relaxed + +ignore: + - "node_modules/" + - "**/templates/" + +rules: + line-length: + max: 119 diff --git a/.gitignore b/.gitignore index 1765116..ae6a091 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,22 @@ +# Generic .idea/ *.iml -**/__pycache__/ -**/venv/ +.vscode/ +.venv/ +venv/ +__pycache__/ +*.egg-info/ +node_modules/ +build/ +dist/ +.*cache +*.log *.pyc .coverage -*.log -*.sqlite3 +coverage.xml +# App +.env +.secrets +.vars settings.env +*.sqlite3 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..96ddbbd --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +package-lock.json +**/templates +**/*.html +app/static diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..34dae84 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,19 @@ +{ + "trailingComma": "es5", + "semi": false, + "singleQuote": true, + "overrides": [ + { + "files": ["**/*.html", "**/*.yaml", "**/*.yml"], + "options": { + "singleQuote": false + } + }, + { + "files": ["**/*.js", "**/*.css", "**/*.scss"], + "options": { + "tabWidth": 4 + } + } + ] +} diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index 98b5d78..0000000 --- a/Jenkinsfile +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env groovy - -@Library('jenkins-libraries')_ - -pipeline { - agent { - label 'jenkins-slave-docker' - } - options { - buildDiscarder(logRotator(numToKeepStr:'5')) - timeout(time: 1, unit: 'HOURS') - } - environment { - DISCORD_ID = "discord-hook-smashed" - COMPOSE_FILE = "docker-compose-swarm.yaml" - REGISTRY_HOST = "registry.hosted-domains.com" - - BUILD_CAUSE = getBuildCause() - VERSION = getVersion("${GIT_BRANCH}") - - GIT_ORG = getGitGroup("${GIT_URL}") - GIT_REPO = getGitRepo("${GIT_URL}") - - BASE_NAME = "${GIT_ORG}-${GIT_REPO}" - SERVICE_NAME = "${BASE_NAME}" - CONFIG_NAME = "shane-red-api" - } - stages { - stage('Init') { - steps { - echo "\n--- Build Details ---\n" + - "JOB_NAME: ${JOB_NAME}\n" + - "GIT_URL: ${GIT_URL}\n" + - "BASE_NAME: ${BASE_NAME}\n" + - "SERVICE_NAME: ${SERVICE_NAME}\n" + - "CONFIG_NAME: ${CONFIG_NAME}\n" + - "REGISTRY_HOST: ${REGISTRY_HOST}\n" + - "BUILD_CAUSE: ${BUILD_CAUSE}\n" + - "GIT_BRANCH: ${GIT_BRANCH}\n" + - "VERSION: ${VERSION}\n" - verifyBuild() - sendDiscord("${DISCORD_ID}", "Pipeline Started by: ${BUILD_CAUSE}") - getConfigs("${SERVICE_NAME}") // use this to get service configs from deploy-configs - } - } - stage('Dev Deploy') { - when { - allOf { - not { branch 'master' } - } - } - environment { - ENV_NAME = "dev" - ENV_FILE = "service-configs/services/${CONFIG_NAME}/${ENV_NAME}.env" - STACK_NAME = "${ENV_NAME}_${SERVICE_NAME}" - TRAEFIK_HOST = "`red-api-dev.cssnr.com`" - } - steps { - echo "\n--- Starting ${ENV_NAME} Deploy ---\n" + - "STACK_NAME: ${STACK_NAME}\n" + - "TRAEFIK_HOST: ${TRAEFIK_HOST}\n" + - "ENV_FILE: ${ENV_FILE}\n" - sendDiscord("${DISCORD_ID}", "${ENV_NAME} Deploy Started") - updateCompose("${COMPOSE_FILE}", "STACK_NAME", "${STACK_NAME}") - stackPush("${COMPOSE_FILE}") - stackDeploy("${COMPOSE_FILE}", "${STACK_NAME}") - sendDiscord("${DISCORD_ID}", "${ENV_NAME} Deploy Finished") - } - } - stage('Prod Deploy') { - when { - allOf { - branch 'master' - triggeredBy 'UserIdCause' - } - } - environment { - ENV_NAME = "prod" - ENV_FILE = "service-configs/services/${CONFIG_NAME}/${ENV_NAME}.env" - STACK_NAME = "${ENV_NAME}_${SERVICE_NAME}" - TRAEFIK_HOST = "`red-api.cssnr.com`" - } - steps { - echo "\n--- Starting ${ENV_NAME} Deploy ---\n" + - "STACK_NAME: ${STACK_NAME}\n" + - "TRAEFIK_HOST: ${TRAEFIK_HOST}\n" + - "ENV_FILE: ${ENV_FILE}\n" - sendDiscord("${DISCORD_ID}", "${ENV_NAME} Deploy Started") - updateCompose("${COMPOSE_FILE}", "STACK_NAME", "${STACK_NAME}") - stackPush("${COMPOSE_FILE}") - stackDeploy("${COMPOSE_FILE}", "${STACK_NAME}") - sendDiscord("${DISCORD_ID}", "${ENV_NAME} Deploy Finished") - } - } - } - post { - always { - cleanWs() - script { if (!env.INVALID_BUILD) { - sendDiscord("${DISCORD_ID}", "Pipeline Complete: ${currentBuild.currentResult}") - } } - } - } -} diff --git a/README.md b/README.md index e00b496..16d8c83 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,10 @@ -[![Discord](https://img.shields.io/discord/111645911091814400?color=7289da&label=discord&logo=discord&logoColor=white&style=plastic)](https://discord.gg/ZrRbfdE6kz) -[![](https://repository-images.githubusercontent.com/660271832/edb15836-ca1c-43fd-883c-4943f2415121)](https://carl.cssnr.com/) # Carl Red Web API Web API for Carl Red Bot -Documentation Coming soon... +## Frameworks -### Frameworks - -* Django (4.x) https://www.djangoproject.com/ -* Celery (5.x) https://docs.celeryproject.org/ -* Bootstrap (5.x) http://getbootstrap.com/ -* Font Awesome (6.x) http://fontawesome.io/ -* JQuery (3.x) https://jquery.com/ +- Django (5.x) https://www.djangoproject.com/ +- Bootstrap (5.x) http://getbootstrap.com/ +- Font Awesome (6.x) http://fontawesome.io/ +- JQuery (3.x) https://jquery.com/ diff --git a/app/Dockerfile b/app/Dockerfile index b26dbe7..24638fd 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -1,27 +1,25 @@ -FROM python:3.11-alpine as base - -ENV TZ=UTC -ENV PYTHONDONTWRITEBYTECODE 1 - -COPY requirements.txt . -RUN python3 -m pip install --no-cache-dir --upgrade pip &&\ - python3 -m pip install --no-cache-dir -r requirements.txt +FROM ghcr.io/astral-sh/uv:python3.13-alpine - -FROM python:3.11-alpine +LABEL org.opencontainers.image.source="https://github.com/cssnr/red-api" +LABEL org.opencontainers.image.description="Carl Red Web API" +LABEL org.opencontainers.image.authors="smashedr" ENV TZ=UTC -ENV PYTHONUNBUFFERED 1 - -COPY --from=base /usr/local/lib/python3.11/site-packages/ /usr/local/lib/python3.11/site-packages/ -COPY --from=base /usr/local/bin/ /usr/local/bin/ +ENV PYTHONDONTWRITEBYTECODE=1 +ENV UV_PROJECT_ENVIRONMENT=/usr/local +ENV UV_LOCKED=1 +ENV UV_NO_DEV=1 +ENV UV_NO_CACHE=1 +ENV UV_NO_PROGRESS=1 RUN addgroup -g 1000 app &&\ adduser -S -H -h /app -s /sbin/nologin -u 1000 -G app app &&\ - mkdir -p /app /data && chown app:app /app /data &&\ - apk add --update --no-cache curl + mkdir -p /app /data /data/static && chown -R app:app /app /data /data/static &&\ + apk add --no-cache curl WORKDIR /app -COPY --chown=app:app . . +COPY --chown=app:app pyproject.toml uv.lock ./ +RUN uv sync +COPY --chown=app:app app ./ USER app ENTRYPOINT ["sh", "docker-entrypoint.sh"] diff --git a/app/docker-entrypoint.sh b/app/docker-entrypoint.sh index 56b36b6..cfd9bc4 100644 --- a/app/docker-entrypoint.sh +++ b/app/docker-entrypoint.sh @@ -1,15 +1,15 @@ #!/usr/bin/env sh -if echo "${*}" | grep -q "gun";then +set -ex + +if echo "${*}" | grep -q "gun\|runserver";then if [ -n "${DJANGO_SUPERUSER_PASSWORD}" ] && [ -n "${DJANGO_SUPERUSER_USERNAME}" ] && [ -n "${DJANGO_SUPERUSER_EMAIL}" ];then - python manage.py createsuperuser --noinput + uv run python manage.py createsuperuser --noinput || : fi - set -ex - python manage.py collectstatic --noinput + #uv run python manage.py migrate --noinput + uv run python manage.py collectstatic --noinput -v 0 fi -set -ex - exec "$@" diff --git a/app/home/admin.py b/app/home/admin.py index 811a50c..0c7179b 100644 --- a/app/home/admin.py +++ b/app/home/admin.py @@ -1,4 +1,5 @@ from django.contrib import admin + from .models import Contact admin.site.register(Contact) diff --git a/app/home/apps.py b/app/home/apps.py index 6c58000..f4afef0 100644 --- a/app/home/apps.py +++ b/app/home/apps.py @@ -2,5 +2,5 @@ class HomeConfig(AppConfig): - name = 'home' - verbose_name = 'Home' + name = "home" + verbose_name = "Home" diff --git a/app/home/models.py b/app/home/models.py index aca7a83..7893eb7 100644 --- a/app/home/models.py +++ b/app/home/models.py @@ -3,16 +3,16 @@ class Contact(models.Model): id = models.AutoField(primary_key=True) - name = models.CharField(max_length=128, verbose_name='Name') - message = models.TextField(verbose_name='Message') + name = models.CharField(max_length=128, verbose_name="Name") + message = models.TextField(verbose_name="Message") created_at = models.DateTimeField(auto_now_add=True) def __str__(self): - return '{} - {}'.format(self.name, self.message[:16]) + return "{} - {}".format(self.name, self.message[:16]) class Meta: - verbose_name = 'Contact' - verbose_name_plural = 'Contacts' + verbose_name = "Contact" + verbose_name_plural = "Contacts" # class Plots(models.Model): diff --git a/app/home/templatetags/home_tags.py b/app/home/templatetags/home_tags.py index fb7685b..ad97116 100644 --- a/app/home/templatetags/home_tags.py +++ b/app/home/templatetags/home_tags.py @@ -1,31 +1,32 @@ import logging +from typing import Optional + from django import template from django.conf import settings from django.templatetags.static import static -from typing import Optional + from oauth.models import CustomUser -logger = logging.getLogger('app') +logger = logging.getLogger("app") register = template.Library() -@register.simple_tag(name='get_config') +@register.simple_tag(name="get_config") def get_config(value: str) -> Optional[str]: # get django setting value or return none return getattr(settings, value, None) -@register.filter(name='absolute_url') +@register.filter(name="absolute_url") def absolute_url(absolute_uri: str) -> str: # returns the absolute_url from the absolute_uri - return '{0}//{2}'.format(*absolute_uri.split('/')) + return "{0}//{2}".format(*absolute_uri.split("/")) -@register.filter(name='avatar_url') +@register.filter(name="avatar_url") def avatar_url(user: CustomUser) -> str: # return discord avatar url from user model if user.avatar_hash: - return f'https://cdn.discordapp.com/avatars/' \ - f'{ user.username }/{ user.avatar_hash }.png' + return f"https://cdn.discordapp.com/avatars/{user.username}/{user.avatar_hash}.png" else: - return static('images/assets/default.png') + return static("images/assets/default.png") diff --git a/app/home/urls.py b/app/home/urls.py index 82b9f96..bfe5d20 100644 --- a/app/home/urls.py +++ b/app/home/urls.py @@ -1,14 +1,15 @@ from django.urls import path + from . import views -app_name = 'home' +app_name = "home" urlpatterns = [ - path('', views.home_view, name='index'), - path('contact/', views.contact_view, name='contact'), - path('flightaware/', views.fa_view, name='flightaware'), - path('youtube/', views.yt_view, name='youtube'), - path('verify/', views.verify_view, name='verify'), - path('plotly/', views.plotly_view, name='plotly'), - path('plotly//', views.plotly_view, name='plotly_pk'), + path("", views.home_view, name="index"), + path("contact/", views.contact_view, name="contact"), + path("flightaware/", views.fa_view, name="flightaware"), + path("youtube/", views.yt_view, name="youtube"), + path("verify/", views.verify_view, name="verify"), + path("plotly/", views.plotly_view, name="plotly"), + path("plotly//", views.plotly_view, name="plotly_pk"), ] diff --git a/app/home/views.py b/app/home/views.py index cfee65c..488757b 100644 --- a/app/home/views.py +++ b/app/home/views.py @@ -1,10 +1,12 @@ -import httpx import json import logging import time +from datetime import datetime +from typing import Optional, Union + +import httpx import redis import xmltodict -from datetime import datetime from django.conf import settings from django.core.cache import cache from django.http import HttpRequest, HttpResponse, JsonResponse @@ -13,24 +15,24 @@ from django.utils.crypto import get_random_string from django.views.decorators.csrf import csrf_exempt from django_redis import get_redis_connection -from typing import Optional, Union + from .forms import ContactForm from .models import Contact -logger = logging.getLogger('app') +logger = logging.getLogger("app") def home_view(request): # View: / - logger.debug('home_view: is_secure: %s', request.is_secure()) - return render(request, 'home.html') + logger.debug("home_view: is_secure: %s", request.is_secure()) + return render(request, "home.html") def contact_view(request): # View: /contact/ - logger.debug('contact_view: is_secure: %s', request.is_secure()) - if not request.method == 'POST': - return render(request, 'contact.html') + logger.debug("contact_view: is_secure: %s", request.is_secure()) + if request.method != "POST": + return render(request, "contact.html") try: logger.debug(request.POST) @@ -40,12 +42,12 @@ def contact_view(request): return JsonResponse(form.errors, status=400) if not request.user.is_authenticated and not google_verify(request): - data = {'error_message': 'Google CAPTCHA not verified.'} + data = {"error_message": "Google CAPTCHA not verified."} return JsonResponse(data, status=400) contact = Contact.objects.create( - name=form.cleaned_data['name'], - message=form.cleaned_data['message'], + name=form.cleaned_data["name"], + message=form.cleaned_data["message"], ) r = send_discord_message(contact.pk) if not r.is_success: @@ -55,7 +57,7 @@ def contact_view(request): except Exception as error: logger.exception(error) - return JsonResponse({'error_message': str(error)}, status=400) + return JsonResponse({"error_message": str(error)}, status=400) def fa_view(request): @@ -107,9 +109,9 @@ def fa_view(request): @csrf_exempt def plotly_view(request, pk: Optional[str] = None): # View: /plotly/ - logger.debug('%s - plotly_view - is_secure: %s', request.method, request.is_secure()) + logger.debug("%s - plotly_view - is_secure: %s", request.method, request.is_secure()) try: - if request.method == 'GET': + if request.method == "GET": logger.debug(request.GET) logger.debug(pk) data = cache.get(pk) @@ -118,14 +120,14 @@ def plotly_view(request, pk: Optional[str] = None): return HttpResponse(status=404) return HttpResponse(data) - if request.method == 'POST': + if request.method == "POST": logger.debug(request.POST) body = request.body.decode() - logger.debug('-'*20) + logger.debug("-" * 20) logger.debug(body) - logger.debug('-'*20) + logger.debug("-" * 20) key = str(datetime.now().timestamp()) - cache.set(key, body, 60*60*24*7) + cache.set(key, body, 60 * 60 * 24 * 7) return HttpResponse(key) except Exception as error: @@ -136,92 +138,92 @@ def plotly_view(request, pk: Optional[str] = None): @csrf_exempt def yt_view(request): # View: /youtube/ - logger.debug('%s - yt_view - is_secure: %s', request.method, request.is_secure()) + logger.debug("%s - yt_view - is_secure: %s", request.method, request.is_secure()) try: - if request.method == 'GET': + if request.method == "GET": logger.debug(request.GET) - challenge = request.GET.get('hub.challenge', None) + challenge = request.GET.get("hub.challenge", None) logger.debug(challenge) if challenge: - logger.debug('return 200: %s', challenge) + logger.debug("return 200: %s", challenge) return HttpResponse(challenge, status=200) - logger.debug('return 400') + logger.debug("return 400") return HttpResponse(status=400) - if request.method == 'POST': + if request.method == "POST": logger.debug(request.POST) - body = request.body.decode('utf-8') - logger.debug('-'*20) + body = request.body.decode("utf-8") + logger.debug("-" * 20) logger.debug(body) - logger.debug('-'*20) + logger.debug("-" * 20) data = xmltodict.parse(body) logger.debug(data) - r = bot_request('red.youtube', ['new'], 0, 0, data) + r = bot_request("red.youtube", ["new"], 0, 0, data) logger.debug(r) - logger.debug('return 204') + logger.debug("return 204") return HttpResponse(status=204) except Exception as error: logger.exception(error) - logger.debug('return 400') + logger.debug("return 400") return HttpResponse(status=400) -def verify_view(request): +def verify_view(request): # NOSONAR # View: /verify/ - logger.debug('verify_view: is_secure: %s', request.is_secure()) - if request.method in ['GET', 'HEAD']: + logger.debug("verify_view: is_secure: %s", request.is_secure()) + if request.method in ["GET", "HEAD"]: logger.debug(request.GET) - guild_id = request.GET.get('guild') - logger.debug('guild_id: %s', guild_id) - user_id = request.GET.get('user') - logger.debug('user_id: %s', user_id) + guild_id = request.GET.get("guild") + logger.debug("guild_id: %s", guild_id) + user_id = request.GET.get("user") + logger.debug("user_id: %s", user_id) # context = {'verified': False} if not guild_id or not user_id: - context = {'error': 'Invalid Request. No guild or user in query.'} - return render(request, 'verify.html', context) + context = {"error": "Invalid Request. No guild or user in query."} + return render(request, "verify.html", context) - br = cache.get(f'br:{user_id}') + br = cache.get(f"br:{user_id}") if not br: - br = bot_request('red.captcha', ['data'], guild_id, user_id) + br = bot_request("red.captcha", ["data"], guild_id, user_id) if br: - cache.set(f'br:{user_id}', br, 15) + cache.set(f"br:{user_id}", br, 15) else: - context = {'error': 'Error fetching data from Discord.'} - return render(request, 'verify.html', context) + context = {"error": "Error fetching data from Discord."} + return render(request, "verify.html", context) - logger.debug('br: %s', br) - context = {'guild': br['guild'], 'member': br['member']} - if 'verified' in br and br['verified']: - context['verified'] = True - logger.debug('context: %s', context) - return render(request, 'verify.html', context) + logger.debug("br: %s", br) + context = {"guild": br["guild"], "member": br["member"]} + if "verified" in br and br["verified"]: + context["verified"] = True + logger.debug("context: %s", context) + return render(request, "verify.html", context) try: logger.debug(request.POST) if not google_verify(request): - context = {'error_message': 'Google CAPTCHA not verified.'} + context = {"error_message": "Google CAPTCHA not verified."} return JsonResponse(context, status=400) - guild_id = request.POST['guild'] - logger.debug('guild: %s', guild_id) - user_id = request.POST['user'] - logger.debug('user: %s', user_id) - data = {'guild': int(guild_id), 'user': int(user_id)} - br = bot_request('red.captcha', ['verify'], guild_id, user_id, data) - logger.debug('br: %s', br) + guild_id = request.POST["guild"] + logger.debug("guild: %s", guild_id) + user_id = request.POST["user"] + logger.debug("user: %s", user_id) + data = {"guild": int(guild_id), "user": int(user_id)} + br = bot_request("red.captcha", ["verify"], guild_id, user_id, data) + logger.debug("br: %s", br) if not br: - context = {'error_message': 'Error fetching data from Discord.'} + context = {"error_message": "Error fetching data from Discord."} return JsonResponse(context, status=400) - if 'success' not in br or not br['success']: - if 'message' in br: - message = br['message'] + if "success" not in br or not br["success"]: + if "message" in br: + message = br["message"] else: - message = 'Error completing verification with Discord Bot.' - return JsonResponse({'error_message': message}, status=400) + message = "Error completing verification with Discord Bot." + return JsonResponse({"error_message": message}, status=400) - cache.delete(f'br:{user_id}') + cache.delete(f"br:{user_id}") # br = cache.get(f'br:{guild_id}') # if br: # br.delete() @@ -229,49 +231,52 @@ def verify_view(request): except Exception as error: logger.exception(error) - return JsonResponse({'error_message': str(error)}, status=400) + return JsonResponse({"error_message": str(error)}, status=400) -def bot_request(channel: Union[str, int], requests: list[str], - guild_id: Union[str, int], user_id: Union[str, int], - data: Optional[dict] = None) -> Optional[dict]: +def bot_request( + channel: Union[str, int], + requests: list[str], + guild_id: Union[str, int], + user_id: Union[str, int], + data: Optional[dict] = None, +) -> Optional[dict]: try: # Send Data - logger.debug('channel: %s', channel) + logger.debug("channel: %s", channel) return_channel = get_random_string(length=16) - logger.debug('return_channel: %s', return_channel) - r = get_redis_connection('default') + logger.debug("return_channel: %s", return_channel) + r = get_redis_connection("default") p = r.pubsub(ignore_subscribe_messages=True) p.subscribe(return_channel) default = { - 'channel': return_channel, - 'guild': int(guild_id), - 'user': int(user_id), - 'requests': requests, + "channel": return_channel, + "guild": int(guild_id), + "user": int(user_id), + "requests": requests, } if data: - logger.debug('data: %s', data) + logger.debug("data: %s", data) default.update(data) - logger.debug('default: %s', default) + logger.debug("default: %s", default) pub_ret = r.publish(channel, json.dumps(default)) - logger.debug('pub_ret: %s', pub_ret) + logger.debug("pub_ret: %s", pub_ret) if pub_ret == 0: - logger.warning('BOT NOT Listening on pubsub channel: %s', channel) + logger.warning("BOT NOT Listening on pubsub channel: %s", channel) return None # Get Data message = get_pubsub_message(p) - return json.loads(message['data'].decode('utf-8')) + return json.loads(message["data"].decode("utf-8")) except Exception as error: - logger.warning('Error Getting BOT Data') + logger.warning("Error Getting BOT Data") logger.exception(error) return None -def get_pubsub_message(pubsub: redis.client.PubSub, - timeout: int = 6) -> Optional[dict]: - logger.debug('get_pubsub_message') +def get_pubsub_message(pubsub: redis.client.PubSub, timeout: int = 6) -> Optional[dict]: + logger.debug("get_pubsub_message") message = None now = time.time() t = now + timeout @@ -285,20 +290,16 @@ def get_pubsub_message(pubsub: redis.client.PubSub, def google_verify(request: HttpRequest) -> bool: - logger.debug('google_verify') - if 'g_verified' in request.session and request.session['g_verified']: + logger.debug("google_verify") + if "g_verified" in request.session and request.session["g_verified"]: return True try: - url = 'https://www.google.com/recaptcha/api/siteverify' - data = { - 'secret': settings.GOOGLE_SITE_SECRET, - 'response': request.POST['g-recaptcha-response'] - } + url = "https://www.google.com/recaptcha/api/siteverify" + data = {"secret": settings.GOOGLE_SITE_SECRET, "response": request.POST["g-recaptcha-response"]} r = httpx.post(url, data=data, timeout=10) - if r.is_success: - if r.json()['success']: - request.session['g_verified'] = True - return True + if r.is_success and r.json()["success"]: + request.session["g_verified"] = True + return True return False except Exception as error: logger.exception(error) @@ -306,30 +307,29 @@ def google_verify(request: HttpRequest) -> bool: def send_discord_message(message: str) -> httpx.Response: - logger.debug('send_discord_message') - context = {'message': message} - discord_message = render_to_string('message/discord-message.html', context) - logger.debug('discord_message: %s', discord_message) - data = {'content': discord_message} + logger.debug("send_discord_message") + context = {"message": message} + discord_message = render_to_string("message/discord-message.html", context) + logger.debug("discord_message: %s", discord_message) + data = {"content": discord_message} r = httpx.post(settings.DISCORD_WEBHOOK, json=data, timeout=5) - logger.debug('r.status_code: %s', r.status_code) + logger.debug("r.status_code: %s", r.status_code) return r -def yt_sub(callback='https://intranet.cssnr.com/youtube/', - channel_id='UCkHizGOU0va29RVjrfdz_Aw') -> int: +def yt_sub(callback="https://intranet.cssnr.com/youtube/", channel_id="UCkHizGOU0va29RVjrfdz_Aw") -> int: data = { - 'hub.callback': callback, - 'hub.topic': f'https://www.youtube.com/feeds/videos.xml?channel_id={channel_id}', - 'hub.verify': 'async', - 'hub.mode': 'subscribe', - 'hub.verify_token': '', - 'hub.secret': '', - 'hub.lease_numbers': '', + "hub.callback": callback, + "hub.topic": f"https://www.youtube.com/feeds/videos.xml?channel_id={channel_id}", + "hub.verify": "async", + "hub.mode": "subscribe", + "hub.verify_token": "", # nosec + "hub.secret": "", + "hub.lease_numbers": "", } - url = 'https://pubsubhubbub.appspot.com/subscribe' + url = "https://pubsubhubbub.appspot.com/subscribe" r = httpx.post(url, data=data, timeout=10) - logger.debug('r.status_code: %s', r.status_code) + logger.debug("r.status_code: %s", r.status_code) if not r.is_success: r.raise_for_status() return r.status_code diff --git a/app/manage.py b/app/manage.py index a53204a..68fd47f 100644 --- a/app/manage.py +++ b/app/manage.py @@ -1,12 +1,13 @@ #!/usr/bin/env python """Django's command-line utility for administrative tasks.""" + import os import sys def main(): """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myapp.settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myapp.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -18,5 +19,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/app/myapp/settings.py b/app/myapp/settings.py index 2195f5a..2bb1167 100644 --- a/app/myapp/settings.py +++ b/app/myapp/settings.py @@ -1,144 +1,139 @@ -from decouple import config, Csv -from django.contrib.messages import constants as message_constants from pathlib import Path +from decouple import Csv, config +from django.contrib.messages import constants as message_constants + BASE_DIR = Path(__file__).resolve().parent.parent -SECRET_KEY = config('SECRET_KEY') -DEBUG = config('DEBUG', 'False', bool) -ALLOWED_HOSTS = config('ALLOWED_HOSTS', '*', Csv()) -SESSION_COOKIE_AGE = config('SESSION_COOKIE_AGE', 3600 * 24 * 14, int) +SECRET_KEY = config("SECRET_KEY") +DEBUG = config("DEBUG", "False", bool) +ALLOWED_HOSTS = config("ALLOWED_HOSTS", "*", Csv()) +SESSION_COOKIE_AGE = config("SESSION_COOKIE_AGE", 3600 * 24 * 14, int) -WSGI_APPLICATION = 'myapp.wsgi.application' -ROOT_URLCONF = 'myapp.urls' -AUTH_USER_MODEL = 'oauth.CustomUser' +WSGI_APPLICATION = "myapp.wsgi.application" +ROOT_URLCONF = "myapp.urls" +AUTH_USER_MODEL = "oauth.CustomUser" -LOGIN_REDIRECT_URL = '/' -LOGIN_URL = '/oauth/' -STATIC_URL = '/static/' -MEDIA_URL = '/media/' -STATIC_ROOT = config('STATIC_ROOT') -MEDIA_ROOT = config('MEDIA_ROOT') -STATICFILES_DIRS = [BASE_DIR / 'static'] -TEMPLATES_DIRS = [BASE_DIR / 'templates'] +LOGIN_REDIRECT_URL = "/" +LOGIN_URL = "/oauth/" +STATIC_URL = "/static/" +MEDIA_URL = "/media/" +STATIC_ROOT = config("STATIC_ROOT") +MEDIA_ROOT = config("MEDIA_ROOT") +STATICFILES_DIRS = [BASE_DIR / "static"] +TEMPLATES_DIRS = [BASE_DIR / "templates"] -LANGUAGE_CODE = config('LANGUAGE_CODE', 'en-us') -USE_TZ = config('USE_TZ', 'True', bool) -TIME_ZONE = config('TZ', 'UTC') +LANGUAGE_CODE = config("LANGUAGE_CODE", "en-us") +USE_TZ = config("USE_TZ", "True", bool) +TIME_ZONE = config("TZ", "UTC") USE_I18N = True USE_L10N = True -GOOGLE_SITE_PUBLIC = config('GOOGLE_SITE_PUBLIC') -GOOGLE_SITE_SECRET = config('GOOGLE_SITE_SECRET') +GOOGLE_SITE_PUBLIC = config("GOOGLE_SITE_PUBLIC") +GOOGLE_SITE_SECRET = config("GOOGLE_SITE_SECRET") -DJANGO_REDIS_IGNORE_EXCEPTIONS = config('REDIS_IGNORE_EXCEPTIONS', True, bool) -USE_X_FORWARDED_HOST = config('USE_X_FORWARDED_HOST', 'False', bool) -SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' -SESSION_ENGINE = 'django.contrib.sessions.backends.cache' -X_FRAME_OPTIONS = 'SAMEORIGIN' +DJANGO_REDIS_IGNORE_EXCEPTIONS = config("REDIS_IGNORE_EXCEPTIONS", True, bool) +USE_X_FORWARDED_HOST = config("USE_X_FORWARDED_HOST", "False", bool) +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" +SESSION_ENGINE = "django.contrib.sessions.backends.cache" +X_FRAME_OPTIONS = "SAMEORIGIN" -DISCORD_INVITE = config('DISCORD_INVITE') -DISCORD_WEBHOOK = config('DISCORD_WEBHOOK') +DISCORD_INVITE = config("DISCORD_INVITE") +DISCORD_WEBHOOK = config("DISCORD_WEBHOOK") MESSAGE_TAGS = { - message_constants.DEBUG: 'secondary', - message_constants.INFO: 'info', - message_constants.SUCCESS: 'success', - message_constants.WARNING: 'warning', - message_constants.ERROR: 'danger', + message_constants.DEBUG: "secondary", + message_constants.INFO: "info", + message_constants.SUCCESS: "success", + message_constants.WARNING: "warning", + message_constants.ERROR: "danger", } CACHES = { - 'default': { - 'BACKEND': config('CACHE_BACKEND', - 'django.core.cache.backends.dummy.DummyCache'), - 'LOCATION': config('CACHE_LOCATION'), - 'OPTIONS': { - 'CLIENT_CLASS': 'django_redis.client.DefaultClient', + "default": { + "BACKEND": config("CACHE_BACKEND", "django.core.cache.backends.dummy.DummyCache"), + "LOCATION": config("CACHE_LOCATION"), + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", }, }, } DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': config('DATABASE_NAME'), - 'USER': config('DATABASE_USER'), - 'PASSWORD': config('DATABASE_PASS'), - 'HOST': config('DATABASE_HOST'), - 'PORT': config('DATABASE_PORT'), - 'OPTIONS': { - }, + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": config("DATABASE_NAME"), + "USER": config("DATABASE_USER"), + "PASSWORD": config("DATABASE_PASS"), + "HOST": config("DATABASE_HOST"), + "PORT": config("DATABASE_PORT"), + "OPTIONS": {}, }, } INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'home', - 'oauth', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "home", + "oauth", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': TEMPLATES_DIRS, - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.template.context_processors.media', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - 'django.template.context_processors.static', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": TEMPLATES_DIRS, + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.template.context_processors.media", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + "django.template.context_processors.static", ], }, }, ] LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'standard': { - 'format': ('%(asctime)s - ' - '%(levelname)s - ' - '%(filename)s ' - '%(module)s.%(funcName)s:%(lineno)d - ' - '%(message)s'), + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "standard": { + "format": ("%(asctime)s - %(levelname)s - %(filename)s %(module)s.%(funcName)s:%(lineno)d - %(message)s"), }, }, - 'handlers': { - 'console': { - 'class': 'logging.StreamHandler', - 'formatter': 'standard', + "handlers": { + "console": { + "class": "logging.StreamHandler", + "formatter": "standard", }, }, - 'loggers': { - 'django': { - 'handlers': ['console'], - 'level': config('DJANGO_LOG_LEVEL', 'INFO'), - 'propagate': True, + "loggers": { + "django": { + "handlers": ["console"], + "level": config("DJANGO_LOG_LEVEL", "INFO"), + "propagate": True, }, - 'app': { - 'handlers': ['console'], - 'level': config('APP_LOG_LEVEL', 'DEBUG'), - 'propagate': True, + "app": { + "handlers": ["console"], + "level": config("APP_LOG_LEVEL", "DEBUG"), + "propagate": True, }, }, } diff --git a/app/myapp/urls.py b/app/myapp/urls.py index f66ecc0..f4f65ce 100644 --- a/app/myapp/urls.py +++ b/app/myapp/urls.py @@ -1,22 +1,22 @@ from django.conf import settings from django.conf.urls.static import static -from django.conf.urls import include from django.contrib import admin -from django.urls import path +from django.urls import include, path from django.views.generic.base import RedirectView + from . import views urlpatterns = [ - path('', include('home.urls')), - path('oauth/', include('oauth.urls')), - path('admin/', admin.site.urls), - path('redis/', RedirectView.as_view(url='/redis/'), name='redis'), - path('pgadmin/', RedirectView.as_view(url='/pgadmin/'), name='pgadmin'), - path('app-health-check/', views.health_check, name='health_check'), + path("", include("home.urls")), + path("oauth/", include("oauth.urls")), + path("admin/", admin.site.urls), + path("redis/", RedirectView.as_view(url="/redis/"), name="redis"), + path("pgadmin/", RedirectView.as_view(url="/pgadmin/"), name="pgadmin"), + path("app-health-check/", views.health_check, name="health_check"), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) -handler400 = 'myapp.views.handler400_view' -handler403 = 'myapp.views.handler403_view' -handler404 = 'myapp.views.handler404_view' -handler500 = 'myapp.views.handler500_view' +handler400 = "myapp.views.handler400_view" +handler403 = "myapp.views.handler403_view" +handler404 = "myapp.views.handler404_view" +handler500 = "myapp.views.handler500_view" diff --git a/app/myapp/views.py b/app/myapp/views.py index c4bfe88..575434b 100644 --- a/app/myapp/views.py +++ b/app/myapp/views.py @@ -1,33 +1,34 @@ import logging + from django.http import HttpResponse from django.shortcuts import render -logger = logging.getLogger('app') +logger = logging.getLogger("app") def health_check(request): - return HttpResponse('success', status=200) + return HttpResponse("success", status=200) def handler400_view(request, exception): - logger.debug('handler400_view') + logger.debug("handler400_view") logger.debug(exception) - return render(request, 'error/400.html', status=400) + return render(request, "error/400.html", status=400) def handler403_view(request, exception): - logger.debug('handler403_view') + logger.debug("handler403_view") logger.debug(exception) - return render(request, 'error/403.html', status=403) + return render(request, "error/403.html", status=403) def handler404_view(request, exception): - logger.debug('handler404_view') + logger.debug("handler404_view") logger.debug(exception) - return render(request, 'error/404.html', status=404) + return render(request, "error/404.html", status=404) def handler500_view(request): - logger.debug('handler500_view') + logger.debug("handler500_view") # logger.debug(exception) - return render(request, 'error/500.html', status=500) + return render(request, "error/500.html", status=500) diff --git a/app/myapp/wsgi.py b/app/myapp/wsgi.py index 298171c..145ef6d 100644 --- a/app/myapp/wsgi.py +++ b/app/myapp/wsgi.py @@ -2,6 +2,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myapp.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myapp.settings") application = get_wsgi_application() diff --git a/app/oauth/admin.py b/app/oauth/admin.py index c32ef2a..a1a780c 100644 --- a/app/oauth/admin.py +++ b/app/oauth/admin.py @@ -1,15 +1,24 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin + from .models import CustomUser @admin.register(CustomUser) class CustomUserAdmin(UserAdmin): - list_display = ('username', 'first_name', 'last_name', 'is_staff', 'is_superuser',) - list_filter = ('is_superuser',) - search_fields = ('username',) - ordering = ('first_name',) - readonly_fields = ('username', 'first_name', 'last_name',) - fieldsets = UserAdmin.fieldsets + ( - ('OAuth', {'fields': ('avatar_hash',)}), + list_display = ( + "username", + "first_name", + "last_name", + "is_staff", + "is_superuser", + ) + list_filter = ("is_superuser",) + search_fields = ("username",) + ordering = ("first_name",) + readonly_fields = ( + "username", + "first_name", + "last_name", ) + fieldsets = UserAdmin.fieldsets + (("OAuth", {"fields": ("avatar_hash",)}),) diff --git a/app/oauth/forms.py b/app/oauth/forms.py index 1c84c68..223afc5 100644 --- a/app/oauth/forms.py +++ b/app/oauth/forms.py @@ -1,14 +1,15 @@ -from django.contrib.auth.forms import UserCreationForm, UserChangeForm +from django.contrib.auth.forms import UserChangeForm, UserCreationForm + from .models import CustomUser class CustomUserCreationForm(UserCreationForm): class Meta(UserCreationForm): model = CustomUser - fields = ('username',) + fields = ("username",) class CustomUserChangeForm(UserChangeForm): class Meta: model = CustomUser - fields = ('username',) + fields = ("username",) diff --git a/app/oauth/models.py b/app/oauth/models.py index 0b90dd8..2ac201b 100644 --- a/app/oauth/models.py +++ b/app/oauth/models.py @@ -13,5 +13,5 @@ def __str__(self): return self.username class Meta: - verbose_name = 'Custom User' - verbose_name_plural = 'Custom Users' + verbose_name = "Custom User" + verbose_name_plural = "Custom Users" diff --git a/app/oauth/urls.py b/app/oauth/urls.py index d2f4ed6..d2218ea 100644 --- a/app/oauth/urls.py +++ b/app/oauth/urls.py @@ -1,10 +1,11 @@ from django.urls import path + from . import views -app_name = 'oauth' +app_name = "oauth" urlpatterns = [ - path('', views.oauth_start, name='login'), - path('logout/', views.oauth_logout, name='logout'), - path('callback/', views.oauth_callback, name='callback'), + path("", views.oauth_start, name="login"), + path("logout/", views.oauth_logout, name="logout"), + path("callback/", views.oauth_callback, name="callback"), ] diff --git a/app/oauth/views.py b/app/oauth/views.py index 43afd83..9141666 100644 --- a/app/oauth/views.py +++ b/app/oauth/views.py @@ -1,33 +1,35 @@ -import httpx import logging import urllib.parse from datetime import datetime, timedelta -from decouple import config, Csv + +import httpx +from decouple import Csv, config from django.contrib import messages from django.contrib.auth import login, logout from django.http import HttpRequest from django.shortcuts import HttpResponseRedirect, redirect from django.urls import reverse from django.views.decorators.http import require_http_methods + from .models import CustomUser -logger = logging.getLogger('app') +logger = logging.getLogger("app") def oauth_start(request): """ View /oauth/ """ - request.session['login_redirect_url'] = get_next_url(request) + request.session["login_redirect_url"] = get_next_url(request) params = { - 'redirect_uri': config('OAUTH_REDIRECT_URL'), - 'client_id': config('OAUTH_CLIENT_ID'), - 'response_type': config('OAUTH_RESPONSE_TYPE', 'code'), - 'scope': config('OAUTH_SCOPE', 'identify'), - 'prompt': config('OAUTH_PROMPT', 'none'), + "redirect_uri": config("OAUTH_REDIRECT_URL"), + "client_id": config("OAUTH_CLIENT_ID"), + "response_type": config("OAUTH_RESPONSE_TYPE", "code"), + "scope": config("OAUTH_SCOPE", "identify"), + "prompt": config("OAUTH_PROMPT", "none"), } url_params = urllib.parse.urlencode(params) - url = f'https://discord.com/api/oauth2/authorize?{url_params}' + url = f"https://discord.com/api/oauth2/authorize?{url_params}" return HttpResponseRedirect(url) @@ -35,29 +37,29 @@ def oauth_callback(request): """ View /oauth/callback/ """ - if 'code' not in request.GET: - messages.warning(request, 'User aborted or no code in request.') + if "code" not in request.GET: + messages.warning(request, "User aborted or no code in request.") return HttpResponseRedirect(get_login_redirect_url(request)) try: - logger.debug('code: %s', request.GET['code']) - token_data = get_access_token(request.GET['code']) - logger.debug('token_response: %s', token_data) + logger.debug("code: %s", request.GET["code"]) + token_data = get_access_token(request.GET["code"]) + logger.debug("token_response: %s", token_data) profile = get_user_profile(token_data) - logger.debug('profile: %s', profile) - user, _ = CustomUser.objects.get_or_create(username=profile['id']) + logger.debug("profile: %s", profile) + user, _ = CustomUser.objects.get_or_create(username=profile["id"]) update_profile(user, profile) login(request, user) - messages.info(request, f'Successfully logged in as {user.first_name}.') + messages.info(request, f"Successfully logged in as {user.first_name}.") except Exception as error: logger.exception(error) - messages.error(request, f'Exception during login: {error}') + messages.error(request, f"Exception during login: {error}") return HttpResponseRedirect(get_login_redirect_url(request)) -@require_http_methods(['POST']) +@require_http_methods(["POST"]) def oauth_logout(request): """ View /oauth/logout/ @@ -65,15 +67,15 @@ def oauth_logout(request): next_url = get_next_url(request) # Hack to prevent login loop when logging out on a secure page - if len(next_url.split('/')) > 1: - logger.debug('next_url: %s', next_url.split('/')[1]) - secure_views_list = ['profile'] - if next_url.split('/')[1] in secure_views_list: - next_url = '/' + if len(next_url.split("/")) > 1: + logger.debug("next_url: %s", next_url.split("/")[1]) + secure_views_list = ["profile"] + if next_url.split("/")[1] in secure_views_list: + next_url = "/" - request.session['login_next_url'] = next_url + request.session["login_next_url"] = next_url logout(request) - messages.info(request, 'Successfully logged out.') + messages.info(request, "Successfully logged out.") return redirect(next_url) @@ -81,19 +83,19 @@ def get_access_token(code: str) -> dict: """ Post OAuth code and Return access_token """ - url = 'https://discord.com/api/v8/oauth2/token' + url = "https://discord.com/api/v8/oauth2/token" data = { - 'redirect_uri': config('OAUTH_REDIRECT_URL'), - 'client_id': config('OAUTH_CLIENT_ID'), - 'client_secret': config('OAUTH_CLIENT_SECRET'), - 'grant_type': config('OAUTH_GRANT_TYPE', 'authorization_code'), - 'code': code, + "redirect_uri": config("OAUTH_REDIRECT_URL"), + "client_id": config("OAUTH_CLIENT_ID"), + "client_secret": config("OAUTH_CLIENT_SECRET"), + "grant_type": config("OAUTH_GRANT_TYPE", "authorization_code"), + "code": code, } - headers = {'Content-Type': 'application/x-www-form-urlencoded'} + headers = {"Content-Type": "application/x-www-form-urlencoded"} r = httpx.post(url, data=data, headers=headers, timeout=10) if not r.is_success: - logger.info('status_code: %s', r.status_code) - logger.error('content: %s', r.content) + logger.info("status_code: %s", r.status_code) + logger.error("content: %s", r.content) r.raise_for_status() return r.json() @@ -102,24 +104,24 @@ def get_user_profile(token_data: dict) -> dict: """ Get Profile for Authenticated User """ - url = 'https://discord.com/api/v8/users/@me' - headers = {'Authorization': f"Bearer {token_data['access_token']}"} + url = "https://discord.com/api/v8/users/@me" + headers = {"Authorization": f"Bearer {token_data['access_token']}"} r = httpx.get(url, headers=headers, timeout=10) if not r.is_success: - logger.info('status_code: %s', r.status_code) - logger.error('content: %s', r.content) + logger.info("status_code: %s", r.status_code) + logger.error("content: %s", r.content) r.raise_for_status() - logger.debug('r.json(): %s', r.json()) + logger.debug("r.json(): %s", r.json()) p = r.json() # profile - Custom user data from oauth provider return { - 'id': p['id'], - 'username': p['username'], - 'discriminator': p['discriminator'], - 'avatar': p['avatar'], - 'access_token': token_data['access_token'], - 'refresh_token': token_data['refresh_token'], - 'expires_in': datetime.now() + timedelta(0, token_data['expires_in']), + "id": p["id"], + "username": p["username"], + "discriminator": p["discriminator"], + "avatar": p["avatar"], + "access_token": token_data["access_token"], + "refresh_token": token_data["refresh_token"], + "expires_in": datetime.now() + timedelta(0, token_data["expires_in"]), } @@ -127,14 +129,14 @@ def update_profile(user: CustomUser, profile: dict) -> None: """ Update Django user profile with provided data """ - user.first_name = profile['username'] - user.last_name = profile['discriminator'] - user.avatar_hash = profile['avatar'] - user.access_token = profile['access_token'] - user.refresh_token = profile['refresh_token'] - user.expires_in = profile['expires_in'] - if profile['id'] in config('SUPER_USERS', '', Csv()): - logger.info('Super user login: %s', profile['id']) + user.first_name = profile["username"] + user.last_name = profile["discriminator"] + user.avatar_hash = profile["avatar"] + user.access_token = profile["access_token"] + user.refresh_token = profile["refresh_token"] + user.expires_in = profile["expires_in"] + if profile["id"] in config("SUPER_USERS", "", Csv()): + logger.info("Super user login: %s", profile["id"]) user.is_staff, user.is_admin, user.is_superuser = True, True, True user.save() @@ -143,25 +145,25 @@ def get_next_url(request: HttpRequest) -> str: """ Determine 'next' parameter """ - if 'next' in request.GET: - return str(request.GET['next']) - if 'next' in request.POST: - return str(request.POST['next']) - if 'next_url' in request.session: - url = request.session['next_url'] - del request.session['next_url'] + if "next" in request.GET: + return str(request.GET["next"]) + if "next" in request.POST: + return str(request.POST["next"]) + if "next_url" in request.session: + url = request.session["next_url"] + del request.session["next_url"] request.session.modified = True return url - return reverse('home:index') + return reverse("home:index") def get_login_redirect_url(request: HttpRequest) -> str: """ Determine 'login_redirect_url' parameter """ - if 'login_redirect_url' in request.session: - url = request.session['login_redirect_url'] - del request.session['login_redirect_url'] + if "login_redirect_url" in request.session: + url = request.session["login_redirect_url"] + del request.session["login_redirect_url"] request.session.modified = True return url - return reverse('home:index') + return reverse("home:index") diff --git a/app/requirements.txt b/app/requirements.txt deleted file mode 100644 index 1812093..0000000 --- a/app/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -django>=4,<5 -django-redis -gunicorn -httpx -# kaleido -# pandas -# plotly -psycopg[binary] -python-decouple -xmltodict diff --git a/app/setup.cfg b/app/setup.cfg deleted file mode 100644 index 8fbaec7..0000000 --- a/app/setup.cfg +++ /dev/null @@ -1,6 +0,0 @@ -[flake8] -exclude = venv,*migrations* -max-line-length = 119 - -[coverage:run] -omit = *venv*,*migrations* diff --git a/docker-compose-stack.yaml b/docker-compose-stack.yaml new file mode 100644 index 0000000..a23a28c --- /dev/null +++ b/docker-compose-stack.yaml @@ -0,0 +1,68 @@ +version: "3.8" + +services: + nginx: + image: ghcr.io/cssnr/red-api-nginx:${VERSION:-latest} + env_file: settings.env + deploy: + mode: global + resources: + limits: + cpus: "2.0" + memory: 50M + volumes: + - data_dir:/data + depends_on: + - app + ports: + - "80:80" + + app: + image: ghcr.io/cssnr/red-api-app:${VERSION:-latest} + env_file: settings.env + command: "gunicorn myapp.wsgi:application -b 0.0.0.0:9000 -w 2" + healthcheck: + test: ["CMD", "curl", "-sf", "http://localhost:9000/app-health-check/"] + interval: 1m + timeout: 10s + retries: 3 + start_period: 15s + deploy: + mode: global + resources: + limits: + cpus: "2.0" + memory: 250M + volumes: + - data_dir:/data + + redis: + image: redis:6-alpine + command: "redis-server --appendonly yes" + deploy: + replicas: 1 + resources: + limits: + cpus: "2.0" + memory: 50M + volumes: + - redis_data:/data + + redis-commander: + image: ghcr.io/joeferner/redis-commander:latest + environment: + - REDIS_HOSTS=local:redis:6379:0,local:redis:6379:1 + - URL_PREFIX=/redis + - TRUST_PROXY=true + deploy: + replicas: 1 + resources: + limits: + cpus: "2.0" + memory: 100M + depends_on: + - redis + +volumes: + data_dir: + redis_data: diff --git a/docker-compose-swarm.yaml b/docker-compose-swarm.yaml index fc207f7..4a26145 100644 --- a/docker-compose-swarm.yaml +++ b/docker-compose-swarm.yaml @@ -1,29 +1,28 @@ -version: '3.7' +version: "3.8" services: nginx: - build: - context: ./nginx - network: host - image: ${REGISTRY_HOST}/shane/redapi-nginx:${VERSION} + image: ghcr.io/cssnr/red-api-nginx:${VERSION:-latest} deploy: mode: global resources: limits: - cpus: '2.0' + cpus: "2.0" memory: 50M labels: - "traefik.enable=true" - "traefik.docker.network=traefik-public" - "traefik.constraint-label=traefik-public" - - "traefik.http.routers.STACK_NAME-http.rule=Host(${TRAEFIK_HOST})" - - "traefik.http.routers.STACK_NAME-http.entrypoints=http" - - "traefik.http.routers.STACK_NAME-http.middlewares=https-redirect" - - "traefik.http.routers.STACK_NAME-https.rule=Host(${TRAEFIK_HOST})" - - "traefik.http.routers.STACK_NAME-https.entrypoints=https" - - "traefik.http.routers.STACK_NAME-https.tls=true" - - "traefik.http.services.STACK_NAME.loadbalancer.server.port=80" - - "traefik.http.services.STACK_NAME.loadbalancer.server.scheme=http" + - "traefik.http.routers.${STACK_NAME}-http.rule=Host(`${TRAEFIK_HOST}`)" + - "traefik.http.routers.${STACK_NAME}-http.entrypoints=http" + - "traefik.http.routers.${STACK_NAME}-http.middlewares=${STACK_NAME}-http-redirect" + - "traefik.http.middlewares.${STACK_NAME}-http-redirect.redirectscheme.scheme=https" + - "traefik.http.middlewares.${STACK_NAME}-http-redirect.redirectscheme.permanent=true" + - "traefik.http.routers.${STACK_NAME}-https.rule=Host(`${TRAEFIK_HOST}`)" + - "traefik.http.routers.${STACK_NAME}-https.entrypoints=https" + - "traefik.http.routers.${STACK_NAME}-https.tls=true" + - "traefik.http.services.${STACK_NAME}.loadbalancer.server.port=80" + - "traefik.http.services.${STACK_NAME}.loadbalancer.server.scheme=http" volumes: - data_dir:/data secrets: @@ -31,20 +30,54 @@ services: target: /etc/nginx/auth.users depends_on: - app - #- flower - redis-commander - #- uptime-kuma - #- pgadmin networks: - traefik-public - net-internal app: - build: - context: ./app - network: host - image: ${REGISTRY_HOST}/shane/redapi-app:${VERSION} - env_file: ${ENV_FILE} + image: ghcr.io/cssnr/red-api-app:${VERSION:-latest} + environment: + APP_LOG_LEVEL: ${APP_LOG_LEVEL} + DJANGO_LOG_LEVEL: ${DJANGO_LOG_LEVEL} + DEBUG: ${DEBUG} + ALLOWED_HOSTS: ${ALLOWED_HOSTS} + CSRF_ORIGINS: ${CSRF_ORIGINS} + SESSION_COOKIE_AGE: ${SESSION_COOKIE_AGE} + SECRET_KEY: ${SECRET_KEY} + STATIC_ROOT: ${STATIC_ROOT} + MEDIA_ROOT: ${MEDIA_ROOT} + LANGUAGE_CODE: ${LANGUAGE_CODE} + USE_TZ: ${USE_TZ} + TZ: ${TZ} + USE_X_FORWARDED_HOST: ${USE_X_FORWARDED_HOST} + SECURE_REFERRER_POLICY: ${SECURE_REFERRER_POLICY} + DATABASE_NAME: ${DATABASE_NAME} + DATABASE_USER: ${DATABASE_USER} + DATABASE_PASS: ${DATABASE_PASS} + DATABASE_HOST: ${DATABASE_HOST} + DATABASE_PORT: ${DATABASE_PORT} + DEFAULT_FROM_EMAIL: ${DEFAULT_FROM_EMAIL} + EMAIL_HOST: ${EMAIL_HOST} + EMAIL_HOST_USER: ${EMAIL_HOST_USER} + EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} + EMAIL_USE_TLS: ${EMAIL_USE_TLS} + EMAIL_PORT: ${EMAIL_PORT} + REDIS_IGNORE_EXCEPTIONS: ${REDIS_IGNORE_EXCEPTIONS} + CACHE_BACKEND: ${CACHE_BACKEND} + CACHE_LOCATION: ${CACHE_LOCATION} + SENTRY_URL: ${SENTRY_URL} + SENTRY_SAMPLE_RATE: ${SENTRY_SAMPLE_RATE} + SENTRY_DEBUG: ${SENTRY_DEBUG} + SENTRY_ENVIRONMENT: ${SENTRY_ENVIRONMENT} + GOOGLE_SITE_PUBLIC: ${GOOGLE_SITE_PUBLIC} + GOOGLE_SITE_SECRET: ${GOOGLE_SITE_SECRET} + OAUTH_CLIENT_ID: ${OAUTH_CLIENT_ID} + OAUTH_CLIENT_SECRET: ${OAUTH_CLIENT_SECRET} + OAUTH_REDIRECT_URL: ${OAUTH_REDIRECT_URL} + SUPER_USERS: ${SUPER_USERS} + DISCORD_WEBHOOK: ${DISCORD_WEBHOOK} + DISCORD_INVITE: ${DISCORD_INVITE} command: "gunicorn myapp.wsgi:application -b 0.0.0.0:9000 -w 2" healthcheck: test: ["CMD", "curl", "-sf", "http://localhost:9000/app-health-check/"] @@ -56,60 +89,12 @@ services: mode: global resources: limits: - cpus: '2.0' + cpus: "2.0" memory: 250M volumes: - data_dir:/data - depends_on: - - redis networks: - net-internal - - postgres - -# worker: -# image: ${REGISTRY_HOST}/shane/redapi-app:${VERSION} -# env_file: ${ENV_FILE} -# command: "celery -A myapp worker -l INFO -c 2" -# deploy: -# replicas: 1 -# resources: -# limits: -# cpus: '2.0' -# memory: 200M -# depends_on: -# - app -# - redis -# networks: -# - net-internal - -# beat: -# image: ${REGISTRY_HOST}/shane/redapi-app:${VERSION} -# env_file: ${ENV_FILE} -# command: "celery -A myapp beat -l INFO -S django" -# deploy: -# replicas: 1 -# resources: -# limits: -# cpus: '2.0' -# memory: 150M -# depends_on: -# - app -# - redis -# networks: -# - net-internal - -# flower: -# image: mher/flower:latest -# env_file: ${ENV_FILE} -# deploy: -# replicas: 1 -# volumes: -# - flower_data:/data -# networks: -# - net-internal -# depends_on: -# - app -# - redis redis: image: redis:6-alpine @@ -118,7 +103,7 @@ services: replicas: 1 resources: limits: - cpus: '2.0' + cpus: "2.0" memory: 50M volumes: - redis_data:/data @@ -135,7 +120,7 @@ services: replicas: 1 resources: limits: - cpus: '2.0' + cpus: "2.0" memory: 100M depends_on: - redis @@ -148,7 +133,6 @@ secrets: volumes: data_dir: -# flower_data: redis_data: networks: @@ -156,5 +140,3 @@ networks: driver: overlay traefik-public: external: true - postgres: - external: true diff --git a/docker-compose.yaml b/docker-compose.yaml index 8562e50..aacc4b8 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,16 +1,14 @@ -version: '3.7' +version: "3.8" services: nginx: - build: - context: ./nginx - network: host - image: shane-redapi-nginx:latest + build: nginx + image: red-api-nginx:latest deploy: mode: global resources: limits: - cpus: '2.0' + cpus: "2.0" memory: 50M labels: - "traefik.enable=true" @@ -31,10 +29,7 @@ services: target: /etc/nginx/auth.users depends_on: - app - #- flower - redis-commander - #- uptime-kuma - #- pgadmin networks: - redbot-internal ports: @@ -42,9 +37,9 @@ services: app: build: - context: ./app - network: host - image: shane-redapi-app:latest + context: . + dockerfile: ./app/Dockerfile + image: red-api-app:latest env_file: settings.env command: "gunicorn myapp.wsgi:application -b 0.0.0.0:9000 -w 2" healthcheck: @@ -57,54 +52,13 @@ services: mode: global resources: limits: - cpus: '2.0' + cpus: "2.0" memory: 250M volumes: - data_dir:/data networks: - redbot-internal -# worker: -# image: shane-redapi-app:${VERSION} -# env_file: ${ENV_FILE} -# command: "celery -A myapp worker -l INFO -c 2" -# deploy: -# replicas: 1 -# resources: -# limits: -# cpus: '2.0' -# memory: 200M -# depends_on: -# - app -# - redis - -# beat: -# image: ${REGISTRY_HOST}/shane/redapi-app:${VERSION} -# env_file: ${ENV_FILE} -# command: "celery -A myapp beat -l INFO -S django" -# deploy: -# replicas: 1 -# resources: -# limits: -# cpus: '2.0' -# memory: 150M -# depends_on: -# - app -# - redis - -# flower: -# image: mher/flower:latest -# env_file: ${ENV_FILE} -# deploy: -# replicas: 1 -# volumes: -# - flower_data:/data -# networks: -# - net-internal -# depends_on: -# - app -# - redis - redis: image: redis:6-alpine command: "redis-server --appendonly yes" @@ -112,7 +66,7 @@ services: replicas: 1 resources: limits: - cpus: '2.0' + cpus: "2.0" memory: 50M volumes: - redis_data:/data @@ -129,7 +83,7 @@ services: replicas: 1 resources: limits: - cpus: '2.0' + cpus: "2.0" memory: 100M depends_on: - redis @@ -142,7 +96,6 @@ secrets: volumes: data_dir: -# flower_data: redis_data: networks: diff --git a/nginx/Dockerfile b/nginx/Dockerfile index c092cd1..4f96082 100644 --- a/nginx/Dockerfile +++ b/nginx/Dockerfile @@ -1,5 +1,9 @@ FROM nginx:alpine-slim +LABEL org.opencontainers.image.source="https://github.com/cssnr/red-api" +LABEL org.opencontainers.image.description="Carl Red Web API Nginx" +LABEL org.opencontainers.image.authors="smashedr" + ENV TZ=UTC COPY nginx.conf /etc/nginx/nginx.conf diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 00e738d..b918be1 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -1,4 +1,5 @@ -worker_processes 2; +user nginx; +worker_processes auto; error_log /dev/stderr warn; @@ -8,13 +9,12 @@ events { http { sendfile on; + client_max_body_size 1024M; include /etc/nginx/mime.types; default_type application/octet-stream; - client_max_body_size 1024M; - - log_format main '$time_local - $http_x_real_ip [$status] ' - '$body_bytes_sent $remote_user "$request" ' + log_format main '$time_local - $http_x_real_ip ($realip_remote_addr) ' + '[$status] $body_bytes_sent $remote_user "$request" ' '"$http_referer" "$http_user_agent"'; access_log /dev/stdout main; @@ -22,6 +22,12 @@ http { server { listen 80; + location /health-check { + add_header Content-Type text/plain; + return 200 "success"; + access_log off; + } + location ~ ^/favicon.(?[a-z]+)$ { alias /data/static/images/favicon.$extension; } diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..37a0b3e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,102 @@ +[project] +name = "red-api" +version = "0.0.1" +requires-python = ">=3.13" +dependencies = [ + "django>=5,<6", + "django-redis", + "gunicorn", + "httpx", + "psycopg[binary]", + "python-decouple", + "xmltodict", +] + +[dependency-groups] +dev = [ + "bandit", + "black", + "coverage", + "django-stubs>=6.0.7", + "isort", + "mypy", + "ruff", + "tombi", + "toml-run", + "ty", + "types-requests", + "yamllint", +] + +# Bandit +# https://bandit.readthedocs.io/en/latest/config.html +[tool.bandit] +exclude_dirs = [".venv"] + +# Black +# https://black.readthedocs.io/en/stable/usage_and_configuration/ +[tool.black] +line-length = 119 +target-version = ["py313"] +extend-exclude = '(migrations|templates|\.github)' + +# Coverage +# https://coverage.readthedocs.io/en/latest/ +[tool.coverage.run] +omit = [".venv/*", "migrations/*", "node_modules/*"] + +# Isort +# https://pycqa.github.io/isort/docs/configuration/options.html +[tool.isort] +profile = "black" +line_length = 119 +src_paths = ["app"] +skip = [".venv", "migrations", "node_modules", ".github"] + +# Mypy +# https://mypy.readthedocs.io/en/stable/config_file.html +[tool.mypy] +ignore_missing_imports = true +exclude = [".venv", "migrations", "node_modules"] + +# Ruff +# https://docs.astral.sh/ruff/configuration/ +[tool.ruff] +line-length = 119 +target-version = "py313" +extend-exclude = ["migrations", ".github"] + +[tool.ruff.lint] +select = ["E4", "E7", "E9", "F", "B", "Q"] + +# Scripts +# https://github.com/cssnr/toml-run +[tool.scripts] +test = [ + "uv run coverage run --rcfile=pyproject.toml app/manage.py test --verbosity 2 --keepdb", + "uv run coverage xml", + "uv run coverage report -m", +] +format = [ + "uv run black app", + "uv run isort app", + "uv run ruff format", + "uv run tombi format", + "which prettier && prettier --write . || echo missing prettier", +] +lint = [ + "run bandit", + # "run mypy", + "run ruff", + "run yamllint", + "uv run black --check app", + "uv run isort -c app", + "uv run tombi lint", + # "uv run ty check -v app", + "uv lock --check", + "which prettier && prettier --check . || echo missing prettier", +] +bandit = "uv run bandit -c pyproject.toml -r app" +mypy = "uv run mypy app" +ruff = "uv run ruff check app" +yamllint = "uv run yamllint -c .github/yamllint.yaml ." diff --git a/settings.env.example b/settings.env.example new file mode 100644 index 0000000..3593e84 --- /dev/null +++ b/settings.env.example @@ -0,0 +1,50 @@ +APP_LOG_LEVEL=DEBUG +DJANGO_LOG_LEVEL=INFO + +DEBUG=True +ALLOWED_HOSTS=* +CSRF_ORIGINS= +SESSION_COOKIE_AGE=1209600 +SECRET_KEY=your-secret-key-here +STATIC_ROOT=/data/static +MEDIA_ROOT=/data/media +LANGUAGE_CODE=en-us +USE_TZ=True +TZ=UTC + +USE_X_FORWARDED_HOST=True +SECURE_REFERRER_POLICY=no-referrer-when-downgrade + +DATABASE_NAME=redapilocal +DATABASE_USER=redapilocal +DATABASE_PASS=your-database-password +DATABASE_HOST=jammy.local +DATABASE_PORT=5432 + +DEFAULT_FROM_EMAIL=no-reply@example.com +EMAIL_HOST=mail.example.com +EMAIL_HOST_USER=smtp@example.com +EMAIL_HOST_PASSWORD=your-email-password +EMAIL_USE_TLS=True +EMAIL_PORT=587 + +REDIS_IGNORE_EXCEPTIONS=True +CACHE_BACKEND=django_redis.cache.RedisCache +CACHE_LOCATION=redis://redis:6379/0 + +SENTRY_URL= +SENTRY_SAMPLE_RATE=0.1 +SENTRY_DEBUG=False +SENTRY_ENVIRONMENT=dev + +GOOGLE_SITE_PUBLIC=your-google-site-key +GOOGLE_SITE_SECRET=your-google-site-secret + +OAUTH_CLIENT_ID=your-oauth-client-id +OAUTH_CLIENT_SECRET=your-oauth-client-secret +OAUTH_REDIRECT_URL=https://your-domain.com/oauth/callback/ + +SUPER_USERS=your-discord-user-id + +DISCORD_WEBHOOK=https://discord.com/api/webhooks/your-webhook-id/your-webhook-token +DISCORD_INVITE=https://example.com/discord diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..2d96382 --- /dev/null +++ b/uv.lock @@ -0,0 +1,833 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version < '3.15'", +] + +[[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 = "ast-serialize" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/ad/0d70a3a2d6e01968d985415259e8ec7ad3f777903f9b1c1f3c8c44642c60/ast_serialize-0.6.0.tar.gz", hash = "sha256:aadd3ffcf4858c9726bf3515f7b199c7eadbe504f96028e4a87172c0da65a8fe", size = 61489, upload-time = "2026-06-30T20:02:55.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/12/3e5f575f156555547c250a8b0d1347517a3a20fc7f4492e9703a69d4f45e/ast_serialize-0.6.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:a7520b672827885bafeae7501f684d14d47d17e5f45256f9df547686cca52264", size = 1177640, upload-time = "2026-06-30T20:02:06.708Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a4/921a9e27951627983b0f368859ea00f8330a551dc0bf4c2fdcb11855a98b/ast_serialize-0.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a14191beec7e0c078d2fc1f6edc0aee88bcd4db9f18e1bc9f8052b559c22dddc", size = 1168111, upload-time = "2026-06-30T20:02:08.366Z" }, + { url = "https://files.pythonhosted.org/packages/00/69/950cf404de7b8782cf95e5c1237e25e2aa46177b287f39f9eeddf481fd6f/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32ef62ec34cf6be20ad77d4799556638fbdf187f3ae10698dfb20ef9f2c89516", size = 1227656, upload-time = "2026-06-30T20:02:09.843Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a8/46f8f6a6479d9d2273980957bb091a506c55f5b95d3c029ee58518a78407/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13b7769970a39983b0adf2f38917b1cd3b8946f76df045756c3d741bc689f089", size = 1227706, upload-time = "2026-06-30T20:02:11.367Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b9/9ac415bda0a40e49eab8fea3b2741c19c98bb84d57d62c4cfc6230eb67be/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f7a408601bb3edaefb3bc67a4c01f5235e3253653b6a5729a2ee2382b35341c", size = 1431705, upload-time = "2026-06-30T20:02:12.737Z" }, + { url = "https://files.pythonhosted.org/packages/e5/06/8807115d441444879f7561b5eede5ac18fc80392f11826d61ccf31f503b1/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8670bfa51208a2c0c8d138928e40e998fab158f9200d53bb80c088b5b8eda7b8", size = 1249533, upload-time = "2026-06-30T20:02:14.571Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c0/c2ba82ef9618650357d9421a1fdb27ffec862a7f57e8e2de82a3ccd11e12/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4826809eb8597a8cd59fd924b6d7c285b8969a1e0007e2cb652cab62376270f", size = 1252619, upload-time = "2026-06-30T20:02:16.219Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a7/fa31d52dd4102cede29fb9634e98d214129b2783b4f95528c6dc6a8f6587/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:577a6c189068686869f5f1ddc38363f3ae1808a4753b577266f9202071a7bb66", size = 1242983, upload-time = "2026-06-30T20:02:17.813Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/ddf742b5ad3c4bafd3466f2265037cfd99bc1b9a5ee46a5d58c90d523242/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:085de7f62dc9cc247eb01e965a362707d1d90b1d89a82c5bf78301a60a3c417b", size = 1296148, upload-time = "2026-06-30T20:02:19.146Z" }, + { url = "https://files.pythonhosted.org/packages/24/cb/9f6f217cce8b3b632c5568b478d195a35e79dce4dbe309438cb89ba6ea4f/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9f8a8b78b13173de6a9ec22111d9be674874cd5bdccda04f14ae5ebc2bef403a", size = 1403826, upload-time = "2026-06-30T20:02:20.696Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f8/9d16d4f0107a183924425cc0e7618d8bf76f96b45afa9ff19f924ed1ad57/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f2ff3baffc3a29c1f15bc9098aa0c09763410262d5e6cef42116f7356c184554", size = 1502943, upload-time = "2026-06-30T20:02:22.034Z" }, + { url = "https://files.pythonhosted.org/packages/80/dd/bbc1c38756350dddf7e24acae1c9482ef42051c267417e019aecc1ed4075/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0067b25fce104eaae5b88383de9ab803faeb671831e14ca698b771b356e2600f", size = 1497632, upload-time = "2026-06-30T20:02:23.517Z" }, + { url = "https://files.pythonhosted.org/packages/42/7e/9daffefcf5b97e6bb4c3e0b3c024c1aee9722f23d3cf7cd2ff80d6fb4a40/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c617417f9cbb0cb144f6283c3cbe0d2e0f01beaf9f608f662b21191058a626ec", size = 1448858, upload-time = "2026-06-30T20:02:24.889Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1f/f9baaab81a677ea0af7d2458cac2f94ebcc85958f8a3c15ba9d9e5dab653/ast_serialize-0.6.0-cp314-cp314t-win32.whl", hash = "sha256:5337cb256dcea3df9288205213d1601581536526b8f4da44b6974f1180f3252a", size = 1052600, upload-time = "2026-06-30T20:02:26.263Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1f/41b535866519512d8cf6669cb2cff7823b7672bb6279c0333b4ff89d7d9f/ast_serialize-0.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2d947e45cafc4b09bd7528917fa84c517654a43de173c79785574b7b3068ac24", size = 1095570, upload-time = "2026-06-30T20:02:27.639Z" }, + { url = "https://files.pythonhosted.org/packages/50/64/e472fe3e3a2d33d874b987e8518aedf24562919e3b6161a4fa1797e89c0f/ast_serialize-0.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6e15ec740436e1a0d62de848641abe5f3a2f89a7f94907d534795ac91bbacf14", size = 1067267, upload-time = "2026-06-30T20:02:28.949Z" }, + { url = "https://files.pythonhosted.org/packages/52/19/ac8348ae8711c9b5ae834634f635780cab62a0f5e6f988882e048b89c2ae/ast_serialize-0.6.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:093cb8bb91b720d8523580498d031791bb1bbaa048599c3d21085d380e11a596", size = 1185367, upload-time = "2026-06-30T20:02:30.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f6/ec7ec652c51db77c2f61d8573338e13e4704303265ccc658cb4031d9f354/ast_serialize-0.6.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:e61580a69faf47e3689795367ed211f2a10fd741478cc0f36a0f128793360aad", size = 1178657, upload-time = "2026-06-30T20:02:31.964Z" }, + { url = "https://files.pythonhosted.org/packages/6f/02/613a7534a41d0122f37d1e0c64aa8ac78bfb831f8c92f6db057a311abb3c/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:305802f2ce2a7c4e87835078ea85c58b586ddda8095b92fe2ead9364ae19c80a", size = 1238620, upload-time = "2026-06-30T20:02:33.664Z" }, + { url = "https://files.pythonhosted.org/packages/4d/21/087957bba486242afc52f49b2d9e21c9dad00289356cf9efe67084015a9d/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c7b8b8f0c42f752ea00b2b7d7c090b3f80d9c1c5c75cadf16423790a0cc74081", size = 1236075, upload-time = "2026-06-30T20:02:34.936Z" }, + { url = "https://files.pythonhosted.org/packages/82/04/78128bbb170071c2c72a210a181f1c00e11cc1cec60a8beef747b07f9201/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd5b91b9e6f2356ace3a556963b0cd783b395fbbb0bb17b4defc283415466e77", size = 1441348, upload-time = "2026-06-30T20:02:36.245Z" }, + { url = "https://files.pythonhosted.org/packages/64/64/62fb99d6faf199b4c3e5b08a07136e9a0d7664bb249c6de3670e5b63e9b6/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d6ef91590258ada18909b9caea344dac4de2013906b035473cd674a43f4b790", size = 1258580, upload-time = "2026-06-30T20:02:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/ca/87/b4d6c38e0ccd5e85dc54cecdf933a152c60b28fe5d993a6d8a72fa6d5896/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcbed41e9386059fc0261d602445ede0976c2ecec2939688bcbcb9ed0b6f28b7", size = 1261693, upload-time = "2026-06-30T20:02:39.123Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4b/3676ca2191f39bafb75f93f99b2f429ec464586158fece2165f3572805dc/ast_serialize-0.6.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:cdc4e6f930b9090c2f92c9036ad12ffb8e6e44d4a5ba06f1458a05d60f203f7b", size = 1252517, upload-time = "2026-06-30T20:02:40.511Z" }, + { url = "https://files.pythonhosted.org/packages/f3/58/494ef8c4b4acb2f4a265ac934caf45f792a08fe27d6b853de35ad991941a/ast_serialize-0.6.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:897ac47b5637be41c0c07061c8a912fafa967ef1dc73fa115e4bfa70882a093b", size = 1304843, upload-time = "2026-06-30T20:02:41.961Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f2/13736d920ab3d49bbee80ef1a277dd7b7aaf3b3545efd9d2a8114fe05525/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c4af9a1386166e40ed01464991806f89038a2d89782576c7774876fa77034e32", size = 1413698, upload-time = "2026-06-30T20:02:44.179Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5a/e046f3899e2acba4677d7427b76431443a1aa1a0e583dfb05b55b69d55cf/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:c901adbd750029b9ac4ad3d6aa56853e0ad4875119fbf52b7b8298afc223828b", size = 1512209, upload-time = "2026-06-30T20:02:45.584Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c7/e42aaca7bb2d22a7c06d5a8c7930086c5a334e93d716e6fa5e6647a4515f/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae22a366b752ab4496191525b78b097b5b72d531752e3c1dd7e383a8f2c8a1a", size = 1508464, upload-time = "2026-06-30T20:02:46.942Z" }, + { url = "https://files.pythonhosted.org/packages/95/93/5524a3dc6c3f593de3228ed9cbef73afa047625b7000ec21b7f58e6eb4d4/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4ed29121da8b3fdc291002801a1de0f76248fa07dce89157a5f277842cf6126e", size = 1457164, upload-time = "2026-06-30T20:02:48.294Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c0/36a6ffb4d653cf621427b4c4928671f53ad800c453474de2b82564a44ad9/ast_serialize-0.6.0-cp39-abi3-pyemscripten_2026_0_wasm32.whl", hash = "sha256:b1dac4e09d341c1300ba69cdcbe62867b32a8c75d90db9bf4d083bec3b039f0b", size = 863014, upload-time = "2026-06-30T20:02:49.742Z" }, + { url = "https://files.pythonhosted.org/packages/09/c7/7d5ad8b49e1278e1c2a1e0274bd7850560b3f09313aa00c13bc8d5544792/ast_serialize-0.6.0-cp39-abi3-win32.whl", hash = "sha256:82c312a7844d2fdeb4d5c48bd3d215bf940dafd4704e1a9bcf252a99010a99b1", size = 1063165, upload-time = "2026-06-30T20:02:50.98Z" }, + { url = "https://files.pythonhosted.org/packages/47/ae/6710c14ecb276031cf10249f6adf5a59e2d3fdb3b5183bd59f70524067ee/ast_serialize-0.6.0-cp39-abi3-win_amd64.whl", hash = "sha256:113b58346f9ceb664352032770caca817d4a3c86f611c6088e6ef65ddaa70f0e", size = 1101444, upload-time = "2026-06-30T20:02:52.554Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/c53deb2cd0c9b0fb636d24d9f40924cf2e65028e6b20b10cd5c1eeb2c730/ast_serialize-0.6.0-cp39-abi3-win_arm64.whl", hash = "sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e", size = 1072965, upload-time = "2026-06-30T20:02:54.097Z" }, +] + +[[package]] +name = "bandit" +version = "1.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "stevedore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/c3/0cb80dfe0f3076e5da7e4c5ad8e57bac6ac357ff4a6406205501cade4965/bandit-1.9.4.tar.gz", hash = "sha256:b589e5de2afe70bd4d53fa0c1da6199f4085af666fde00e8a034f152a52cd628", size = 4242677, upload-time = "2026-02-25T06:44:15.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/a4/a26d5b25671d27e03afb5401a0be5899d94ff8fab6a698b1ac5be3ec29ef/bandit-1.9.4-py3-none-any.whl", hash = "sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e", size = 134741, upload-time = "2026-02-25T06:44:13.694Z" }, +] + +[[package]] +name = "black" +version = "26.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/37/5628dd55bf2b34257fc7603f0fe97c40e3aaf24265f416a9c85c95ca1436/black-26.5.1.tar.gz", hash = "sha256:dd321f668053961824bcc1be1cc1df748b2d7e4fa28086b08331e577b0100a73", size = 679439, upload-time = "2026-05-18T16:53:36.107Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/5c/c384363980e11e25ca6b93205949bb331fbf35f4e0dbec376dfa6326cec8/black-26.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b36cf2ddf5566e205f6535f782a62194a184d33e175b64ae8c40b1737522be3", size = 2009020, upload-time = "2026-05-18T17:05:28.132Z" }, + { url = "https://files.pythonhosted.org/packages/0b/df/9f31c5e0babbfed77d505fc5d120beb98b21b33feaeded3924ea941fe360/black-26.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f7ea64ebfa01b50f693508fc39f875e264446d3b097088f84f203b9d09618a0", size = 1813335, upload-time = "2026-05-18T17:05:31.266Z" }, + { url = "https://files.pythonhosted.org/packages/fb/24/8e7b9a2fa61b0afd82209efe937557d180a1fa055bd7f6161eb9defc3719/black-26.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecb3e624844c798144e9bd986954e0adc81d8911a1f30f375e1252fe26e8c294", size = 1881614, upload-time = "2026-05-18T17:05:32.718Z" }, + { url = "https://files.pythonhosted.org/packages/49/ad/b4e0d9365ba8ac34f6bbab62a4b1b2dd5d618fac3fa1b8db968c844201b5/black-26.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:e1a26503279b6b310669fb0b219c39e4820b77e8189fe80f522bb511f247db0a", size = 1488925, upload-time = "2026-05-18T17:05:34.259Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4b/652b859bf5df88a751c30451b09338f7fd26a77d1271c666992f836b7711/black-26.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c34b25da232ead53a6f335b76dbea124f4d152ad568b9080d6f944bc2b34b52", size = 1289883, upload-time = "2026-05-18T17:05:36.019Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a8da8eb208c51c7f4ce74609a45d0dcc6d8a2141e45e81ee5289d1bb0d59/black-26.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e88976690a64b0af98312ca958415849cb42423423c5f2ee74af4b49a97a2168", size = 2004800, upload-time = "2026-05-18T17:05:38.182Z" }, + { url = "https://files.pythonhosted.org/packages/11/8a/a479296a19e383b70a725882a6cf3d786540601ff03cabbaaf1cce864c5a/black-26.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32d5ea7f6c8bdfa6e648326ebca1f02b0764e2a029edc6f8dce2627e19d468c3", size = 1815576, upload-time = "2026-05-18T17:05:40.309Z" }, + { url = "https://files.pythonhosted.org/packages/81/6b/cfaf3d39f25132c156a068f6b805576c9103a84086019507c70e1911ee7d/black-26.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea8d16dc41655aa113cd64665e7219446cd7e4ff2248d7178eaa905190c86b18", size = 1877927, upload-time = "2026-05-18T17:05:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/66/76/302e313964bcff7e28df329d39f84f5270095730d85ff0acc260610a0d82/black-26.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:577f21094ea469ef92ec1adaf2c9441a226d2144d01a5be2fa823cecf6543e50", size = 1511860, upload-time = "2026-05-18T17:05:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/27/4e/a3827e35e0e567f9f9ee59e2a0ab979267dca98718f25547ca8c6733afd4/black-26.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:ed1a20af114c301a0269bf01163d51dbef72737fd65f850001e7cbe7f3c7abae", size = 1316632, upload-time = "2026-05-18T17:05:45.521Z" }, + { url = "https://files.pythonhosted.org/packages/94/51/f975cae76d44274cc2868dc9040ac5d58d464784610234455b4e7b19c6ef/black-26.5.1-py3-none-any.whl", hash = "sha256:4ed7f7da04046d2e488437170797d3b4a4ad83906683bcb7dfc68b673bbce5e2", size = 213693, upload-time = "2026-05-18T16:53:33.964Z" }, +] + +[[package]] +name = "certifi" +version = "2026.6.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, +] + +[[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 = "coverage" +version = "7.15.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/d0/55fe630f4cf94e3fcba868240fad8c8cdd1f764e2a932f8926347e6ec4cd/coverage-7.15.2.tar.gz", hash = "sha256:3df60dc267f0a2ca23cb7a9ab1109c62b9335ffbf519fcfe167157c28c09b81d", size = 927741, upload-time = "2026-07-15T18:56:19.558Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/d5/f8c838e6b7282976f7c918884b792df7a0c42c5bba5d99c60ad2d221d56d/coverage-7.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1121caa19159a38b5463eaae4b1e1fde81e525b15ecc5e000cd5b1a108f743a8", size = 221606, upload-time = "2026-07-15T18:54:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/bf/37/97c926376364f66298cc44893b89cdf17b8bc406376497c4061ae4b8a8ff/coverage-7.15.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a300c6934e0989c327b9e8a1e110329da4641149f872bbe9f70168be66da76c1", size = 221982, upload-time = "2026-07-15T18:54:47.341Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/a36050a6e83c2135ee0776f452ca3948224befc6d7f26acecc082d0c106a/coverage-7.15.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2617f8799d268fabdeef42a7e89ac3a23e1deee9025427db2df970f99a89a578", size = 252972, upload-time = "2026-07-15T18:54:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/31/d3/06b5f1daf95f0f15ab05bd75f26ba5f3c8b33d0bb72f3aaa3cf41d1bad3a/coverage-7.15.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7dc2950a2992cd676d35c20ae63522836deeb034f08874699d14068710af3dc1", size = 255569, upload-time = "2026-07-15T18:54:51.098Z" }, + { url = "https://files.pythonhosted.org/packages/81/1c/9afb3f8de2b8d36960391c48559a2e3ff96594b58099f115921549ea8d0d/coverage-7.15.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e36686f7a442185db2400b3df171aac520869faf9deb59df687d28659eda2a6", size = 256806, upload-time = "2026-07-15T18:54:53.145Z" }, + { url = "https://files.pythonhosted.org/packages/64/d8/b989f96061a5e32d82fddd1b1b9ff48a7c8f8ae7606f0e80fd9de54b1e33/coverage-7.15.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d29ca7bd67af6e12e74632d65f026eabc1364da5c254494cd914446a28a3ef7", size = 258936, upload-time = "2026-07-15T18:54:55.015Z" }, + { url = "https://files.pythonhosted.org/packages/b8/fa/f99771f5110457c7b511c1935ca49ddf288218eaa84322e028b9334146ae/coverage-7.15.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:db9c8438057e5b0f6a22a0af99c0c1d26b57fbbdbd1be5861ddb8f897fcc3a2d", size = 253178, upload-time = "2026-07-15T18:54:57.527Z" }, + { url = "https://files.pythonhosted.org/packages/f6/96/c098a6044d119c751ceede7be91035fa8310170ec24a6523aff72f0a5793/coverage-7.15.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:63022c4c8dec1d0342f05c3ede99842fe3d007689acc45e86f123a1746e4a026", size = 254934, upload-time = "2026-07-15T18:54:59.41Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a2/1457b3a7a50c8d77500103b97a046db863e2f59a1cf6d2f814595f349885/coverage-7.15.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6c0be82b4d4aa5b2704e08518e2252f3e3d110164bcca826816801052e48a7aa", size = 252898, upload-time = "2026-07-15T18:55:01.338Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0e/76958874c471ecfcdde0d2b2747bb2c61bdbf34a40636f4ce9db9923e643/coverage-7.15.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4510fb9cdf6bb02dfa6af0be4a534b8102d086e22e4a33f8836df663da3d660d", size = 257056, upload-time = "2026-07-15T18:55:03.243Z" }, + { url = "https://files.pythonhosted.org/packages/7c/7c/3d7c4e3bf58baa40327dc7edc2272b17cf02299366d52763db1b0ca1556a/coverage-7.15.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:42ec3d989421b174a2ab607c1539f24127ad362757b7f1c0c0d7a2993f7eb37b", size = 252718, upload-time = "2026-07-15T18:55:05.029Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b8/1cecffed9ce14fb25be9ba42d37b6bb61485c9a3ddd43cd3dde36b6087d8/coverage-7.15.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8f91bce78e32343af184c3b7fa28fcf5a9e2641f4b6623d392038f804939188", size = 254490, upload-time = "2026-07-15T18:55:06.889Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2c/42984561bc7f4c045dca67516a0c50ee5ef8d84352dbeb5559dc86c4823e/coverage-7.15.2-cp313-cp313-win32.whl", hash = "sha256:434e68d531858205895eb0d74b73d20b84260de426387d53c422a5acda2cf050", size = 223647, upload-time = "2026-07-15T18:55:08.941Z" }, + { url = "https://files.pythonhosted.org/packages/41/9f/39c7c9245efc583beddf89a87683574e663ed93637f3afb6cd7b88405676/coverage-7.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:26c3b04a6377fd7c09800921fa934e3a17c0020439cd59df73e73ae1d4b6a78c", size = 224190, upload-time = "2026-07-15T18:55:10.789Z" }, + { url = "https://files.pythonhosted.org/packages/c7/de/3a2883cf8a213659280ef4b403059e17a9acaeb7fc7fd4105e1226ff2e6d/coverage-7.15.2-cp313-cp313-win_arm64.whl", hash = "sha256:3ed010aa1b69cda8e827aabfca9866216c980e2dca82ab9a78c5f83689964c8b", size = 223583, upload-time = "2026-07-15T18:55:12.678Z" }, + { url = "https://files.pythonhosted.org/packages/81/5f/aed265fd7a3551a394f36dfe41868aee709b7f95db4052205b4ad1563ac3/coverage-7.15.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:40f633c5c5fc783732f6312280122e859538fa24461235597c13d803ea9a108a", size = 221650, upload-time = "2026-07-15T18:55:14.527Z" }, + { url = "https://files.pythonhosted.org/packages/6b/2c/222ba12a545189017120f8eddfc1a0bd4616b47d5d4a8d99421edb2fe4c6/coverage-7.15.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:075560438765b7a2ef43bf7aa7758661b53d889df47f062a31bda6c1ade553a2", size = 221988, upload-time = "2026-07-15T18:55:16.674Z" }, + { url = "https://files.pythonhosted.org/packages/aa/38/304b5877ab46e6c290b4292cfcf3fe28245f0e5597cad7f6acc91fc7e0a4/coverage-7.15.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:25fd15dd40a0a2c51a500d664ca29053c09c3259d998407bf982b6e114696138", size = 253029, upload-time = "2026-07-15T18:55:18.856Z" }, + { url = "https://files.pythonhosted.org/packages/6c/58/821b533b8db9e44cf1d8a97bd525149ced40dde1d0093da02cb78e715244/coverage-7.15.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f", size = 255536, upload-time = "2026-07-15T18:55:21.027Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f2/7aa06604c389d32ea7f0a6a988359a7eafc3cd3f8e7bc2e88cd2fdf0b877/coverage-7.15.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9854ca62c152874b2060772503535be2e8f53f70b8aaa7686b094888d872f984", size = 256881, upload-time = "2026-07-15T18:55:23.125Z" }, + { url = "https://files.pythonhosted.org/packages/a2/4f/1ef342339c7916d0096bc5888cc0f653882cc7bc8f897d5cb89143287c9b/coverage-7.15.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:913b6c56e110da40e035bbd168353bf7aaa2544a5eaccea5d98a4629aac156c7", size = 259196, upload-time = "2026-07-15T18:55:25.099Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f4/7ed055d7a9c5ec13b161773a115a5ccc6b0081d568c31fad830806306cc7/coverage-7.15.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aaccad4129d735a8a4d526f26929894c9a4e8ef7034566f210b176749d6906e3", size = 253036, upload-time = "2026-07-15T18:55:27.018Z" }, + { url = "https://files.pythonhosted.org/packages/14/79/ea82cca18c242a3a38b6c017da39726aa62dcb64aa635abf79b92009975c/coverage-7.15.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a164b50081fc7357331c4024ef4d17b78ba325f8380d05f5a69599a7e05257ee", size = 254887, upload-time = "2026-07-15T18:55:29.084Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ba/a136db3c0d9562b00e10b72540dbf3a33cd3bc5b95060c9308e247494623/coverage-7.15.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:bfd341ccf78128e72c094bc70cc25b3ef309c33c7c2c66ba3ed4309549e02de1", size = 252852, upload-time = "2026-07-15T18:55:31.184Z" }, + { url = "https://files.pythonhosted.org/packages/17/17/ea334246b16b7d059953fad6fdefa11e33c68efbd3fe37b1098120a1fac2/coverage-7.15.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1473b3ba8e7ee0f076117b1a72c23f579a2b9e2bb742f48a8d86ea27ca93f91a", size = 257128, upload-time = "2026-07-15T18:55:33.163Z" }, + { url = "https://files.pythonhosted.org/packages/ed/c3/074fb66d46d607855f710876b117cbda562c5ab08363528e78820449f937/coverage-7.15.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:17c432b5f73ad52ef46fb06019f6fa7c66ce381961cf0f7dfd1d3a4bd3a98145", size = 252668, upload-time = "2026-07-15T18:55:35.063Z" }, + { url = "https://files.pythonhosted.org/packages/e1/c1/f620850ada9b36435921c9a3a8057013422b1d964eb4bf37fe138724d192/coverage-7.15.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:77f0ef5011df53a4bd1b35211ab122287f8d9b8d7aa1c4553e5c2deb24b1d446", size = 254325, upload-time = "2026-07-15T18:55:37.125Z" }, + { url = "https://files.pythonhosted.org/packages/cc/31/a729ca3689404493af82ef8e6ff70bd88bdda8da89aeef6ca9b387aeb2b4/coverage-7.15.2-cp314-cp314-win32.whl", hash = "sha256:f653e5d7248c1191ec988a85c72edeab46c3ff44f90639a4ed4874ec0be90243", size = 223844, upload-time = "2026-07-15T18:55:39.078Z" }, + { url = "https://files.pythonhosted.org/packages/c6/83/5d809dc808fb1698c671f3e372259bb9158e64b7ea526fc6ab7de64de9fe/coverage-7.15.2-cp314-cp314-win_amd64.whl", hash = "sha256:9911f31aad8906abe337c271343485cf20df5e70df5d2f57f9f136e7b55f26bc", size = 224331, upload-time = "2026-07-15T18:55:41.346Z" }, + { url = "https://files.pythonhosted.org/packages/16/4e/35e488548e952795829e129995c4174df33bf432b591d1aa42c8d9e4e7ad/coverage-7.15.2-cp314-cp314-win_arm64.whl", hash = "sha256:e38def96ad59853824c97953fdcd2c320a84ba3ce99b417db78af8bb6c3db635", size = 223760, upload-time = "2026-07-15T18:55:43.518Z" }, + { url = "https://files.pythonhosted.org/packages/ed/49/dd2c86cd6374038f6e415fb5bfb86db5218553209c081384a020369dee79/coverage-7.15.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:835ec4e20b45f0a7f63ed78f94065aca00de033403df8377bfe8b9c6abc0a7be", size = 222384, upload-time = "2026-07-15T18:55:45.569Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/173ff17a1c0808e5a438f549f6f145d5ac7528f2791310b63523e3200ac7/coverage-7.15.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7466cc7ab6dc0db871d264bf99e8779f0917ee63d40730af0552f71535a6e072", size = 222647, upload-time = "2026-07-15T18:55:47.544Z" }, + { url = "https://files.pythonhosted.org/packages/84/f8/b8cba872162356fb44ac79c10309d987206a4461e32072fc29228dad7331/coverage-7.15.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e370c12133095ff18432de8c044962be85a5a96d90c6fcbce8e17e76236d2328", size = 264013, upload-time = "2026-07-15T18:55:49.768Z" }, + { url = "https://files.pythonhosted.org/packages/ee/67/a807a7586d0b8cae485308ddd55756f0806c92f8e0b411bacbf23c48edf3/coverage-7.15.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fe41909c9515c3bfdb5f02c4d1f857dba322d9a9a1178069b91eea77889df63a", size = 266135, upload-time = "2026-07-15T18:55:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/67/cd78771dc985f7e4ebdcc82b1a96d9a932af9e806f01f2f91a89f4c72e80/coverage-7.15.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6aa28cfb6488e5453b5b762d65f73aa586380f6693a04d58078ce228a29b06c0", size = 268555, upload-time = "2026-07-15T18:55:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/18/3e/10134cf81275188c58568f324fc74aedff32c63ca4d5bbc513a91944a6f0/coverage-7.15.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcc0aae933921d03096f53b0b03eeb702129fd406dee59f08d2efacc68681fa5", size = 269674, upload-time = "2026-07-15T18:55:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/75/4a/771b77de446cba985dc414bbc5844bd21604da05dbc044286df8318a48a7/coverage-7.15.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7c63387e21ab21f512c69c9756a8c7dadd322c7275edb064064433c9a09c3743", size = 263101, upload-time = "2026-07-15T18:55:58.107Z" }, + { url = "https://files.pythonhosted.org/packages/5f/b5/70a7011da15f4071943361183aefa27847f3e3aec4fd335f1cb3d3a622b1/coverage-7.15.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e55510bc98ae943cece9e667a6c0fe94c6a92913720dea34243657a17993d0c", size = 266007, upload-time = "2026-07-15T18:56:00.468Z" }, + { url = "https://files.pythonhosted.org/packages/b4/0d/f9547e804ce7ad49646ffeffac26699510efbe6c0f751b66fdc960c4e825/coverage-7.15.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2ff08701be2d1556fc78b326c80a3e8042da09352ecb3819105f8e386c8a3071", size = 263611, upload-time = "2026-07-15T18:56:02.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/59/f576a396659c0efd351f5c1544f67c3560e89c7761cabf7f65e412beeda5/coverage-7.15.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:38c9518b7103826c403a461544e3c2e77151e8676d06eaed85911a97e962584a", size = 267344, upload-time = "2026-07-15T18:56:04.622Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5d/c2e4fce3579c0cb635024293f1a32bbe26df101b3e3a69f22243d1352b6c/coverage-7.15.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:dee88b1ed88587abd8c0269a1fc1f4cc77f7750d1dfde2869e2a123af420e67d", size = 262456, upload-time = "2026-07-15T18:56:06.641Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/956287d69436b66094bc4b57ac2da71e43bfd2a5524e958900b9f582fcf8/coverage-7.15.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fbeeeecea279727f8ac16c8e1133ddfeee793e985c86ae343d6a5ce744eef8c", size = 264771, upload-time = "2026-07-15T18:56:08.795Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5a/6f979530c2734c575de77cf58f5f28d51f7123a94b5030fd9156fe5f363c/coverage-7.15.2-cp314-cp314t-win32.whl", hash = "sha256:cb0fddaa6884be6aae36ced9544b5e90f7d5f03845a2853bf47a14953a4e8688", size = 224151, upload-time = "2026-07-15T18:56:10.856Z" }, + { url = "https://files.pythonhosted.org/packages/54/7e/27f6b2a74d484742f4017553e710b01e396b23d809df3e95ca0bb9a2824b/coverage-7.15.2-cp314-cp314t-win_amd64.whl", hash = "sha256:77f091ea3a9cc611cd29f433565476bc1936c084ac8eee00ea0e7e70c27e4199", size = 224981, upload-time = "2026-07-15T18:56:12.928Z" }, + { url = "https://files.pythonhosted.org/packages/b1/48/284863423aa474240f6842bd00d680da22f4e6ea2e466618ef7c9c9e69a9/coverage-7.15.2-cp314-cp314t-win_arm64.whl", hash = "sha256:6fc448c377d6eeb00a47c673494bd9bae29280ca53987e1869e67ebedfe20658", size = 224294, upload-time = "2026-07-15T18:56:15.156Z" }, + { url = "https://files.pythonhosted.org/packages/ec/82/32e3bd191d498e64f6f911ad55d14006a0861e54869d2d32452326399e65/coverage-7.15.2-py3-none-any.whl", hash = "sha256:eb6bcae8d1a9d305351ecb108232441d11c5cfe9de840a04388ba5d2db8d735c", size = 213375, upload-time = "2026-07-15T18:56:17.305Z" }, +] + +[[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-redis" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "redis" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/78/203a0cdc0f1c083a5407d01f77b58099a120bb8a5a04562f56a9bb341314/django_redis-7.0.0.tar.gz", hash = "sha256:e48491c862f4350b0747ceb1016700686fb93c4f4e0fb9c490fe6c6658ffd933", size = 64601, upload-time = "2026-06-02T14:17:48.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/9f/09cdb9a1eebe8533b02a7694ca787acfc1e4d93b5b6175ff99366d4e6d64/django_redis-7.0.0-py3-none-any.whl", hash = "sha256:4b23aa6e0cd0937bb1242e9a463809e6004de3ca2150f34e986306bb6220d688", size = 38932, upload-time = "2026-06-02T14:17:47.281Z" }, +] + +[[package]] +name = "django-stubs" +version = "6.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "django-stubs-ext" }, + { name = "types-pyyaml" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/90/087c6e424e705e05182e543ef6b366a59eb5c92ab008b0dbbba55f357a40/django_stubs-6.0.7.tar.gz", hash = "sha256:bc55431c0af745a64e39cf33a8d36c87dccbedeae2fe26fab47dd355270e8538", size = 282293, upload-time = "2026-07-14T10:08:27.122Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/86/230ae6056221b543d63f7710d73967fe1c6840693540e99ea3c54f45859c/django_stubs-6.0.7-py3-none-any.whl", hash = "sha256:7ed9a14c438e589272ca04e966dee82a4d1ff7ca5c2171bc986c50a0d03ec35b", size = 547460, upload-time = "2026-07-14T10:08:25.626Z" }, +] + +[[package]] +name = "django-stubs-ext" +version = "6.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/50/917f7224ea470e89cdcdc93d3dfe75b8391adf976cf12f2ecdb5f5d122be/django_stubs_ext-6.0.7.tar.gz", hash = "sha256:c3172c5126614fd2a44d0196b313b44c21f717cb09477ba52b447d41f4ce613e", size = 6665, upload-time = "2026-07-14T10:07:56.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/65/4d73fce956b5ebf26449259664360e539fbe95f0e86be396c28b636ba72a/django_stubs_ext-6.0.7-py3-none-any.whl", hash = "sha256:53a9c7c5a7c7e718cc6308cfce1e7470f2cac0b9d38dbcd60fbfa82704f1d592", size = 10362, upload-time = "2026-07-14T10:07:55.653Z" }, +] + +[[package]] +name = "gunicorn" +version = "26.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/b7/a4a3f632f823e432ce6bc65f62961b7980c898c77f075a2f7118cb3846fe/gunicorn-26.0.0.tar.gz", hash = "sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf", size = 727286, upload-time = "2026-05-05T06:38:25.529Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/40/9c2384fc2be4ad25dd4a49decd5ad9ea5a3639814c11bd40ab77cb9f0a14/gunicorn-26.0.0-py3-none-any.whl", hash = "sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc", size = 212009, upload-time = "2026-05-05T06:38:23.007Z" }, +] + +[[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 = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[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 = "isort" +version = "8.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/7c/ec4ab396d31b3b395e2e999c8f46dec78c5e29209fac49d1f4dace04041d/isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d", size = 769592, upload-time = "2026-02-28T10:08:20.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/95/c7c34aa53c16353c56d0b802fba48d5f5caa2cdee7958acbcb795c830416/isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75", size = 89733, upload-time = "2026-02-28T10:08:19.466Z" }, +] + +[[package]] +name = "librt" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/2f/3908645ddddab7120b46295e541ead308109fa48dbec7d67d7a778870d60/librt-0.13.0.tar.gz", hash = "sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781", size = 211402, upload-time = "2026-07-08T12:26:29.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/3b/18e7b63255297a2bdc9c25c8d6d4ca8eca9f63aceb1252c0f7427ac7099e/librt-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a468951af16155824e88bdd8326ebe5bdb371f3ec0ac04642994b98201d914f3", size = 151027, upload-time = "2026-07-08T12:25:19.638Z" }, + { url = "https://files.pythonhosted.org/packages/4d/68/e2248452c00d1a03b45fee1752cdc8f790a476efd2402b75181da88a9e61/librt-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ae01d8512cc17079e53425635327dbf3f7ff57a42c00dec348bf79791c56444c", size = 155152, upload-time = "2026-07-08T12:25:20.851Z" }, + { url = "https://files.pythonhosted.org/packages/0e/16/52b1c99bf19057a062aac39c900cbb81499f6f75d6c537c14463d247ba78/librt-0.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32c26893cd085c1efe83219e78d866da23fb20a066101b8f68210004361d224c", size = 502499, upload-time = "2026-07-08T12:25:22.055Z" }, + { url = "https://files.pythonhosted.org/packages/9f/54/b811151805c795f55e0dedee6ec687b75f9982a8105d240ea3910737a77b/librt-0.13.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5929da1981a46bcf4b28b1b9499905f0ff58e2419da402a048234e9783acbc4b", size = 496108, upload-time = "2026-07-08T12:25:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f8/094d6b2bd93f3fdaa54db54cc788c4a365333bddad65ab02e04da0b1d004/librt-0.13.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94b85d664d777bab6c0d709416cb42938251fda9e221b79e3a2215d85df5f4f9", size = 531576, upload-time = "2026-07-08T12:25:24.648Z" }, + { url = "https://files.pythonhosted.org/packages/2e/40/541733d5755824f968f7ec39d78ffbd75d145964157ae5e69a09ec6d7326/librt-0.13.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:531b2df3e9fe96b1fcf73a6d165921e4656be5f58d631d384ebce344298368db", size = 524390, upload-time = "2026-07-08T12:25:25.898Z" }, + { url = "https://files.pythonhosted.org/packages/c6/b5/255673cfdbf5ba663339d36cd863c897289ab4337577e19f9405ce059f36/librt-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:109b84a9edf69ad89dc1f66358659e14a031baca95e3e5b0060bd903ede8efd6", size = 543053, upload-time = "2026-07-08T12:25:27.436Z" }, + { url = "https://files.pythonhosted.org/packages/9e/11/ab5005e9c9850710f21e354201bf090646349d3fabf5f951eaf70235729e/librt-0.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1304368a3e7ffc3e9db986796cc5326fdb5943a3567ecc137cff318e4240c0e7", size = 546387, upload-time = "2026-07-08T12:25:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/a2/04/a5d7ce1d1df1afd15ca283dcdf7530ac073e12d69ae8c40879dda96f7868/librt-0.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e4f9b472e7d308d94b62c801982065661158c6ed02790d6c7ddb4337cea0f9c1", size = 535970, upload-time = "2026-07-08T12:25:30.171Z" }, + { url = "https://files.pythonhosted.org/packages/5a/76/927e267a6daa290174ac281b23c9804c8829b042ade9c6f24a065f540958/librt-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f836c37478f167a81200d8c8b2c920a22224564bed2c23d7aeec760965c367a", size = 573582, upload-time = "2026-07-08T12:25:31.507Z" }, + { url = "https://files.pythonhosted.org/packages/10/24/b6c5213efe39c19f9e13605644d0cf063b4ddaa33ac2e45b088e23a70e2e/librt-0.13.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:4000d961ff9598ac6ea603c6c836a5ed49bc205ade5fc378b998dfe1e2c36628", size = 82189, upload-time = "2026-07-08T12:25:32.675Z" }, + { url = "https://files.pythonhosted.org/packages/4c/00/d29736be177a906ac0b84a5b04b4fbfa22c776dc2f366de4172b0f968c08/librt-0.13.0-cp313-cp313-win32.whl", hash = "sha256:79e44cff71750d299d61a678e49995b0d5935a9cda238c2574daeca3ba536927", size = 106193, upload-time = "2026-07-08T12:25:33.692Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ac/aff6fb45393cb8912f39dfb156ef6b2d1cadb207ff465fc8f66141054be8/librt-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:54dab44a847d5ad1acd05c8a83fe518ae685516ecf4d3f7cc6e3df2a66767650", size = 126962, upload-time = "2026-07-08T12:25:34.769Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3a/d68cb2b334d53fd30fac81d3a489ce4ba0d9506f4df43fcf676b68352b19/librt-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:d4cb6fbfdf874340ab5e51450753c0f817b6958a3621125ee695bbc3de866566", size = 112127, upload-time = "2026-07-08T12:25:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/7b/66/f49ae0d592bd45b6941e9a8bafcb6a87cddcd501ee7874707e767f01b585/librt-0.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71", size = 149818, upload-time = "2026-07-08T12:25:37.203Z" }, + { url = "https://files.pythonhosted.org/packages/3d/50/51c76d74014d04fb95b6506d286808984b78a2f7a41039094e6b2194ac48/librt-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6", size = 154071, upload-time = "2026-07-08T12:25:39.399Z" }, + { url = "https://files.pythonhosted.org/packages/b8/fe/f19b0f5f82d5a1f2da736586bc840abd00ce07d6388136ae80b7333883fc/librt-0.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f", size = 494168, upload-time = "2026-07-08T12:25:40.641Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/b8550c75775127fd31a5f20e8775997f7b527ad661fc8ddccd7497c064f7/librt-0.13.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f19e181de5b3a1148bb3420b8c4b0b0ea0fce6950099724ad151d6cea5acc180", size = 491054, upload-time = "2026-07-08T12:25:41.905Z" }, + { url = "https://files.pythonhosted.org/packages/30/14/4d0204867623df3f33f86efd3d3692ba5e01321443f4d6eab35a22697618/librt-0.13.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22034924f5b42d5a56371cf271771bfeaabf235a7a8b6264bef2d20013f786c6", size = 523006, upload-time = "2026-07-08T12:25:43.327Z" }, + { url = "https://files.pythonhosted.org/packages/19/0a/c45fc9a260934696bace1ac5df1e148ac92bd71767aee3bf7cd7a4534f4c/librt-0.13.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7897db4e95e22468bdda33d8e012ceacd0182abf001e6389d763f0def6286b9", size = 515058, upload-time = "2026-07-08T12:25:44.541Z" }, + { url = "https://files.pythonhosted.org/packages/13/0a/50c5ce45b326854ef8fa6ae4c36cf5142e5c55315eaf9e51d0ae73ac4da3/librt-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1ce61b3746545029d4f5c17d6bd74b676254ad98433086c846ffb5e8fa73f007", size = 534025, upload-time = "2026-07-08T12:25:45.825Z" }, + { url = "https://files.pythonhosted.org/packages/89/2d/08c413c8f93fc13b8103624fce38e5caa86cd08cbbc8465870ab287af54b/librt-0.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:46c330e82565962c761dbce7941be2cff7db674ee807455a8d0cadc5f9b759b0", size = 540557, upload-time = "2026-07-08T12:25:47.059Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c1/93af71fb4a364952210051811dd4e40174e79656b050c89cacac18af3330/librt-0.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:375f5af8f99cbaa99dd293af986e3d57caabc9ba81a5d3f021603764854197a1", size = 523201, upload-time = "2026-07-08T12:25:48.392Z" }, + { url = "https://files.pythonhosted.org/packages/c1/6e/9766f07b676a4889d9f8bc2864e9ba5fff165653143ef4dda7df6aa34d16/librt-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9320d34c3376ae204b2cd176e8d4883a013934e0aef822f1aed9c536490c275d", size = 565740, upload-time = "2026-07-08T12:25:49.678Z" }, + { url = "https://files.pythonhosted.org/packages/a2/1e/664e3472ce2b6e10e9b83f29d4a36eb982ff6b5a169ae7567bba3a4c4ff5/librt-0.13.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9af313c66157a69dc69ea0059a66961692250e0dc95af9c385a48ffb770a0d16", size = 81611, upload-time = "2026-07-08T12:25:50.857Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d4/8582a4d65e2234673685e07309d02c230b28a85724eb0acbf13f019b7f6e/librt-0.13.0-cp314-cp314-win32.whl", hash = "sha256:f2a7253458e34f33543551394ae4fe104b497ec2a65ac266074de64c1df82e37", size = 100106, upload-time = "2026-07-08T12:25:52.03Z" }, + { url = "https://files.pythonhosted.org/packages/63/ce/0cb99efe6086b46cd985dc26672166fae312a239690e75871f7fafbd3fc5/librt-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:a3dfe4edf10e8ed7e55b026a8bfc2c2a8704218b659cd4bffdf604fab966dc39", size = 121209, upload-time = "2026-07-08T12:25:53.166Z" }, + { url = "https://files.pythonhosted.org/packages/26/85/4f3ccb083a3c9b0d42e223acdb3c3f507953324a59cdcab4826e8e2e3b89/librt-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:68a5faee4bba381cb93b5961f684a514cf0053cb92308ff9c792c2fea0b174c6", size = 106404, upload-time = "2026-07-08T12:25:54.253Z" }, + { url = "https://files.pythonhosted.org/packages/b2/77/333191499538c8e8189de7a4cba8e6f49ee949fd6d6e6324b21fd1522466/librt-0.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a38fb81d8376dfa2f8963b265fec07637802b0d01e2a127c19c66cb070fb24f5", size = 159231, upload-time = "2026-07-08T12:25:55.432Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9e/2aa83758f22c278b837a1d8025898434ce2b8bff36678d5330ecaef56dff/librt-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d4c8d9bd5abce34b2e75edb3bf37ab0f34e49b1f915a40ae8468eb7c85bc5b46", size = 161300, upload-time = "2026-07-08T12:25:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c0/86791e936553ca763d6b3c2fb4d31d596cd00e14fa631c283a40ba01559a/librt-0.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:387e2f1d27e89bffe0d3f520f0da0662c973fd607ca16c1808f8a5085419485e", size = 582056, upload-time = "2026-07-08T12:25:58.144Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d3/a9ec15984a185e000c4d2a16ba28bd623124ad4c38a10974c7ff78e3a893/librt-0.13.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:4f6db193d2e5e0ed60359b9a5a682cd67205d0d3b1e459a867dd4b5c4e7eaa7a", size = 562758, upload-time = "2026-07-08T12:25:59.544Z" }, + { url = "https://files.pythonhosted.org/packages/3c/af/dbe36b78b19c06a55097f99305e4ea9458e2273e6ae16a3cbecaad7ee978/librt-0.13.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d38604854e8d22faadf683ec6c02bb0f886e2ba56ef981a1c36ee275f21ea22", size = 602095, upload-time = "2026-07-08T12:26:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a8/2966891b4dd2830f5203fbee92ac2c4947653a2390ba73dfa44244fad025/librt-0.13.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:371f7ce73026815dafd51c50ce38416e91428b28c4b2ec97cd39271164b0045c", size = 593452, upload-time = "2026-07-08T12:26:02.352Z" }, + { url = "https://files.pythonhosted.org/packages/61/f5/4df8bfc8405ecf8c0d525b4d69636f694bdd8620b313ec8b76e54a5926cc/librt-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3aaedf52171bee90860704c560bc798fe83b76247df47568e0197e9b13c735a0", size = 623729, upload-time = "2026-07-08T12:26:04.294Z" }, + { url = "https://files.pythonhosted.org/packages/d6/13/9ac202dffc8db06f75d06c08c2f9f6ff054be67d21272dcc078fa1cc0c57/librt-0.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:96bad8725a4f196a798366c25ce075d1f7543a4ec045ffc13e6a7ec095cdab04", size = 617077, upload-time = "2026-07-08T12:26:05.845Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f0/ebe38610716aee5cb28efd95089bb90192096179802779381e1c5dcf239c/librt-0.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6bf6a559ffe4a93bbea6cf31ddf01a7fd9ba342ef51f27beb178e318b74acd61", size = 599561, upload-time = "2026-07-08T12:26:07.21Z" }, + { url = "https://files.pythonhosted.org/packages/4f/5c/c2e72e236fff7abc716d5b1753b8b8cd3ea85ac46fe17d2e7c51d4e1c723/librt-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:301067672387902c55f94b51d5022304b36c966ea9fe1f21caab99a9bef487c9", size = 645511, upload-time = "2026-07-08T12:26:08.562Z" }, + { url = "https://files.pythonhosted.org/packages/0c/99/6203ce619dee940d6bfbe099ec3fe4be00a68e9d60f70abf906cf124fe66/librt-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:5fdcf34f86de8fb66d7dc7589f96ba91c4aa46671200d400e6fd6f109a483f18", size = 104357, upload-time = "2026-07-08T12:26:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/52/dd/843b6314087c41657c7036d7914d8f294bdf9b580aa8513ea0588c8e9a3d/librt-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:260c33e92263fa629b4f6d3c51967a1c2158fe6c33237aaa3ebeac586b085259", size = 126998, upload-time = "2026-07-08T12:26:10.975Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/3dcec2884ba1b0806d1408612555c38dd5d68e90156b59f75f6e36435c3a/librt-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99", size = 110771, upload-time = "2026-07-08T12:26:12.303Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mypy" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/af/4e516a05d3ca2eb9283e9ec45b2c02225c1514dd6da49fd3c9eaa6639370/mypy-2.3.0.tar.gz", hash = "sha256:465965d41cd9a2726694e983e8ce7113259327bec798115d1e1dfa2a52fb666e", size = 3988104, upload-time = "2026-07-13T11:34:53.387Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/ae/f7d056eb0294586a572d0d0d89580ec633c064db520f11d37d5a2fb833bd/mypy-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:91ad22a52ae2c7e621c2f67c94d5a17f66b3209a4cff5cf8a573579835c69e97", size = 14947298, upload-time = "2026-07-13T11:27:47.734Z" }, + { url = "https://files.pythonhosted.org/packages/32/d5/db3e7af01e7844d21662c6ddc1f7825ec7cb4053f0391ac02faf3638396f/mypy-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:99ac767cc5d3b64c8d0ae226ead10c96694f94e4e7da1668642225dcd4e75aac", size = 13950768, upload-time = "2026-07-13T11:27:57.726Z" }, + { url = "https://files.pythonhosted.org/packages/d9/fb/43c031f0190513d1ec248ed037eceb742ddd2a4d74bbf406658a28173837/mypy-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6d2c484742a4d7b0ed6d07b143375624d3b899c5749c7b3c947f56261f48a6", size = 14151586, upload-time = "2026-07-13T11:29:18.615Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c3/f8b2ffc60883084da91be51af58e88a7ffd4ff9795acb7d902ff88d31eb1/mypy-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7da939dd335cfd2ad788bdfd081c9f4e47634ab995e5a45eb15fd1e5bc052f8b", size = 15227411, upload-time = "2026-07-13T11:30:29.904Z" }, + { url = "https://files.pythonhosted.org/packages/83/2e/16b917fc7adcf03f1aadddfc93aab804ffb234b1ab09c0ffd6d92a5d34a2/mypy-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7247eb2824f996722a949530183394921ca71deb9680052a338cf53cff7925c2", size = 15478790, upload-time = "2026-07-13T11:33:14.686Z" }, + { url = "https://files.pythonhosted.org/packages/c0/88/aaa65a93c73d0cdae7e42f8adb302bf6885bb281302084f99d0290a35347/mypy-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:75b0984bb3cbd76bb5c9291a8671f7ae66ca3b51c7584c358fc2e923259f0757", size = 11234919, upload-time = "2026-07-13T11:33:39.28Z" }, + { url = "https://files.pythonhosted.org/packages/35/19/b40de63f1a80e63bc2d40f0679a6a8dbd34e95176c8122119bdf406aa552/mypy-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:d78fcf900b59cb7e82cb7e3a235e31b462d9333d92285bd1e4952d355b8ffba1", size = 10201510, upload-time = "2026-07-13T11:31:52.619Z" }, + { url = "https://files.pythonhosted.org/packages/a4/58/fa0ae047da911f540284009b4f44b96fe09d83c076d7c103e9d645f46303/mypy-2.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ea317b060ce83e26050f8f9e4d7d6bf44ed7597c8ff9990bccffbb9d1d8522db", size = 14941909, upload-time = "2026-07-13T11:32:34.332Z" }, + { url = "https://files.pythonhosted.org/packages/15/14/2ba1d61452d7c2a7fe12741e8d374e52b183476b07aa7f9e2a0d02b0720a/mypy-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:094af99f92638aa92852326188b85a89e50f4a472f44827c03362228482f0762", size = 13967581, upload-time = "2026-07-13T11:30:00.587Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5a/483fb9e5ffbbb1a28dccc7b0a13d141b17ac769b6c9f488c0a0c63698962/mypy-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de121747278144fc9ae7caa2e978cf5df12aebc82933182f5b3b86081a30baef", size = 14168807, upload-time = "2026-07-13T11:28:48.6Z" }, + { url = "https://files.pythonhosted.org/packages/ae/77/70d7a10732063beb74ad713682cf871e88f5c5fa39bfc8beff8a524bf9cb/mypy-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37fa4de896a84e2dc9200d91e614c22563b43d1a266789d4bbac7b22ebe6192b", size = 15200144, upload-time = "2026-07-13T11:31:25.283Z" }, + { url = "https://files.pythonhosted.org/packages/56/72/766218ac783be4fdfcd699b90037b63017348a3e86fb2c1fbfb18302637d/mypy-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f1b3a98dfd21058bc759bb3337d5d1f61d0fdf9f3cf9c00f4291790fb5427bff", size = 15460389, upload-time = "2026-07-13T11:29:29.077Z" }, + { url = "https://files.pythonhosted.org/packages/38/4e/8a9db7411ecb8ec0cb1fd05dba432f28bafffcd38b4e887714a4a0506689/mypy-2.3.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:944c665d984157cb96a679dfb7a4a81dd1d36b24b9c284b699514e6e626b82d4", size = 7753664, upload-time = "2026-07-13T11:29:08.147Z" }, + { url = "https://files.pythonhosted.org/packages/65/4c/c3f8bfd6ed0e5e38b5a244403b27f821d433443df5a15a278417c10a3a3c/mypy-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:4359424140d985192c778c1ce2c114a10c1ca58a381ed79cfa70d37df94b299f", size = 11417237, upload-time = "2026-07-13T11:33:47.467Z" }, + { url = "https://files.pythonhosted.org/packages/3c/00/89a32eaf5ccf174bc4f90db0eaea5d70636c01b8d49f384bdab2e8834390/mypy-2.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:3dd0bed92c4bdec57c42505b96416fb9e6a5aa7be84d2809bcd5f2ecec2860d7", size = 10389252, upload-time = "2026-07-13T11:31:43.81Z" }, + { url = "https://files.pythonhosted.org/packages/31/56/104f93d69aa9f339b6b9d3b0a7faa699b8b466c942cf3ae86cc2a2ec0915/mypy-2.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:691fdc37132b1ae628d834f672e74de83462d9fb4aff621835767fb43a8dd373", size = 16385495, upload-time = "2026-07-13T11:29:49.818Z" }, + { url = "https://files.pythonhosted.org/packages/d2/03/f1d2123313f55efafdd27706960f43a771c62f1b68426c76043f3ab9ebf3/mypy-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:aec15d465d477558fd842757b487849007311cf3897849cdda0e3162ac0ac556", size = 15098155, upload-time = "2026-07-13T11:30:40.301Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5d/d5f9200399b445e81726c4f23becee33f233aee81c72680b1ef3a258b641/mypy-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b352b7e49f5e6576009e8df730e1ff4f915cb565b851b396d2ffe2f5a6f5da88", size = 15514155, upload-time = "2026-07-13T11:34:38.569Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ce/69977c555f08faa3190cfde44189b89dbd56861b1ab97aa18fc5f3a2e4a3/mypy-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c6c6bf687b17f90dbfcad95b960d32eaa0154c00da45f03ab50bf8952e047fe", size = 16766351, upload-time = "2026-07-13T11:33:29.195Z" }, + { url = "https://files.pythonhosted.org/packages/bc/92/6648b6caa3ab9e00f9ac0c2a78307805f873dd48139b24a6f6f7c3667bbf/mypy-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f4ed18f111bfe2d599bca7468e7f9251042c1c2118f762c8de2766a56d773c60", size = 17043490, upload-time = "2026-07-13T11:30:53.927Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ab/0dc91d80f3f016634c68d451f294a97320fe903a9b6f90b9e57b3f7f1717/mypy-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0b025a93cffb9781d231f232be07a17912f35f10a313c24f301c81e842870654", size = 12146869, upload-time = "2026-07-13T11:29:38.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/b5/4c964d02634ba81f4d1c84838e5c5b18ab06d13ed568960f5d6318495ccc/mypy-2.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:adebc76aab4f3495a88b41d48aa4aff0c03f2822501da76625afcca5975f19e5", size = 10965113, upload-time = "2026-07-13T11:28:07.056Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fa/fdc54fe583ba3cafbcedfb70eeeaf03849f75b1827a07096c7bd996f582d/mypy-2.3.0-py3-none-any.whl", hash = "sha256:6b1cdb579446b60432432b2b2403a6201b4b475a004d7f488511c9ba177c9e88", size = 2753292, upload-time = "2026-07-13T11:33:18.48Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[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 = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/cd/4f25b2f95b23f5d2c9c1fe43e49841bff5800562149b2666afc09309aa8f/platformdirs-4.10.1.tar.gz", hash = "sha256:ceab4084426fe6319ce18e86deada8ab1b7487c7aee7040c55e277c9ae793695", size = 31678, upload-time = "2026-07-18T03:53:43.808Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/73/6fd0bb9ce84138c3857f12e9de63bc901852975a092d545f18087a204aa2/platformdirs-4.10.1-py3-none-any.whl", hash = "sha256:0e4eff26be2d75293977f7cddc153fd9b8eaa7fb0c7b64ffe4076cb443117443", size = 22906, upload-time = "2026-07-18T03:53:42.576Z" }, +] + +[[package]] +name = "psycopg" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/2f/cb91e5502ec9de1de6f1b76cfbf69531932725361168bb06963620c77e2e/psycopg-3.3.4.tar.gz", hash = "sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc", size = 165799, upload-time = "2026-05-01T23:31:55.179Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl", hash = "sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a", size = 213001, upload-time = "2026-05-01T23:20:50.816Z" }, +] + +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/43/13e9c406fbbf354580476e248a16b64802a376873ebe6339e30bb655572d/psycopg_binary-3.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fbd1d4ed566895ad2d3bf4ddfd8bae90026930ddf29df3b9d91d32c8c47866a7", size = 4590377, upload-time = "2026-05-01T23:29:18.782Z" }, + { url = "https://files.pythonhosted.org/packages/22/be/2923cd7c3683e7afdecf4f10796a18de02f5c5ddc0969aa2ad0a8cdd3bbd/psycopg_binary-3.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:75a9067e236f9b9ae3535b66fe99bddb33d39c0de10112e49b9ab11eee53dc31", size = 4669023, upload-time = "2026-05-01T23:29:25.884Z" }, + { url = "https://files.pythonhosted.org/packages/96/a0/2c913d6fe13d6a8bd13597d36739bf47af063ad9399e402cfecab16f3c1e/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b56b603ebcea8aa10b46228b8410ba7f13e7c2ee54389d4d9be0927fd8ce2a70", size = 5467423, upload-time = "2026-05-01T23:29:33.416Z" }, + { url = "https://files.pythonhosted.org/packages/e7/38/205d10bc1ad0df4a21c5c51659126bd3ea0ef98fcad1e852f78c249bb9c3/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c677c4ad433cb7150c8cd304a0769ae3bcfbe5ea0676eb53faa7b1443b16d0d3", size = 5151137, upload-time = "2026-05-01T23:29:42.013Z" }, + { url = "https://files.pythonhosted.org/packages/36/fc/f0381ddcd45eff3bb70dbca6823a996048d7f507b2ec3fc92c6fabc0fe87/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26df2717e59c0473e4465a97dfb1b7afebaa479277870fd5784d1436470db47c", size = 6736671, upload-time = "2026-05-01T23:29:51.626Z" }, + { url = "https://files.pythonhosted.org/packages/95/40/fa545ae152c24327651e5624e4902121e808270be36c10b12e9939be09bc/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dc1f79fd16bb1f3f4421417a514607539f17804d95c7ed617265369d1981cae", size = 4979601, upload-time = "2026-05-01T23:29:56.961Z" }, + { url = "https://files.pythonhosted.org/packages/86/e4/2f8a47ee97f90cd2b933d0463081d35631ff419de2b8c984a5f369857de0/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:136f199a407b5348b9b857c504aff60c77622a28482e7195839ce1b51238c4cc", size = 4510513, upload-time = "2026-05-01T23:30:07.243Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0e/94e842ff4a7f98ed162580ca2e8b8864b28c1e0350f2443f8ee47f821167/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b6f5a29e9c775b9f12a1a717aa7a2c80f9e1db6f27ba44a5b59c80ac61d2ffcf", size = 4187243, upload-time = "2026-05-01T23:30:15.352Z" }, + { url = "https://files.pythonhosted.org/packages/d0/83/fc6c174b672e29b7de996ea77b6cbddf46c891751c3355f6974292baa6b4/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ee17a2cf4943cde261adfad1bbc5bf38d6b3776d7afff74c7cabcbeaeb08c260", size = 3927347, upload-time = "2026-05-01T23:30:21.186Z" }, + { url = "https://files.pythonhosted.org/packages/e9/65/768364d4a97a15b1a7f47ba52688c1686f22941d8332a8398cefc468e25f/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c4ab71be17bdca30cb34c34c4e1496e2f5d6f20c199c12bad226070b22ef9bf", size = 4236393, upload-time = "2026-05-01T23:30:26.211Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/218efbc9e645becd80cdf651acda05f85cfe546b7a9c0458c7cbc8fe1f74/psycopg_binary-3.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:dbfdb9b6cc79f31104a7b162a2b921b765fcc62af6c00540a167a8de47e4ed38", size = 3564592, upload-time = "2026-05-01T23:30:31.764Z" }, + { url = "https://files.pythonhosted.org/packages/48/a6/828c9185701dab71b234c2a76c38a08b098ebfec5020716b4e93807492b5/psycopg_binary-3.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:28b7398fdd19db3232c884fb24550bdfe951221f510e195e233299e4c9b78f97", size = 4607292, upload-time = "2026-05-01T23:30:38.962Z" }, + { url = "https://files.pythonhosted.org/packages/92/58/5b40dbc9d839045c9dae956960e4fb6d20bcabe6c59a2aa34fc3a371913f/psycopg_binary-3.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1fbaa292a3c8bb61b45df1ad3da1908ccee7cb889db9425e3557d9e34e2a4829", size = 4687023, upload-time = "2026-05-01T23:30:47.227Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/793f0ac107a9003b48441d0d1f9f616d96e0f37458dd8dc12528ceff55fb/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94596f9e7633ee3f6440711d43bb70aa31cc0a46a900ab8b4201a366ace5c9e7", size = 5486985, upload-time = "2026-05-01T23:30:55.517Z" }, + { url = "https://files.pythonhosted.org/packages/8f/26/42e8533497e2592334f68ec529cf5f840f7fa4e99575a4bb61aa184dbfbf/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8c0056529e68dbe9184cd4019a1f3d8f3a4ead2f6fc7a5afcf27d3314edd1277", size = 5168745, upload-time = "2026-05-01T23:31:01.904Z" }, + { url = "https://files.pythonhosted.org/packages/15/af/b7151776cc08d5935d45c833ec818a9beb417cf7c08239af1aafbdae78ee/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c09aad7051326e7603c14e50636db9c01f78272dc54b3accff03d46370461e6", size = 6761486, upload-time = "2026-05-01T23:31:14.511Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ed/c92533b9124712d592cbf1cd6c76da933a2e0acea81dfe1fbe7e735f0cff/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:514404ed543efd620c85602b747df2a23cf1241b4067199e1a66f2d2757aaa41", size = 4997427, upload-time = "2026-05-01T23:31:20.901Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/ccadfd0de416aa188356daa199453af24087b042e296088706d190ae0295/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:46893c26858be12cc49ca4226ed6a60b4bfccadd946b3bebb783a60b38788228", size = 4533549, upload-time = "2026-05-01T23:31:26.204Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a0/c8f43cee36386f7bc891ab41a9d31ea07cf9826038e732da79f26b1e5f34/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:df1d567fc430f6df15c9fcf67d87685fc49bdb325adc0db5af1adfb2f44eb5c9", size = 4210256, upload-time = "2026-05-01T23:31:33.884Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2c/c1547871be3790676e8868b38655496422f94f0978dfb66b74bdba2f1676/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6b9016b1714da4dd5ecaaa75b82098aa5a0b87854ce9b092e21c27c4ae23e014", size = 3946204, upload-time = "2026-05-01T23:31:39.626Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b1/f6670f00fa7ea601584623f6c11602ab92117d83eaff885e0210f6de7418/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:47c656a8a7ba6eb0cff1801a4caaa9c8bdc12d03080e273aff1c8ac39971a77e", size = 4255811, upload-time = "2026-05-01T23:31:44.986Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e6/5fff07a70d1f945ed90ae131c3bd76cab32beff7c58c6db15ad5820b6d1f/psycopg_binary-3.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:c37e024c07308cd06cf3ec51bfd0e7f6157585a4d84d1bce4a7f5f7913719bf8", size = 3666849, upload-time = "2026-05-01T23:31:51.165Z" }, +] + +[[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 = "python-decouple" +version = "3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/97/373dcd5844ec0ea5893e13c39a2c67e7537987ad8de3842fe078db4582fa/python-decouple-3.8.tar.gz", hash = "sha256:ba6e2657d4f376ecc46f77a3a615e058d93ba5e465c01bbe57289bfb7cce680f", size = 9612, upload-time = "2023-03-01T19:38:38.143Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/d4/9193206c4563ec771faf2ccf54815ca7918529fe81f6adb22ee6d0e06622/python_decouple-3.8-py3-none-any.whl", hash = "sha256:d0d45340815b25f4de59c974b855bb38d03151d81b037d9e3f463b0c9f8cbd66", size = 9947, upload-time = "2023-03-01T19:38:36.015Z" }, +] + +[[package]] +name = "pytokens" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, + { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, +] + +[[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/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { 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 = "red-api" +version = "0.0.1" +source = { virtual = "." } +dependencies = [ + { name = "django" }, + { name = "django-redis" }, + { name = "gunicorn" }, + { name = "httpx" }, + { name = "psycopg", extra = ["binary"] }, + { name = "python-decouple" }, + { name = "xmltodict" }, +] + +[package.dev-dependencies] +dev = [ + { name = "bandit" }, + { name = "black" }, + { name = "coverage" }, + { name = "django-stubs" }, + { name = "isort" }, + { name = "mypy" }, + { name = "ruff" }, + { name = "tombi" }, + { name = "toml-run" }, + { name = "ty" }, + { name = "types-requests" }, + { name = "yamllint" }, +] + +[package.metadata] +requires-dist = [ + { name = "django", specifier = ">=5,<6" }, + { name = "django-redis" }, + { name = "gunicorn" }, + { name = "httpx" }, + { name = "psycopg", extras = ["binary"] }, + { name = "python-decouple" }, + { name = "xmltodict" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "bandit" }, + { name = "black" }, + { name = "coverage" }, + { name = "django-stubs", specifier = ">=6.0.7" }, + { name = "isort" }, + { name = "mypy" }, + { name = "ruff" }, + { name = "tombi" }, + { name = "toml-run" }, + { name = "ty" }, + { name = "types-requests" }, + { name = "yamllint" }, +] + +[[package]] +name = "redis" +version = "8.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/c3/928b290c2c0ca99ab96eea5b4ff8f30be8112b075301a7d3ba214a3c8c12/redis-8.0.1.tar.gz", hash = "sha256:afc5a7a2f5a084f5b1880dec548dd45be17db7e43c82a30d84f952aefb05cfb0", size = 5114170, upload-time = "2026-06-23T14:52:37.728Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/0a/c2345ebf1ebe70840ce3f6c6ee612f8fa749cfbd1b03069c53bf0c62aaad/redis-8.0.1-py3-none-any.whl", hash = "sha256:47daa35a058c23468d6437f17a8c76882cb316b838ef763036af99b96cedd743", size = 502406, upload-time = "2026-06-23T14:52:36.137Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/06/ae069393fc66e8ff33036d4b368003833bf6e88ccf182e17e7a2f1c754fd/ruff-0.15.22.tar.gz", hash = "sha256:3f15175b1fb580126f58285a5dae6b2ea89000136d980c64499211f116b54809", size = 4785063, upload-time = "2026-07-16T15:14:13.244Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/18/ee54b7ae1e121be7a28ea6da4b67564ebb0530e183a54415ab7e3bcd2c4e/ruff-0.15.22-py3-none-linux_armv6l.whl", hash = "sha256:44423e73493737f5e7c5b41d475483898ff37afcdae38bc3da5085e29af1c2d8", size = 10781258, upload-time = "2026-07-16T15:13:19.452Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d2/2520cb14761ddbeaf57642a76942fc36adcbdbe53b4532241995f6fc485c/ruff-0.15.22-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b82c6482946e9eda7ff2e091d25b8bad3f718684e1916d41bd56873cee05b697", size = 10999477, upload-time = "2026-07-16T15:13:23.318Z" }, + { url = "https://files.pythonhosted.org/packages/c9/10/74e53572aa758dfaa678c2a2646b5c5515d884b7ca56be4d2ce03ca4b560/ruff-0.15.22-py3-none-macosx_11_0_arm64.whl", hash = "sha256:11c1c715af53a09f714e011106bffc419751ec8232fcb5da42173284ea3fec6f", size = 10466716, upload-time = "2026-07-16T15:13:26.162Z" }, + { url = "https://files.pythonhosted.org/packages/1e/cc/44eaaf0844e028182f2d0a8f2190d0f359159aed0a9e5ab861d892f1ae2a/ruff-0.15.22-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:742a29cf29bddb7c8327895d6a10e0e6c5b38a96dd407af9b5d0857f809c0576", size = 10892644, upload-time = "2026-07-16T15:13:29.229Z" }, + { url = "https://files.pythonhosted.org/packages/9f/21/8edf559014d2b0f82beea19cfb713993ad802ccda16868769979c6090a84/ruff-0.15.22-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72af58b951b0ae395935ae79763dc349bc0eb706319d28f7a33ad2cfb3cfc178", size = 10576719, upload-time = "2026-07-16T15:13:32.35Z" }, + { url = "https://files.pythonhosted.org/packages/bf/1e/3a13abd392a3b50b62e5938a831f9ab6e588358cacad5c18545b716d2182/ruff-0.15.22-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d425005c1835eb24e2ee4161cb90e8db263415f4a71c8c72c33abaa6c0c224", size = 11376494, upload-time = "2026-07-16T15:13:35.958Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3e/422d3d95bcf04dd78e1aeac22184d4f9a8fb2c01865d39d44618484a0317/ruff-0.15.22-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b9b3f8779a4f08c969defc3c8c35abffaa757e601ed5ae66d6d1db6519969a", size = 12208370, upload-time = "2026-07-16T15:13:39.185Z" }, + { url = "https://files.pythonhosted.org/packages/1e/91/5d065a0e0a02bf4813f5119ad278462eed081d2b832eb7c021ade0ec9e65/ruff-0.15.22-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e0dd1b2e4d3d585f897a0d137cbf4eaf6223bef4e8ce34d6bb12556c5f9249e", size = 11581098, upload-time = "2026-07-16T15:13:42.132Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f9/a0d4871d12fae702eb1f41b686caf05f1f8b124dc6db6f784f53d74918fa/ruff-0.15.22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365523eb91d9224e1bcb03b022fbf0facb8f9e23792a2c53d9d4b3924bdbdebb", size = 11399422, upload-time = "2026-07-16T15:13:45.2Z" }, + { url = "https://files.pythonhosted.org/packages/18/80/c843a5176cddbceb0b7e8dd41cf9993490796c1c469348d384f5a5c13c56/ruff-0.15.22-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fabfd168afdf29fee5be98b831efa9683c94d7c5a3b58b9ce5a2e38444589a74", size = 11381683, upload-time = "2026-07-16T15:13:48.46Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/8485de0ae92239438a36cfc51350db9b9e85c9ebdfaea91b18e422706662/ruff-0.15.22-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:225dbf095a87f1d9f90f5fd7924d2613ee452a75a4308c63a8f50f761787aa7c", size = 10850295, upload-time = "2026-07-16T15:13:51.655Z" }, + { url = "https://files.pythonhosted.org/packages/fa/91/24977ec2ec72eaf15e4394ace2959fdff2dd1e14f03e005e838023407169/ruff-0.15.22-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1877d63b9d24ed278744f1523fd11b85540566d54641f97c566d7d9dc5ca5296", size = 10579640, upload-time = "2026-07-16T15:13:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/9c/47/9b51216951974df1f263ac19da550d34252e0ed7218c25f10c5ef9ed7517/ruff-0.15.22-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a1606c510bd7215680d32efab38965f7cdec3ef69f5170a3f4791404ffdd5262", size = 11105077, upload-time = "2026-07-16T15:13:57.915Z" }, + { url = "https://files.pythonhosted.org/packages/c2/47/20e9d4a3b8016778acea5fc32bb50d35d207500a17ddb529ffa6996feef8/ruff-0.15.22-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:630479b18625f5ffc373f77603a22a9f8ac0acd7ff0501178b5db28ec71e9c64", size = 11490980, upload-time = "2026-07-16T15:14:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/4d/76/3f72d8fc38c1cb77b38c56a70da9d0c17700cc1cc50f9649c9d3c8f5ba71/ruff-0.15.22-py3-none-win32.whl", hash = "sha256:e5ba0e4a13fd14abbed2a77b517a3911290c6c6c59ef67784328d1668fab76cf", size = 10789165, upload-time = "2026-07-16T15:14:04.16Z" }, + { url = "https://files.pythonhosted.org/packages/cb/46/4965251734c2b6fcdca1b1b187d20bcac3af0ee5b083b89c910bb961ce3a/ruff-0.15.22-py3-none-win_amd64.whl", hash = "sha256:9be63ba1eb936acd2d1342fb8337c356353706fce233b2a15a09a97037e6acde", size = 11938297, upload-time = "2026-07-16T15:14:07.316Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/e69b1ff4c8b69093ef08b8919ab767af0569666865b39c30a8795d88d3c6/ruff-0.15.22-py3-none-win_arm64.whl", hash = "sha256:e1168075b72158510839f250027659cdd78476f40507dd517892304c41318661", size = 11298172, upload-time = "2026-07-16T15:14:10.51Z" }, +] + +[[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 = "stevedore" +version = "5.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/dd/04d56c2a5232358df41f3d0f0e31833d378b6c8ed7803a6b1b7867b0eba6/stevedore-5.9.0.tar.gz", hash = "sha256:abbd0af7a38a8bbb1d6adea2e35b17609cf004eaac323e88a8d8963640dd2b3c", size = 514850, upload-time = "2026-07-02T11:38:08.509Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/8d/008761f6e1000600e5303db30d05724bdcf3d2d186cbb59fac79b52e39ed/stevedore-5.9.0-py3-none-any.whl", hash = "sha256:e520945d4c257700eddc1eb1d79df04b2ea578eef185e0e3fa5b442fc848d3f7", size = 54463, upload-time = "2026-07-02T11:38:07.43Z" }, +] + +[[package]] +name = "tombi" +version = "1.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/e9/c38bd38f536ef83c0d682239c97acca7b21ddc2ca8e42976dd884c244641/tombi-1.2.4.tar.gz", hash = "sha256:a015b7fa9958b1f6fdc939295a5cbbe9f3c27af6e6e33da6ea132cd7db32fdb0", size = 701730, upload-time = "2026-07-19T11:11:55.852Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/8b/73de3405693d225758e9f53cd79d8854c6cfffabb3430f4759ce962cf317/tombi-1.2.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6d15e379ff60e9c116c57e97635df3f119fd7ca83768ef0abc97cde6b52fb708", size = 8948250, upload-time = "2026-07-19T11:11:28.349Z" }, + { url = "https://files.pythonhosted.org/packages/cf/15/6a75f658de6a20e61c32714ecd4f6fece135d4b50f6277e95fc7cbba7125/tombi-1.2.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e453aca87eb19033e22c1f45b8f89d8c24190c3ecb7bb4eac7e12a8b7f2eb2aa", size = 8482376, upload-time = "2026-07-19T11:11:30.512Z" }, + { url = "https://files.pythonhosted.org/packages/da/51/571e3e0454a03a83d4fa7fe3069c7bdcf951f3937f5056c8021ded39cdc7/tombi-1.2.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b74a9318d9b8c1ca87267eee764e4501474fe5d708174066f8138a89743b5513", size = 8966710, upload-time = "2026-07-19T11:11:32.272Z" }, + { url = "https://files.pythonhosted.org/packages/15/11/4dacb1d6be51cae0f5f8a3a962418a41baf74592b60e3f647755f36ec603/tombi-1.2.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db24b0554d9af3d71f77333bafa996e91f7064dbfb648d3f7017f86f83881b02", size = 9346516, upload-time = "2026-07-19T11:11:34.161Z" }, + { url = "https://files.pythonhosted.org/packages/c0/47/f26c42fefa363c6837e929f47ebc119a695cb9c96ad92480183465644c19/tombi-1.2.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:030c4d04a9cdb966be106b890cd096affb7d485225aaa4a72d4bf76ea4ba9435", size = 9607323, upload-time = "2026-07-19T11:11:35.982Z" }, + { url = "https://files.pythonhosted.org/packages/11/d0/fbf4608c6014e023611a49d82e850b786cfe3ef47138ea2597b946f9bed1/tombi-1.2.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9d7e741934bd7ad7a85d7436c2e72e0d8f714a425345e04e76ebb709b37399e6", size = 9496941, upload-time = "2026-07-19T11:11:37.793Z" }, + { url = "https://files.pythonhosted.org/packages/e6/4c/16c5331b20970eb14d2051cba5712765932bcabde281007280b28e8a8a63/tombi-1.2.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c325468f9b3a2e30b92a20bd4d34ef8122d207de24db17f6b25c24be18d7abc4", size = 9246516, upload-time = "2026-07-19T11:11:39.676Z" }, + { url = "https://files.pythonhosted.org/packages/20/78/e78f6010c068427d0e87b00085e169e7410f1a7594783ec08371a5e38188/tombi-1.2.4-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:f4d6d286bfd854060e315fdfad2ebed348cdcc6bf838fab69e3dff50558c4311", size = 8670316, upload-time = "2026-07-19T11:11:41.96Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f9/3c39bbebf0be11b54ffe7ae5c1a7e42fd9911cc9651c93533e0d54454469/tombi-1.2.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8e86e366fd63702b2db422bb78c06b6550d995ebe22347b4796158fa653c79e3", size = 8697669, upload-time = "2026-07-19T11:11:43.928Z" }, + { url = "https://files.pythonhosted.org/packages/d9/6b/cd393581d9a8fc4e377afd680b9e562385e42fd37210994ce9f32b877cca/tombi-1.2.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:cad2405a16c47a912698d81282b85dafcddaa009f1c4480dbebaeadbc5e06e66", size = 8982691, upload-time = "2026-07-19T11:11:45.779Z" }, + { url = "https://files.pythonhosted.org/packages/4d/f6/3ed583bd3c8c56d24f3971efc5228b7b0fef438d77b518b7431cf7c83c4a/tombi-1.2.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:490999cc2f9d2874ce3128032c9a5b71b371ab605165b929ee70a9b1f4d69c43", size = 9163387, upload-time = "2026-07-19T11:11:47.76Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d3/f2caf15bf32e426f4fb0e7fb6f7a0e135e22ebdcd969e28293ce7358696e/tombi-1.2.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:98aa3229a83e90c503ea6e70d28afb54de35895bdbc820e57a8783f21a8adef9", size = 9335342, upload-time = "2026-07-19T11:11:49.812Z" }, + { url = "https://files.pythonhosted.org/packages/f7/e1/daff9eca5a2d2b7d5eb4e7df523a85a0c44b9937ffd71a81a2f1e83675f8/tombi-1.2.4-py3-none-win32.whl", hash = "sha256:c89a1a451363f0d29d2d6993282d50df21181a1c283ea001618ea38fb700e814", size = 8974400, upload-time = "2026-07-19T11:11:51.761Z" }, + { url = "https://files.pythonhosted.org/packages/14/b2/34063aeac0a77685e5f951d038cf557994ed7c78dcea4a6bc62e64dae250/tombi-1.2.4-py3-none-win_amd64.whl", hash = "sha256:a60d363fe07b38f1695cad65572ba9d0b10dd2ad4a34a67299f8ccb384e1e9ab", size = 10353507, upload-time = "2026-07-19T11:11:53.991Z" }, +] + +[[package]] +name = "toml-run" +version = "0.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/9f/cc38ec7c7b94fbba9c50d9e1b0b7f0c08700a58d054ef8ad996d20b66415/toml_run-0.0.2.tar.gz", hash = "sha256:f94ef95267c2aee45c12121993b0a85cdd614c60a54e90542753226ed41d8b86", size = 7479, upload-time = "2025-12-22T00:21:40.574Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/94/90edff2fdd55d29e5bec9633b6783017815ede6390278d260b219adbcffb/toml_run-0.0.2-py3-none-any.whl", hash = "sha256:0d473dfa384c2ce73e45a1152e20a4c75ce9f1dbff4de5ef5dd0a77d36c8032a", size = 7694, upload-time = "2025-12-22T00:21:39.194Z" }, +] + +[[package]] +name = "ty" +version = "0.0.61" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/63/6944925d0fe9a4bb9cc744e6c045a42bbd2ee4654c103190674577a36c3f/ty-0.0.61.tar.gz", hash = "sha256:acbf0d914cc7e2e57ccc440036af36114819e2a604a5ffb554e72e4ca7dd65a2", size = 6234957, upload-time = "2026-07-18T01:39:54.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/cf/044f31523e2768e3e64b0ca2ec32f70b3a731d4a2caa6ea110baf26e251c/ty-0.0.61-py3-none-linux_armv6l.whl", hash = "sha256:148779b8675eac93f40ec58bd70037fe67537117f20a23272264f8f136d41336", size = 11891448, upload-time = "2026-07-18T01:39:18.449Z" }, + { url = "https://files.pythonhosted.org/packages/d2/55/558cfe76b65d91d1854bbfac336020bd42fd887caa632d845d13c0c539eb/ty-0.0.61-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:08217382b3385808ee7288501ea3214b32631b08d1fd091ece6799b0c95264c5", size = 11602442, upload-time = "2026-07-18T01:39:20.914Z" }, + { url = "https://files.pythonhosted.org/packages/27/be/78c0ae6634cd606a68e5b46b338db427a48a1800c96a749b2d2f7a702e03/ty-0.0.61-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6d99c729011b47dec20e78a32ac9c8f6defd4cf62f7bb851bbccf70dde6cee50", size = 11125286, upload-time = "2026-07-18T01:39:22.893Z" }, + { url = "https://files.pythonhosted.org/packages/a4/18/a40793962f1b6337938ddb0bca7496b54e70879e23b4d2cc8dfd7e5d1af3/ty-0.0.61-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cda607978ae271b77e51c947663218bce635c3507e256865444b10c37cdb60d", size = 11663403, upload-time = "2026-07-18T01:39:25.017Z" }, + { url = "https://files.pythonhosted.org/packages/98/c1/7879244da5b30407dc368946d36be5024380073408b079f144ffe034030e/ty-0.0.61-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0d78f160a0f9434d570cdcdbc4dafba1f6aac3c47a32f9f63995b3cb55ffe4b6", size = 11715250, upload-time = "2026-07-18T01:39:27.045Z" }, + { url = "https://files.pythonhosted.org/packages/35/c4/8a4637cd58abd37f315dd515e24c582986cb1bfdf2edc4786882f5a4f69a/ty-0.0.61-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09aeab4800b36e93e4ce918699004da642d74988cac920b7592a6a2b9be6611c", size = 12393876, upload-time = "2026-07-18T01:39:29.197Z" }, + { url = "https://files.pythonhosted.org/packages/27/4b/27e7c640b1272743503229aa17ae2167a538040c4716a2fa1777c2b34fea/ty-0.0.61-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dccc8136df44142a109953a168be17b4915c99876b047d0b6672c31dae939bdf", size = 12958187, upload-time = "2026-07-18T01:39:31.308Z" }, + { url = "https://files.pythonhosted.org/packages/3a/f5/70eaaefb6081fb0a8115cff66fbfaa20dafac8c646df2477adad95a59de2/ty-0.0.61-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:220760c2d13a887d027ee1093172c24ac35b6e634805329c93a30908ae4d3f5c", size = 12560101, upload-time = "2026-07-18T01:39:33.35Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/17bae3b6429b5c479dc6c1e344d34e1f79efbc27531f15f3ee5b5da63745/ty-0.0.61-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:effefbb89da7128d18059529d1c2ea390fe7f1f3882690d257ca2143d49a0c34", size = 12225389, upload-time = "2026-07-18T01:39:35.436Z" }, + { url = "https://files.pythonhosted.org/packages/d2/0e/2ac380ba20d6395542c8df1d6fa4f00e2aead784c2e6aaefa1e02ed0610c/ty-0.0.61-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ba8b28a5ef811d5bb6461e37d76110c06fd20487474865c323d3d18b08b972b2", size = 12548403, upload-time = "2026-07-18T01:39:37.556Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e5/7da4b73e825e1a9808c26d68b0156e9a37aede1846191210dfffb8c64042/ty-0.0.61-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:88ecd6d9b05e8174b1860dac9bd3e188d6cef5702b0d3239fd9f94f6ac73a29d", size = 11621813, upload-time = "2026-07-18T01:39:39.919Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3c/5b58015e998cd0d89b17a463b6321421457d86d987574e8dac65ddfceba3/ty-0.0.61-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fb0cdfe4c48542ffb9a1139825dfa3d4aae49e96e966682ef7da762ab97831ff", size = 11734101, upload-time = "2026-07-18T01:39:42.097Z" }, + { url = "https://files.pythonhosted.org/packages/a6/21/294f4cc819b7b12ed659fd860e5cdfbd592d4c768c8f23596685dbc43e6b/ty-0.0.61-py3-none-musllinux_1_2_i686.whl", hash = "sha256:dff03873c0c3d0b44738f8b6d403b0756a31cf54c65136397df7624c6159b1f0", size = 11988401, upload-time = "2026-07-18T01:39:44.183Z" }, + { url = "https://files.pythonhosted.org/packages/2e/26/0f96f79fdac118521a9771e9eef3f9b3f447d647b2c77953e80a1715c7e8/ty-0.0.61-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a9210e80e3d41c1dfc751e9e8e0980272f475031fafd0fb0f48aee233c78da03", size = 12330624, upload-time = "2026-07-18T01:39:46.662Z" }, + { url = "https://files.pythonhosted.org/packages/e3/08/1e62d1bca5c0cebdc7a34db1f4b61557aab85961cedd56953dd2c32d3e66/ty-0.0.61-py3-none-win32.whl", hash = "sha256:e3e1fe06f49a5492a922a5df2739834aa5ee978c7dd10414119dc8755cc40c9c", size = 11313991, upload-time = "2026-07-18T01:39:48.761Z" }, + { url = "https://files.pythonhosted.org/packages/26/f1/d8e33b3aeb36b73d81ae34d10e46ec4abf506d68f4e0a1491a76a593dd42/ty-0.0.61-py3-none-win_amd64.whl", hash = "sha256:25f2291169e0298fcdbba1b1fea64f8207a6c1908dddef32346fd5e3e6ac9221", size = 12311717, upload-time = "2026-07-18T01:39:50.881Z" }, + { url = "https://files.pythonhosted.org/packages/e1/14/7caec26d93a943c0e7d15eb7374644508d08cbd387d112b722b12d14e044/ty-0.0.61-py3-none-win_arm64.whl", hash = "sha256:3e496f7698bc4b5bbb1eb66d8b5799ba87596d88d36604ca359083893fa2fc49", size = 11693485, upload-time = "2026-07-18T01:39:52.73Z" }, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.20260518" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/83/4a1afc3fbfcf5b8d46fc390cd95ed6b0dc9010a265f4e9f46314efffa37a/types_pyyaml-6.0.12.20260518.tar.gz", hash = "sha256:d917f83fb38462550338c1297faedd860b3ec83912b96b1e3d73255f7473e466", size = 17850, upload-time = "2026-05-18T06:01:58.675Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/a2/c01db32be2ae7d6a1689972f3c492b149ee4e164b12fdfd9f64b50888215/types_pyyaml-6.0.12.20260518-py3-none-any.whl", hash = "sha256:d2150f75a231c9fe9c7463bd29487d93e60bac90400287351384bc2284eba7cd", size = 20312, upload-time = "2026-05-18T06:01:57.368Z" }, +] + +[[package]] +name = "types-requests" +version = "2.33.0.20260712" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/51/703318f7b7be8bee126ec13bf615050f932d0179b8784420f3a0199cc769/types_requests-2.33.0.20260712.tar.gz", hash = "sha256:2141b67ab534a5c5cd2dac5034f2a35f42e699c5bf185eee608c5246a069d7fb", size = 25084, upload-time = "2026-07-12T05:14:20.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/e7/010c87f559e216d83f9dc51e939633fd0d0ead3377340181ab0e223cd3b5/types_requests-2.33.0.20260712-py3-none-any.whl", hash = "sha256:de027e28c171d3da529689cbfa023b0b4eab188c8dfa22fd834eebd2cee6e7bb", size = 21392, upload-time = "2026-07-12T05:14:19.616Z" }, +] + +[[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 = "xmltodict" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/70/80f3b7c10d2630aa66414bf23d210386700aa390547278c789afa994fd7e/xmltodict-1.0.4.tar.gz", hash = "sha256:6d94c9f834dd9e44514162799d344d815a3a4faec913717a9ecbfa5be1bb8e61", size = 26124, upload-time = "2026-02-22T02:21:22.074Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/34/98a2f52245f4d47be93b580dae5f9861ef58977d73a79eb47c58f1ad1f3a/xmltodict-1.0.4-py3-none-any.whl", hash = "sha256:a4a00d300b0e1c59fc2bfccb53d7b2e88c32f200df138a0dd2229f842497026a", size = 13580, upload-time = "2026-02-22T02:21:21.039Z" }, +] + +[[package]] +name = "yamllint" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pathspec" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/a0/8fc2d68e132cf918f18273fdc8a1b8432b60d75ac12fdae4b0ef5c9d2e8d/yamllint-1.38.0.tar.gz", hash = "sha256:09e5f29531daab93366bb061e76019d5e91691ef0a40328f04c927387d1d364d", size = 142446, upload-time = "2026-01-13T07:47:53.276Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/92/aed08e68de6e6a3d7c2328ce7388072cd6affc26e2917197430b646aed02/yamllint-1.38.0-py3-none-any.whl", hash = "sha256:fc394a5b3be980a4062607b8fdddc0843f4fa394152b6da21722f5d59013c220", size = 68940, upload-time = "2026-01-13T07:47:51.343Z" }, +] From a444d67a6e3f996de07175e636f8f81450b6a762 Mon Sep 17 00:00:00 2001 From: Shane <6071159+smashedr@users.noreply.github.com> Date: Tue, 21 Jul 2026 17:58:52 -0700 Subject: [PATCH 2/2] Fix Bake Error --- docker-compose-swarm.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker-compose-swarm.yaml b/docker-compose-swarm.yaml index 4a26145..b4ba271 100644 --- a/docker-compose-swarm.yaml +++ b/docker-compose-swarm.yaml @@ -2,6 +2,7 @@ version: "3.8" services: nginx: + build: nginx image: ghcr.io/cssnr/red-api-nginx:${VERSION:-latest} deploy: mode: global @@ -36,6 +37,9 @@ services: - net-internal app: + build: + context: . + dockerfile: ./app/Dockerfile image: ghcr.io/cssnr/red-api-app:${VERSION:-latest} environment: APP_LOG_LEVEL: ${APP_LOG_LEVEL}