diff --git a/.codecov.yml b/.codecov.yml index b4037d507..e6b4d0347 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1,6 +1,7 @@ +# https://docs.codecov.com/docs/codecovyml-reference#codecov codecov: require_ci_to_pass: true - # https://docs.codecov.com/docs/components +# https://docs.codecov.com/docs/components component_management: individual_components: - component_id: backend @@ -9,35 +10,70 @@ component_management: - component_id: frontend paths: - src-ui/** +# https://docs.codecov.com/docs/flags#step-2-flag-management-in-yaml +# https://docs.codecov.com/docs/carryforward-flags flags: - backend: + # Backend Python versions + backend-python-3.10: paths: - src/** carryforward: true - frontend: + backend-python-3.11: + paths: + - src/** + carryforward: true + backend-python-3.12: + paths: + - src/** + carryforward: true + # Frontend (shards merge into single flag) + frontend-node-24.x: paths: - src-ui/** carryforward: true -# https://docs.codecov.com/docs/pull-request-comments comment: layout: "header, diff, components, flags, files" - # https://docs.codecov.com/docs/javascript-bundle-analysis require_bundle_changes: true bundle_change_threshold: "50Kb" coverage: + # https://docs.codecov.com/docs/commit-status status: project: - default: + backend: + flags: + - backend-python-3.10 + - backend-python-3.11 + - backend-python-3.12 + paths: + - src/** # https://docs.codecov.com/docs/commit-status#threshold threshold: 1% + removed_code_behavior: adjust_base + frontend: + flags: + - frontend-node-24.x + paths: + - src-ui/** + threshold: 1% + removed_code_behavior: adjust_base patch: - default: - # For the changed lines only, target 100% covered, but - # allow as low as 75% + backend: + flags: + - backend-python-3.10 + - backend-python-3.11 + - backend-python-3.12 + paths: + - src/** + target: 100% + threshold: 25% + frontend: + flags: + - frontend-node-24.x + paths: + - src-ui/** target: 100% threshold: 25% # https://docs.codecov.com/docs/javascript-bundle-analysis bundle_analysis: - # Fail if the bundle size increases by more than 1MB warning_threshold: "1MB" status: true diff --git a/.devcontainer/README.md b/.devcontainer/README.md index cec62c802..1b7429c8d 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -89,6 +89,18 @@ Additional tasks are available for common maintenance operations: - **Migrate Database**: To apply database migrations. - **Create Superuser**: To create an admin user for the application. +## Committing from the Host Machine + +The DevContainer automatically installs pre-commit hooks during setup. However, these hooks are configured for use inside the container. + +If you want to commit changes from your host machine (outside the DevContainer), you need to set up pre-commit on your host. This installs it as a standalone tool. + +```bash +uv tool install pre-commit && pre-commit install +``` + +After this, you can commit either from inside the DevContainer or from your host machine. + ## Let's Get Started! Follow the steps above to get your development environment up and running. Happy coding! diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index cec8e2177..e1a65602c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,26 +3,31 @@ "dockerComposeFile": "docker-compose.devcontainer.sqlite-tika.yml", "service": "paperless-development", "workspaceFolder": "/usr/src/paperless/paperless-ngx", - "postCreateCommand": "/bin/bash -c 'rm -rf .venv/.* && uv sync --group dev && uv run pre-commit install'", + "forwardPorts": [4200, 8000], + "containerEnv": { + "UV_CACHE_DIR": "/usr/src/paperless/paperless-ngx/.uv-cache" + }, + "postCreateCommand": "/bin/bash -c 'rm -rf .venv/.* && uv sync --group dev && uv run pre-commit install'", "customizations": { "vscode": { - "extensions": [ - "mhutchie.git-graph", - "ms-python.python", - "ms-vscode.js-debug-nightly", - "eamodio.gitlens", - "yzhang.markdown-all-in-one" - ], - "settings": { - "python.defaultInterpreterPath": "/usr/src/paperless/paperless-ngx/.venv/bin/python", - "python.pythonPath": "/usr/src/paperless/paperless-ngx/.venv/bin/python", - "python.terminal.activateEnvInCurrentTerminal": true, - "editor.formatOnPaste": false, - "editor.formatOnSave": true, - "editor.formatOnType": true, - "files.trimTrailingWhitespace": true - } + "extensions": [ + "mhutchie.git-graph", + "ms-python.python", + "ms-vscode.js-debug-nightly", + "eamodio.gitlens", + "yzhang.markdown-all-in-one", + "pnpm.pnpm" + ], + "settings": { + "python.defaultInterpreterPath": "/usr/src/paperless/paperless-ngx/.venv/bin/python", + "python.pythonPath": "/usr/src/paperless/paperless-ngx/.venv/bin/python", + "python.terminal.activateEnvInCurrentTerminal": true, + "editor.formatOnPaste": false, + "editor.formatOnSave": true, + "editor.formatOnType": true, + "files.trimTrailingWhitespace": true + } } - }, - "remoteUser": "paperless" - } + }, + "remoteUser": "paperless" +} diff --git a/.devcontainer/vscode/tasks.json b/.devcontainer/vscode/tasks.json index 6475e14d1..f7cd18d48 100644 --- a/.devcontainer/vscode/tasks.json +++ b/.devcontainer/vscode/tasks.json @@ -33,7 +33,7 @@ "label": "Start: Frontend Angular", "description": "Start the Frontend Angular Dev Server", "type": "shell", - "command": "pnpm start", + "command": "pnpm exec ng serve --host 0.0.0.0", "isBackground": true, "options": { "cwd": "${workspaceFolder}/src-ui" @@ -174,12 +174,22 @@ { "label": "Maintenance: Install Frontend Dependencies", "description": "Install frontend (pnpm) dependencies", - "type": "pnpm", - "script": "install", - "path": "src-ui", + "type": "shell", + "command": "pnpm install", "group": "clean", "problemMatcher": [], - "detail": "install dependencies from package" + "options": { + "cwd": "${workspaceFolder}/src-ui" + }, + "presentation": { + "echo": true, + "reveal": "always", + "focus": true, + "panel": "shared", + "showReuseMessage": false, + "clear": true, + "revealProblems": "onProblem" + } }, { "description": "Clean install frontend dependencies and build the frontend for production", diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 89c8a96ea..2b8169f24 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -44,6 +44,7 @@ include-labels: - 'notable' exclude-labels: - 'skip-changelog' +filter-by-commitish: true category-template: '### $TITLE' change-template: '- $TITLE @$AUTHOR ([#$NUMBER]($URL))' change-title-escapes: '\<*_&#@' diff --git a/.github/workflows/ci-backend.yml b/.github/workflows/ci-backend.yml index 25d19f73a..a619f01c1 100644 --- a/.github/workflows/ci-backend.yml +++ b/.github/workflows/ci-backend.yml @@ -75,9 +75,6 @@ jobs: env: NLTK_DATA: ${{ env.NLTK_DATA }} PAPERLESS_CI_TEST: 1 - PAPERLESS_MAIL_TEST_HOST: ${{ secrets.TEST_MAIL_HOST }} - PAPERLESS_MAIL_TEST_USER: ${{ secrets.TEST_MAIL_USER }} - PAPERLESS_MAIL_TEST_PASSWD: ${{ secrets.TEST_MAIL_PASSWD }} run: | uv run \ --python ${{ steps.setup-python.outputs.python-version }} \ @@ -88,13 +85,13 @@ jobs: if: always() uses: codecov/codecov-action@v5 with: - flags: backend,backend-python-${{ matrix.python-version }} + flags: backend-python-${{ matrix.python-version }} files: junit.xml report_type: test_results - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 with: - flags: backend,backend-python-${{ matrix.python-version }} + flags: backend-python-${{ matrix.python-version }} files: coverage.xml report_type: coverage - name: Stop containers diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 5793ecfa1..3710e6589 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -35,7 +35,7 @@ jobs: contents: read packages: write outputs: - can-push: ${{ steps.check-push.outputs.can-push }} + should-push: ${{ steps.check-push.outputs.should-push }} push-external: ${{ steps.check-push.outputs.push-external }} repository: ${{ steps.repo.outputs.name }} ref-name: ${{ steps.ref.outputs.name }} @@ -46,29 +46,42 @@ jobs: id: ref run: | ref_name="${GITHUB_HEAD_REF:-$GITHUB_REF_NAME}" - # Sanitize by replacing / with - for cache keys - cache_ref="${ref_name//\//-}" + # Sanitize by replacing / with - for use in tags and cache keys + sanitized_ref="${ref_name//\//-}" echo "ref_name=${ref_name}" - echo "cache_ref=${cache_ref}" + echo "sanitized_ref=${sanitized_ref}" - echo "name=${ref_name}" >> $GITHUB_OUTPUT - echo "cache-ref=${cache_ref}" >> $GITHUB_OUTPUT + echo "name=${sanitized_ref}" >> $GITHUB_OUTPUT - name: Check push permissions id: check-push env: REF_NAME: ${{ steps.ref.outputs.name }} run: | - # can-push: Can we push to GHCR? - # True for: pushes, or PRs from the same repo (not forks) - can_push=${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository }} - echo "can-push=${can_push}" - echo "can-push=${can_push}" >> $GITHUB_OUTPUT + # should-push: Should we push to GHCR? + # True for: + # 1. Pushes (tags/dev/beta) - filtered via the workflow triggers + # 2. Manual dispatch - always push to GHCR + # 3. Internal PRs where the branch name starts with 'feature-' or 'fix-' + + should_push="false" + if [[ "${{ github.event_name }}" == "push" ]]; then + should_push="true" + elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + should_push="true" + elif [[ "${{ github.event_name }}" == "pull_request" && "${{ github.event.pull_request.head.repo.full_name }}" == "${{ github.repository }}" ]]; then + if [[ "${REF_NAME}" == feature-* || "${REF_NAME}" == fix-* ]]; then + should_push="true" + fi + fi + + echo "should-push=${should_push}" + echo "should-push=${should_push}" >> $GITHUB_OUTPUT # push-external: Should we also push to Docker Hub and Quay.io? # Only for main repo on dev/beta branches or version tags push_external="false" - if [[ "${can_push}" == "true" && "${{ github.repository_owner }}" == "paperless-ngx" ]]; then + if [[ "${should_push}" == "true" && "${{ github.repository_owner }}" == "paperless-ngx" ]]; then case "${REF_NAME}" in dev|beta) push_external="true" @@ -98,6 +111,12 @@ jobs: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Maximize space + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/share/boost + sudo rm -rf "$AGENT_TOOLSDIRECTORY" - name: Docker metadata id: docker-meta uses: docker/metadata-action@v5.10.0 @@ -119,20 +138,20 @@ jobs: labels: ${{ steps.docker-meta.outputs.labels }} build-args: | PNGX_TAG_VERSION=${{ steps.docker-meta.outputs.version }} - outputs: type=image,name=${{ env.REGISTRY }}/${{ steps.repo.outputs.name }},push-by-digest=true,name-canonical=true,push=${{ steps.check-push.outputs.can-push }} + outputs: type=image,name=${{ env.REGISTRY }}/${{ steps.repo.outputs.name }},push-by-digest=true,name-canonical=true,push=${{ steps.check-push.outputs.should-push }} cache-from: | - type=registry,ref=${{ env.REGISTRY }}/${{ steps.repo.outputs.name }}/cache/app:${{ steps.ref.outputs.cache-ref }}-${{ matrix.arch }} + type=registry,ref=${{ env.REGISTRY }}/${{ steps.repo.outputs.name }}/cache/app:${{ steps.ref.outputs.name }}-${{ matrix.arch }} type=registry,ref=${{ env.REGISTRY }}/${{ steps.repo.outputs.name }}/cache/app:dev-${{ matrix.arch }} - cache-to: ${{ steps.check-push.outputs.can-push == 'true' && format('type=registry,mode=max,ref={0}/{1}/cache/app:{2}-{3}', env.REGISTRY, steps.repo.outputs.name, steps.ref.outputs.cache-ref, matrix.arch) || '' }} + cache-to: ${{ steps.check-push.outputs.should-push == 'true' && format('type=registry,mode=max,ref={0}/{1}/cache/app:{2}-{3}', env.REGISTRY, steps.repo.outputs.name, steps.ref.outputs.name, matrix.arch) || '' }} - name: Export digest - if: steps.check-push.outputs.can-push == 'true' + if: steps.check-push.outputs.should-push == 'true' run: | mkdir -p /tmp/digests digest="${{ steps.build.outputs.digest }}" echo "digest=${digest}" touch "/tmp/digests/${digest#sha256:}" - name: Upload digest - if: steps.check-push.outputs.can-push == 'true' + if: steps.check-push.outputs.should-push == 'true' uses: actions/upload-artifact@v6.0.0 with: name: digests-${{ matrix.arch }} @@ -143,7 +162,7 @@ jobs: name: Merge and Push Manifest runs-on: ubuntu-24.04 needs: build-arch - if: needs.build-arch.outputs.can-push == 'true' + if: needs.build-arch.outputs.should-push == 'true' permissions: contents: read packages: write diff --git a/.github/workflows/ci-frontend.yml b/.github/workflows/ci-frontend.yml index 907d36abf..d800fe827 100644 --- a/.github/workflows/ci-frontend.yml +++ b/.github/workflows/ci-frontend.yml @@ -109,13 +109,13 @@ jobs: if: always() uses: codecov/codecov-action@v5 with: - flags: frontend,frontend-node-${{ matrix.node-version }} + flags: frontend-node-${{ matrix.node-version }} directory: src-ui/ report_type: test_results - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 with: - flags: frontend,frontend-node-${{ matrix.node-version }} + flags: frontend-node-${{ matrix.node-version }} directory: src-ui/coverage/ e2e-tests: name: "E2E Tests (${{ matrix.shard-index }}/${{ matrix.shard-count }})" diff --git a/.gitignore b/.gitignore index c7b5c4d8e..715760b29 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,7 @@ htmlcov/ .coverage .coverage.* .cache +.uv-cache nosetests.xml coverage.xml *,cover diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 92e18bfbd..0ce2b210e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: - json # See https://github.com/prettier/prettier/issues/15742 for the fork reason - repo: https://github.com/rbubley/mirrors-prettier - rev: 'v3.6.2' + rev: 'v3.8.1' hooks: - id: prettier types_or: @@ -49,7 +49,7 @@ repos: - 'prettier-plugin-organize-imports@4.1.0' # Python hooks - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.5 + rev: v0.14.14 hooks: - id: ruff-check - id: ruff-format @@ -76,7 +76,7 @@ repos: hooks: - id: shellcheck - repo: https://github.com/google/yamlfmt - rev: v0.20.0 + rev: v0.21.0 hooks: - id: yamlfmt exclude: "^src-ui/pnpm-lock.yaml" diff --git a/Dockerfile b/Dockerfile index 5262ea124..ba0592509 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,7 +30,7 @@ RUN set -eux \ # Purpose: Installs s6-overlay and rootfs # Comments: # - Don't leave anything extra in here either -FROM ghcr.io/astral-sh/uv:0.9.15-python3.12-trixie-slim AS s6-overlay-base +FROM ghcr.io/astral-sh/uv:0.9.26-python3.12-trixie-slim AS s6-overlay-base WORKDIR /usr/src/s6 @@ -196,7 +196,11 @@ RUN set -eux \ && apt-get install --yes --quiet --no-install-recommends ${BUILD_PACKAGES} \ && echo "Installing Python requirements" \ && uv export --quiet --no-dev --all-extras --format requirements-txt --output-file requirements.txt \ - && uv pip install --no-cache --system --no-python-downloads --python-preference system --requirements requirements.txt \ + && uv pip install --no-cache --system --no-python-downloads --python-preference system \ + --index https://pypi.org/simple \ + --index https://download.pytorch.org/whl/cpu \ + --index-strategy unsafe-best-match \ + --requirements requirements.txt \ && echo "Installing NLTK data" \ && python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" snowball_data \ && python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" stopwords \ diff --git a/docker/compose/docker-compose.ci-test.yml b/docker/compose/docker-compose.ci-test.yml index d277406b8..d227ac038 100644 --- a/docker/compose/docker-compose.ci-test.yml +++ b/docker/compose/docker-compose.ci-test.yml @@ -23,3 +23,24 @@ services: container_name: tika network_mode: host restart: unless-stopped + greenmail: + image: greenmail/standalone:2.1.8 + hostname: greenmail + container_name: greenmail + environment: + # Enable only IMAP for now (SMTP available via 3025 if needed later) + GREENMAIL_OPTS: >- + -Dgreenmail.setup.test.imap -Dgreenmail.users=test@localhost:test -Dgreenmail.users.login=test@localhost -Dgreenmail.verbose + ports: + - "3143:3143" # IMAP + restart: unless-stopped + nginx: + image: docker.io/nginx:1.29-alpine + hostname: nginx + container_name: nginx + ports: + - "8080:8080" + restart: unless-stopped + volumes: + - ../../docs/assets:/usr/share/nginx/html/assets:ro + - ./test-nginx.conf:/etc/nginx/conf.d/default.conf:ro diff --git a/docker/compose/test-nginx.conf b/docker/compose/test-nginx.conf new file mode 100644 index 000000000..e90f3fad3 --- /dev/null +++ b/docker/compose/test-nginx.conf @@ -0,0 +1,14 @@ +server { + listen 8080; + server_name localhost; + + root /usr/share/nginx/html; + + # Enable CORS for test requests + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, HEAD, OPTIONS' always; + + location / { + try_files $uri $uri/ =404; + } +} diff --git a/docker/install_management_commands.sh b/docker/install_management_commands.sh index 17dae68a2..f7a175e9e 100755 --- a/docker/install_management_commands.sh +++ b/docker/install_management_commands.sh @@ -4,13 +4,13 @@ set -eu -for command in decrypt_documents \ - document_archiver \ +for command in document_archiver \ document_exporter \ document_importer \ mail_fetcher \ document_create_classifier \ document_index \ + document_llmindex \ document_renamer \ document_retagger \ document_thumbnails \ diff --git a/docker/management_script.sh b/docker/management_script.sh index 1fa31c372..91a6336d0 100755 --- a/docker/management_script.sh +++ b/docker/management_script.sh @@ -5,10 +5,8 @@ set -e cd "${PAPERLESS_SRC_DIR}" -if [[ $(id -u) == 0 ]]; then - s6-setuidgid paperless python3 manage.py management_command "$@" -elif [[ $(id -un) == "paperless" ]]; then +if [[ -n "${USER_IS_NON_ROOT}" ]]; then python3 manage.py management_command "$@" -else - echo "Unknown user." +elif [[ $(id -un) == "paperless" ]]; then + s6-setuidgid paperless python3 manage.py management_command "$@" fi diff --git a/docker/rootfs/usr/local/bin/convert_mariadb_uuid b/docker/rootfs/usr/local/bin/convert_mariadb_uuid index 806a98f3b..019c558f1 100755 --- a/docker/rootfs/usr/local/bin/convert_mariadb_uuid +++ b/docker/rootfs/usr/local/bin/convert_mariadb_uuid @@ -5,10 +5,8 @@ set -e cd "${PAPERLESS_SRC_DIR}" -if [[ $(id -u) == 0 ]]; then - s6-setuidgid paperless python3 manage.py convert_mariadb_uuid "$@" +if [[ -n "${USER_IS_NON_ROOT}" ]]; then + python3 manage.py convert_mariadb_uuid "$@" elif [[ $(id -un) == "paperless" ]]; then - python3 manage.py convert_mariadb_uuid "$@" -else - echo "Unknown user." + s6-setuidgid paperless python3 manage.py convert_mariadb_uuid "$@" fi diff --git a/docker/rootfs/usr/local/bin/createsuperuser b/docker/rootfs/usr/local/bin/createsuperuser index f931952ba..2b56869f6 100755 --- a/docker/rootfs/usr/local/bin/createsuperuser +++ b/docker/rootfs/usr/local/bin/createsuperuser @@ -5,10 +5,8 @@ set -e cd "${PAPERLESS_SRC_DIR}" -if [[ $(id -u) == 0 ]]; then - s6-setuidgid paperless python3 manage.py createsuperuser "$@" +if [[ -n "${USER_IS_NON_ROOT}" ]]; then + python3 manage.py createsuperuser "$@" elif [[ $(id -un) == "paperless" ]]; then - python3 manage.py createsuperuser "$@" -else - echo "Unknown user." + s6-setuidgid paperless python3 manage.py createsuperuser "$@" fi diff --git a/docker/rootfs/usr/local/bin/document_archiver b/docker/rootfs/usr/local/bin/document_archiver index 383acfcc6..8d7771d26 100755 --- a/docker/rootfs/usr/local/bin/document_archiver +++ b/docker/rootfs/usr/local/bin/document_archiver @@ -5,10 +5,8 @@ set -e cd "${PAPERLESS_SRC_DIR}" -if [[ $(id -u) == 0 ]]; then - s6-setuidgid paperless python3 manage.py document_archiver "$@" +if [[ -n "${USER_IS_NON_ROOT}" ]]; then + python3 manage.py document_archiver "$@" elif [[ $(id -un) == "paperless" ]]; then - python3 manage.py document_archiver "$@" -else - echo "Unknown user." + s6-setuidgid paperless python3 manage.py document_archiver "$@" fi diff --git a/docker/rootfs/usr/local/bin/document_create_classifier b/docker/rootfs/usr/local/bin/document_create_classifier index 72dc33d6f..23acc6741 100755 --- a/docker/rootfs/usr/local/bin/document_create_classifier +++ b/docker/rootfs/usr/local/bin/document_create_classifier @@ -5,10 +5,8 @@ set -e cd "${PAPERLESS_SRC_DIR}" -if [[ $(id -u) == 0 ]]; then - s6-setuidgid paperless python3 manage.py document_create_classifier "$@" +if [[ -n "${USER_IS_NON_ROOT}" ]]; then + python3 manage.py document_create_classifier "$@" elif [[ $(id -un) == "paperless" ]]; then - python3 manage.py document_create_classifier "$@" -else - echo "Unknown user." + s6-setuidgid paperless python3 manage.py document_create_classifier "$@" fi diff --git a/docker/rootfs/usr/local/bin/document_exporter b/docker/rootfs/usr/local/bin/document_exporter index 7f48215d7..d55f01d48 100755 --- a/docker/rootfs/usr/local/bin/document_exporter +++ b/docker/rootfs/usr/local/bin/document_exporter @@ -5,10 +5,8 @@ set -e cd "${PAPERLESS_SRC_DIR}" -if [[ $(id -u) == 0 ]]; then - s6-setuidgid paperless python3 manage.py document_exporter "$@" +if [[ -n "${USER_IS_NON_ROOT}" ]]; then + python3 manage.py document_exporter "$@" elif [[ $(id -un) == "paperless" ]]; then - python3 manage.py document_exporter "$@" -else - echo "Unknown user." + s6-setuidgid paperless python3 manage.py document_exporter "$@" fi diff --git a/docker/rootfs/usr/local/bin/document_fuzzy_match b/docker/rootfs/usr/local/bin/document_fuzzy_match index 5b9548557..c6e4edadc 100755 --- a/docker/rootfs/usr/local/bin/document_fuzzy_match +++ b/docker/rootfs/usr/local/bin/document_fuzzy_match @@ -5,10 +5,8 @@ set -e cd "${PAPERLESS_SRC_DIR}" -if [[ $(id -u) == 0 ]]; then - s6-setuidgid paperless python3 manage.py document_fuzzy_match "$@" +if [[ -n "${USER_IS_NON_ROOT}" ]]; then + python3 manage.py document_fuzzy_match "$@" elif [[ $(id -un) == "paperless" ]]; then - python3 manage.py document_fuzzy_match "$@" -else - echo "Unknown user." + s6-setuidgid paperless python3 manage.py document_fuzzy_match "$@" fi diff --git a/docker/rootfs/usr/local/bin/document_importer b/docker/rootfs/usr/local/bin/document_importer index 2286e89f7..07c92bb04 100755 --- a/docker/rootfs/usr/local/bin/document_importer +++ b/docker/rootfs/usr/local/bin/document_importer @@ -5,10 +5,8 @@ set -e cd "${PAPERLESS_SRC_DIR}" -if [[ $(id -u) == 0 ]]; then - s6-setuidgid paperless python3 manage.py document_importer "$@" +if [[ -n "${USER_IS_NON_ROOT}" ]]; then + python3 manage.py document_importer "$@" elif [[ $(id -un) == "paperless" ]]; then - python3 manage.py document_importer "$@" -else - echo "Unknown user." + s6-setuidgid paperless python3 manage.py document_importer "$@" fi diff --git a/docker/rootfs/usr/local/bin/document_index b/docker/rootfs/usr/local/bin/document_index index 2d518b5c5..47c893c10 100755 --- a/docker/rootfs/usr/local/bin/document_index +++ b/docker/rootfs/usr/local/bin/document_index @@ -5,10 +5,8 @@ set -e cd "${PAPERLESS_SRC_DIR}" -if [[ $(id -u) == 0 ]]; then - s6-setuidgid paperless python3 manage.py document_index "$@" +if [[ -n "${USER_IS_NON_ROOT}" ]]; then + python3 manage.py document_index "$@" elif [[ $(id -un) == "paperless" ]]; then - python3 manage.py document_index "$@" -else - echo "Unknown user." + s6-setuidgid paperless python3 manage.py document_index "$@" fi diff --git a/docker/rootfs/usr/local/bin/decrypt_documents b/docker/rootfs/usr/local/bin/document_llmindex similarity index 65% rename from docker/rootfs/usr/local/bin/decrypt_documents rename to docker/rootfs/usr/local/bin/document_llmindex index 4da1549ee..8e51245e1 100755 --- a/docker/rootfs/usr/local/bin/decrypt_documents +++ b/docker/rootfs/usr/local/bin/document_llmindex @@ -6,9 +6,9 @@ set -e cd "${PAPERLESS_SRC_DIR}" if [[ $(id -u) == 0 ]]; then - s6-setuidgid paperless python3 manage.py decrypt_documents "$@" + s6-setuidgid paperless python3 manage.py document_llmindex "$@" elif [[ $(id -un) == "paperless" ]]; then - python3 manage.py decrypt_documents "$@" + python3 manage.py document_llmindex "$@" else echo "Unknown user." fi diff --git a/docker/rootfs/usr/local/bin/document_renamer b/docker/rootfs/usr/local/bin/document_renamer index 326317a73..3406182ee 100755 --- a/docker/rootfs/usr/local/bin/document_renamer +++ b/docker/rootfs/usr/local/bin/document_renamer @@ -5,10 +5,8 @@ set -e cd "${PAPERLESS_SRC_DIR}" -if [[ $(id -u) == 0 ]]; then - s6-setuidgid paperless python3 manage.py document_renamer "$@" +if [[ -n "${USER_IS_NON_ROOT}" ]]; then + python3 manage.py document_renamer "$@" elif [[ $(id -un) == "paperless" ]]; then - python3 manage.py document_renamer "$@" -else - echo "Unknown user." + s6-setuidgid paperless python3 manage.py document_renamer "$@" fi diff --git a/docker/rootfs/usr/local/bin/document_retagger b/docker/rootfs/usr/local/bin/document_retagger index 3bab3e790..b0d1047ff 100755 --- a/docker/rootfs/usr/local/bin/document_retagger +++ b/docker/rootfs/usr/local/bin/document_retagger @@ -5,10 +5,8 @@ set -e cd "${PAPERLESS_SRC_DIR}" -if [[ $(id -u) == 0 ]]; then - s6-setuidgid paperless python3 manage.py document_retagger "$@" +if [[ -n "${USER_IS_NON_ROOT}" ]]; then + python3 manage.py document_retagger "$@" elif [[ $(id -un) == "paperless" ]]; then - python3 manage.py document_retagger "$@" -else - echo "Unknown user." + s6-setuidgid paperless python3 manage.py document_retagger "$@" fi diff --git a/docker/rootfs/usr/local/bin/document_sanity_checker b/docker/rootfs/usr/local/bin/document_sanity_checker index 5c0c29ef2..d792124fc 100755 --- a/docker/rootfs/usr/local/bin/document_sanity_checker +++ b/docker/rootfs/usr/local/bin/document_sanity_checker @@ -5,10 +5,8 @@ set -e cd "${PAPERLESS_SRC_DIR}" -if [[ $(id -u) == 0 ]]; then - s6-setuidgid paperless python3 manage.py document_sanity_checker "$@" +if [[ -n "${USER_IS_NON_ROOT}" ]]; then + python3 manage.py document_sanity_checker "$@" elif [[ $(id -un) == "paperless" ]]; then - python3 manage.py document_sanity_checker "$@" -else - echo "Unknown user." + s6-setuidgid paperless python3 manage.py document_sanity_checker "$@" fi diff --git a/docker/rootfs/usr/local/bin/document_thumbnails b/docker/rootfs/usr/local/bin/document_thumbnails index c1000c31a..71d80e00d 100755 --- a/docker/rootfs/usr/local/bin/document_thumbnails +++ b/docker/rootfs/usr/local/bin/document_thumbnails @@ -5,10 +5,8 @@ set -e cd "${PAPERLESS_SRC_DIR}" -if [[ $(id -u) == 0 ]]; then - s6-setuidgid paperless python3 manage.py document_thumbnails "$@" +if [[ -n "${USER_IS_NON_ROOT}" ]]; then + python3 manage.py document_thumbnails "$@" elif [[ $(id -un) == "paperless" ]]; then - python3 manage.py document_thumbnails "$@" -else - echo "Unknown user." + s6-setuidgid paperless python3 manage.py document_thumbnails "$@" fi diff --git a/docker/rootfs/usr/local/bin/mail_fetcher b/docker/rootfs/usr/local/bin/mail_fetcher index 2ae1d1dfb..654c07389 100755 --- a/docker/rootfs/usr/local/bin/mail_fetcher +++ b/docker/rootfs/usr/local/bin/mail_fetcher @@ -5,10 +5,8 @@ set -e cd "${PAPERLESS_SRC_DIR}" -if [[ $(id -u) == 0 ]]; then - s6-setuidgid paperless python3 manage.py mail_fetcher "$@" +if [[ -n "${USER_IS_NON_ROOT}" ]]; then + python3 manage.py mail_fetcher "$@" elif [[ $(id -un) == "paperless" ]]; then - python3 manage.py mail_fetcher "$@" -else - echo "Unknown user." + s6-setuidgid paperless python3 manage.py mail_fetcher "$@" fi diff --git a/docker/rootfs/usr/local/bin/manage_superuser b/docker/rootfs/usr/local/bin/manage_superuser index 9f7f37ecf..a6e41168c 100755 --- a/docker/rootfs/usr/local/bin/manage_superuser +++ b/docker/rootfs/usr/local/bin/manage_superuser @@ -5,10 +5,8 @@ set -e cd "${PAPERLESS_SRC_DIR}" -if [[ $(id -u) == 0 ]]; then - s6-setuidgid paperless python3 manage.py manage_superuser "$@" +if [[ -n "${USER_IS_NON_ROOT}" ]]; then + python3 manage.py manage_superuser "$@" elif [[ $(id -un) == "paperless" ]]; then - python3 manage.py manage_superuser "$@" -else - echo "Unknown user." + s6-setuidgid paperless python3 manage.py manage_superuser "$@" fi diff --git a/docker/rootfs/usr/local/bin/prune_audit_logs b/docker/rootfs/usr/local/bin/prune_audit_logs index b9142e98e..04446df17 100755 --- a/docker/rootfs/usr/local/bin/prune_audit_logs +++ b/docker/rootfs/usr/local/bin/prune_audit_logs @@ -5,10 +5,8 @@ set -e cd "${PAPERLESS_SRC_DIR}" -if [[ $(id -u) == 0 ]]; then - s6-setuidgid paperless python3 manage.py prune_audit_logs "$@" +if [[ -n "${USER_IS_NON_ROOT}" ]]; then + python3 manage.py prune_audit_logs "$@" elif [[ $(id -un) == "paperless" ]]; then - python3 manage.py prune_audit_logs "$@" -else - echo "Unknown user." + s6-setuidgid paperless python3 manage.py prune_audit_logs "$@" fi diff --git a/docs/administration.md b/docs/administration.md index ddf51bf9a..7e5ca2d41 100644 --- a/docs/administration.md +++ b/docs/administration.md @@ -580,39 +580,9 @@ document. documents, such as encrypted PDF documents. The archiver will skip over these documents each time it sees them. -### Managing encryption {#encryption} - -!!! warning - - Encryption was removed in [paperless-ng 0.9](changelog.md#paperless-ng-090) - because it did not really provide any additional security, the passphrase - was stored in a configuration file on the same system as the documents. - Furthermore, the entire text content of the documents is stored plain in - the database, even if your documents are encrypted. Filenames are not - encrypted as well. Finally, the web server provides transparent access to - your encrypted documents. - - Consider running paperless on an encrypted filesystem instead, which - will then at least provide security against physical hardware theft. - -#### Enabling encryption - -Enabling encryption is no longer supported. - -#### Disabling encryption - -Basic usage to disable encryption of your document store: - -(Note: If `PAPERLESS_PASSPHRASE` isn't set already, you need to specify -it here) - -``` -decrypt_documents [--passphrase SECR3TP4SSPHRA$E] -``` - ### Detecting duplicates {#fuzzy_duplicate} -Paperless already catches and prevents upload of exactly matching documents, +Paperless-ngx already catches and warns of exactly matching documents, however a new scan of an existing document may not produce an exact bit for bit duplicate. But the content should be exact or close, allowing detection. diff --git a/docs/advanced_usage.md b/docs/advanced_usage.md index de1068864..638544005 100644 --- a/docs/advanced_usage.md +++ b/docs/advanced_usage.md @@ -501,7 +501,7 @@ The `datetime` filter formats a datetime string or datetime object using Python' See the [strftime format code documentation](https://docs.python.org/3.13/library/datetime.html#strftime-and-strptime-format-codes) for the possible codes and their meanings. -##### Date Localization +##### Date Localization {#date-localization} The `localize_date` filter formats a date or datetime object into a localized string using Babel internationalization. This takes into account the provided locale for translation. Since this must be used on a date or datetime object, @@ -805,6 +805,27 @@ See the relevant settings [`PAPERLESS_CONSUMER_ENABLE_TAG_BARCODE`](configuratio and [`PAPERLESS_CONSUMER_TAG_BARCODE_MAPPING`](configuration.md#PAPERLESS_CONSUMER_TAG_BARCODE_MAPPING) for more information. +#### Splitting on Tag Barcodes + +By default, tag barcodes only assign tags to documents without splitting them. However, +you can enable document splitting on tag barcodes by setting +[`PAPERLESS_CONSUMER_TAG_BARCODE_SPLIT`](configuration.md#PAPERLESS_CONSUMER_TAG_BARCODE_SPLIT) +to `true`. + +When enabled, documents will be split at pages containing tag barcodes, similar to how +ASN barcodes work. Key features: + +- The page with the tag barcode is **retained** in the resulting document +- **Each split document extracts its own tags** - only tags on pages within that document are assigned +- Multiple tag barcodes can trigger multiple splits in the same document +- Works seamlessly with ASN barcodes - each split document gets its own ASN and tags + +This is useful for batch scanning where you place tag barcode pages between different +documents to both separate and categorize them in a single operation. + +**Example:** A 6-page scan with TAG:invoice on page 3 and TAG:receipt on page 5 will create +three documents: pages 1-2 (no tags), pages 3-4 (tagged "invoice"), and pages 5-6 (tagged "receipt"). + ## Automatic collation of double-sided documents {#collate} !!! note @@ -851,8 +872,8 @@ followed by the even pages. It's important that the scan files get consumed in the correct order, and one at a time. You therefore need to make sure that Paperless is running while you upload the files into -the directory; and if you're using [polling](configuration.md#polling), make sure that -`CONSUMER_POLLING` is set to a value lower than it takes for the second scan to appear, +the directory; and if you're using polling, make sure that +`CONSUMER_POLLING_INTERVAL` is set to a value lower than it takes for the second scan to appear, like 5-10 or even lower. Another thing that might happen is that you start a double sided scan, but then forget diff --git a/docs/api.md b/docs/api.md index 1ac634162..ced8eb5b3 100644 --- a/docs/api.md +++ b/docs/api.md @@ -8,7 +8,7 @@ Further documentation is provided here for some endpoints and features. ## Authorization -The REST api provides four different forms of authentication. +The REST api provides five different forms of authentication. 1. Basic authentication @@ -52,6 +52,14 @@ The REST api provides four different forms of authentication. [configuration](configuration.md#PAPERLESS_ENABLE_HTTP_REMOTE_USER_API)), you can authenticate against the API using Remote User auth. +5. Headless OIDC via [`django-allauth`](https://codeberg.org/allauth/django-allauth) + + `django-allauth` exposes API endpoints under `api/auth/` which enable tools + like third-party apps to authenticate with social accounts that are + configured. See + [here](advanced_usage.md#openid-connect-and-social-authentication) for more + information on social accounts. + ## Searching for documents Full text searching is available on the `/api/documents/` endpoint. Two diff --git a/docs/changelog.md b/docs/changelog.md index 189c74ce1..f222a7305 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,9 +1,60 @@ # Changelog +## paperless-ngx 2.20.5 + +### Bug Fixes + +- Fix: ensure horizontal scroll for long tag names in list, wrap tags without parent [@shamoon](https://github.com/shamoon) ([#11811](https://github.com/paperless-ngx/paperless-ngx/pull/11811)) +- Fix: use explicit order field for workflow actions [@shamoon](https://github.com/shamoon) [@stumpylog](https://github.com/stumpylog) ([#11781](https://github.com/paperless-ngx/paperless-ngx/pull/11781)) + +### All App Changes + +
+2 changes + +- Fix: ensure horizontal scroll for long tag names in list, wrap tags without parent [@shamoon](https://github.com/shamoon) ([#11811](https://github.com/paperless-ngx/paperless-ngx/pull/11811)) +- Fix: use explicit order field for workflow actions [@shamoon](https://github.com/shamoon) [@stumpylog](https://github.com/stumpylog) ([#11781](https://github.com/paperless-ngx/paperless-ngx/pull/11781)) +
+ +## paperless-ngx 2.20.4 + +### Security + +- Resolve [GHSA-28cf-xvcf-hw6m](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-28cf-xvcf-hw6m) + +### Bug Fixes + +- Fix: propagate metadata override created value [@shamoon](https://github.com/shamoon) ([#11659](https://github.com/paperless-ngx/paperless-ngx/pull/11659)) +- Fix: support ordering by storage path name [@shamoon](https://github.com/shamoon) ([#11661](https://github.com/paperless-ngx/paperless-ngx/pull/11661)) +- Fix: validate cf integer values within PostgreSQL range [@shamoon](https://github.com/shamoon) ([#11666](https://github.com/paperless-ngx/paperless-ngx/pull/11666)) +- Fixhancement: add error handling and retry when opening index [@shamoon](https://github.com/shamoon) ([#11731](https://github.com/paperless-ngx/paperless-ngx/pull/11731)) +- Fix: fix recurring workflow to respect latest run time [@shamoon](https://github.com/shamoon) ([#11735](https://github.com/paperless-ngx/paperless-ngx/pull/11735)) + +### All App Changes + +
+5 changes + +- Fix: propagate metadata override created value [@shamoon](https://github.com/shamoon) ([#11659](https://github.com/paperless-ngx/paperless-ngx/pull/11659)) +- Fix: support ordering by storage path name [@shamoon](https://github.com/shamoon) ([#11661](https://github.com/paperless-ngx/paperless-ngx/pull/11661)) +- Fix: validate cf integer values within PostgreSQL range [@shamoon](https://github.com/shamoon) ([#11666](https://github.com/paperless-ngx/paperless-ngx/pull/11666)) +- Fixhancement: add error handling and retry when opening index [@shamoon](https://github.com/shamoon) ([#11731](https://github.com/paperless-ngx/paperless-ngx/pull/11731)) +- Fix: fix recurring workflow to respect latest run time [@shamoon](https://github.com/shamoon) ([#11735](https://github.com/paperless-ngx/paperless-ngx/pull/11735)) +
+ ## paperless-ngx 2.20.3 +### Security + +- Resolve [GHSA-7cq3-mhxq-w946](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-7cq3-mhxq-w946) + ## paperless-ngx 2.20.2 +### Security + +- Resolve [GHSA-6653-vcx4-69mc](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-6653-vcx4-69mc) +- Resolve [GHSA-24x5-wp64-9fcc](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-24x5-wp64-9fcc) + ### Features / Enhancements - Tweakhancement: dim inactive users in users-groups list [@shamoon](https://github.com/shamoon) ([#11537](https://github.com/paperless-ngx/paperless-ngx/pull/11537)) diff --git a/docs/configuration.md b/docs/configuration.md index e1f6f6d4c..32d56408f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -170,11 +170,18 @@ Available options are `postgresql` and `mariadb`. !!! note - A small pool is typically sufficient — for example, a size of 4. - Make sure your PostgreSQL server's max_connections setting is large enough to handle: - ```(Paperless workers + Celery workers) × pool size + safety margin``` - For example, with 4 Paperless workers and 2 Celery workers, and a pool size of 4: - (4 + 2) × 4 + 10 = 34 connections required. + A pool of 8-10 connections per worker is typically sufficient. + If you encounter error messages such as `couldn't get a connection` + or database connection timeouts, you probably need to increase the pool size. + + !!! warning + Make sure your PostgreSQL `max_connections` setting is large enough to handle the connection pools: + `(NB_PAPERLESS_WORKERS + NB_CELERY_WORKERS) × POOL_SIZE + SAFETY_MARGIN`. For example, with + 4 Paperless workers and 2 Celery workers, and a pool size of 8:``(4 + 2) × 8 + 10 = 58`, + so `max_connections = 60` (or even more) is appropriate. + + This assumes only Paperless-ngx connects to your PostgreSQL instance. If you have other applications, + you should increase `max_connections` accordingly. #### [`PAPERLESS_DB_READ_CACHE_ENABLED=`](#PAPERLESS_DB_READ_CACHE_ENABLED) {#PAPERLESS_DB_READ_CACHE_ENABLED} @@ -652,7 +659,7 @@ system. See the corresponding : Sync groups from the third party authentication system (e.g. OIDC) to Paperless-ngx. When enabled, users will be added or removed from groups based on their group membership in the third party authentication system. Groups must already exist in Paperless-ngx and have the same name as in the third party authentication system. Groups are updated upon logging in via the third party authentication system, see the corresponding [django-allauth documentation](https://docs.allauth.org/en/dev/socialaccount/signals.html). -: In order to pass groups from the authentication system you will need to update your [PAPERLESS_SOCIALACCOUNT_PROVIDERS](#PAPERLESS_SOCIALACCOUNT_PROVIDERS) setting by adding a top-level "SCOPES" setting which includes "groups", e.g.: +: In order to pass groups from the authentication system you will need to update your [PAPERLESS_SOCIALACCOUNT_PROVIDERS](#PAPERLESS_SOCIALACCOUNT_PROVIDERS) setting by adding a top-level "SCOPES" setting which includes "groups", or the custom groups claim configured in [`PAPERLESS_SOCIAL_ACCOUNT_SYNC_GROUPS_CLAIM`](#PAPERLESS_SOCIAL_ACCOUNT_SYNC_GROUPS_CLAIM) e.g.: ```json {"openid_connect":{"SCOPE": ["openid","profile","email","groups"]... @@ -660,6 +667,12 @@ system. See the corresponding Defaults to False +#### [`PAPERLESS_SOCIAL_ACCOUNT_SYNC_GROUPS_CLAIM=`](#PAPERLESS_SOCIAL_ACCOUNT_SYNC_GROUPS_CLAIM) {#PAPERLESS_SOCIAL_ACCOUNT_SYNC_GROUPS_CLAIM} + +: Allows you to define a custom groups claim. See [PAPERLESS_SOCIAL_ACCOUNT_SYNC_GROUPS](#PAPERLESS_SOCIAL_ACCOUNT_SYNC_GROUPS) which is required for this setting to take effect. + + Defaults to "groups" + #### [`PAPERLESS_SOCIAL_ACCOUNT_DEFAULT_GROUPS=`](#PAPERLESS_SOCIAL_ACCOUNT_DEFAULT_GROUPS) {#PAPERLESS_SOCIAL_ACCOUNT_DEFAULT_GROUPS} : A list of group names that users who signup via social accounts will be added to upon signup. Groups listed here must already exist. @@ -1139,8 +1152,9 @@ via the consumption directory, you can disable the consumer to save resources. #### [`PAPERLESS_CONSUMER_DELETE_DUPLICATES=`](#PAPERLESS_CONSUMER_DELETE_DUPLICATES) {#PAPERLESS_CONSUMER_DELETE_DUPLICATES} -: When the consumer detects a duplicate document, it will not touch -the original document. This default behavior can be changed here. +: As of version 3.0 Paperless-ngx allows duplicate documents to be consumed by default, _except_ when +this setting is enabled. When enabled, Paperless will check if a document with the same hash already +exists in the system and delete the duplicate file from the consumption directory without consuming it. Defaults to false. @@ -1168,21 +1182,45 @@ don't exist yet. #### [`PAPERLESS_CONSUMER_IGNORE_PATTERNS=`](#PAPERLESS_CONSUMER_IGNORE_PATTERNS) {#PAPERLESS_CONSUMER_IGNORE_PATTERNS} -: By default, paperless ignores certain files and folders in the -consumption directory, such as system files created by the Mac OS -or hidden folders some tools use to store data. +: Additional regex patterns for files to ignore in the consumption directory. Patterns are matched against filenames only (not full paths) +using Python's `re.match()`, which anchors at the start of the filename. - This can be adjusted by configuring a custom json array with - patterns to exclude. + See the [watchfiles documentation](https://watchfiles.helpmanual.io/api/filters/#watchfiles.BaseFilter.ignore_entity_patterns) - For example, `.DS_STORE/*` will ignore any files found in a folder - named `.DS_STORE`, including `.DS_STORE/bar.pdf` and `foo/.DS_STORE/bar.pdf` + This setting is for additional patterns beyond the built-in defaults. Common system files and directories are already ignored automatically. + The patterns will be compiled via Python's standard `re` module. - A pattern like `._*` will ignore anything starting with `._`, including: - `._foo.pdf` and `._bar/foo.pdf` + Example custom patterns: - Defaults to - `[".DS_Store", ".DS_STORE", "._*", ".stfolder/*", ".stversions/*", ".localized/*", "desktop.ini", "@eaDir/*", "Thumbs.db"]`. + ```json + ["^temp_", "\\.bak$", "^~"] + ``` + + This would ignore: + + - Files starting with `temp_` (e.g., `temp_scan.pdf`) + - Files ending with `.bak` (e.g., `document.pdf.bak`) + - Files starting with `~` (e.g., `~$document.docx`) + + Defaults to `[]` (empty list, uses only built-in defaults). + + The default ignores are `[.DS_Store, .DS_STORE, ._*, desktop.ini, Thumbs.db]` and cannot be overridden. + +#### [`PAPERLESS_CONSUMER_IGNORE_DIRS=`](#PAPERLESS_CONSUMER_IGNORE_DIRS) {#PAPERLESS_CONSUMER_IGNORE_DIRS} + +: Additional directory names to ignore in the consumption directory. Directories matching these names (and all their contents) will be skipped. + + This setting is for additional directories beyond the built-in defaults. Matching is done by directory name only, not full path. + + Example: + + ```json + ["temp", "incoming", ".hidden"] + ``` + + Defaults to `[]` (empty list, uses only built-in defaults). + + The default ignores are `[.stfolder, .stversions, .localized, @eaDir, .Spotlight-V100, .Trashes, __MACOSX]` and cannot be overridden. #### [`PAPERLESS_CONSUMER_BARCODE_SCANNER=`](#PAPERLESS_CONSUMER_BARCODE_SCANNER) {#PAPERLESS_CONSUMER_BARCODE_SCANNER} @@ -1281,48 +1319,24 @@ within your documents. Defaults to false. -### Polling {#polling} +#### [`PAPERLESS_CONSUMER_POLLING_INTERVAL=`](#PAPERLESS_CONSUMER_POLLING_INTERVAL) {#PAPERLESS_CONSUMER_POLLING_INTERVAL} -#### [`PAPERLESS_CONSUMER_POLLING=`](#PAPERLESS_CONSUMER_POLLING) {#PAPERLESS_CONSUMER_POLLING} +: Configures how the consumer detects new files in the consumption directory. -: If paperless won't find documents added to your consume folder, it -might not be able to automatically detect filesystem changes. In -that case, specify a polling interval in seconds here, which will -then cause paperless to periodically check your consumption -directory for changes. This will also disable listening for file -system changes with `inotify`. + When set to `0` (default), paperless uses native filesystem notifications for efficient, immediate detection of new files. - Defaults to 0, which disables polling and uses filesystem - notifications. + When set to a positive number, paperless polls the consumption directory at that interval in seconds. Use polling for network filesystems (NFS, SMB/CIFS) where native notifications may not work reliably. -#### [`PAPERLESS_CONSUMER_POLLING_RETRY_COUNT=`](#PAPERLESS_CONSUMER_POLLING_RETRY_COUNT) {#PAPERLESS_CONSUMER_POLLING_RETRY_COUNT} + Defaults to 0. -: If consumer polling is enabled, sets the maximum number of times -paperless will check for a file to remain unmodified. If a file's -modification time and size are identical for two consecutive checks, it -will be consumed. +#### [`PAPERLESS_CONSUMER_STABILITY_DELAY=`](#PAPERLESS_CONSUMER_STABILITY_DELAY) {#PAPERLESS_CONSUMER_STABILITY_DELAY} - Defaults to 5. +: Sets the time in seconds that a file must remain unchanged (same size and modification time) before paperless will begin consuming it. -#### [`PAPERLESS_CONSUMER_POLLING_DELAY=`](#PAPERLESS_CONSUMER_POLLING_DELAY) {#PAPERLESS_CONSUMER_POLLING_DELAY} + Increase this value if you experience issues with files being consumed before they are fully written, particularly on slower network storage or + with certain scanner quirks -: If consumer polling is enabled, sets the delay in seconds between -each check (above) paperless will do while waiting for a file to -remain unmodified. - - Defaults to 5. - -### iNotify {#inotify} - -#### [`PAPERLESS_CONSUMER_INOTIFY_DELAY=`](#PAPERLESS_CONSUMER_INOTIFY_DELAY) {#PAPERLESS_CONSUMER_INOTIFY_DELAY} - -: Sets the time in seconds the consumer will wait for additional -events from inotify before the consumer will consider a file ready -and begin consumption. Certain scanners or network setups may -generate multiple events for a single file, leading to multiple -consumers working on the same file. Configure this to prevent that. - - Defaults to 0.5 seconds. + Defaults to 5.0 seconds. ## Workflow webhooks @@ -1543,6 +1557,20 @@ assigns or creates tags if a properly formatted barcode is detected. Please refer to the Python regex documentation for more information. +#### [`PAPERLESS_CONSUMER_TAG_BARCODE_SPLIT=`](#PAPERLESS_CONSUMER_TAG_BARCODE_SPLIT) {#PAPERLESS_CONSUMER_TAG_BARCODE_SPLIT} + +: Enables splitting of documents on tag barcodes, similar to how ASN barcodes work. + + When enabled, documents will be split into separate PDFs at pages containing + tag barcodes that match the configured `PAPERLESS_CONSUMER_TAG_BARCODE_MAPPING` + patterns. The page with the tag barcode will be retained in the new document. + + Each split document will have the detected tags assigned to it. + + This only has an effect if `PAPERLESS_CONSUMER_ENABLE_TAG_BARCODE` is also enabled. + + Defaults to false. + ## Audit Trail #### [`PAPERLESS_AUDIT_LOG_ENABLED=`](#PAPERLESS_AUDIT_LOG_ENABLED) {#PAPERLESS_AUDIT_LOG_ENABLED} @@ -1603,6 +1631,16 @@ processing. This only has an effect if Defaults to `0 1 * * *`, once per day. +## Share links + +#### [`PAPERLESS_SHARE_LINK_BUNDLE_CLEANUP_CRON=`](#PAPERLESS_SHARE_LINK_BUNDLE_CLEANUP_CRON) {#PAPERLESS_SHARE_LINK_BUNDLE_CLEANUP_CRON} + +: Controls how often Paperless-ngx removes expired share link bundles (and their generated ZIP archives). + +: If set to the string "disable", expired bundles are not cleaned up automatically. + + Defaults to `0 2 * * *`, once per day at 02:00. + ## Binaries There are a few external software packages that Paperless expects to @@ -1824,3 +1862,67 @@ password. All of these options come from their similarly-named [Django settings] : The endpoint to use for the remote OCR engine. This is required for Azure AI. Defaults to None. + +## AI {#ai} + +#### [`PAPERLESS_AI_ENABLED=`](#PAPERLESS_AI_ENABLED) {#PAPERLESS_AI_ENABLED} + +: Enables the AI features in Paperless. This includes the AI-based +suggestions. This setting is required to be set to true in order to use the AI features. + + Defaults to false. + +#### [`PAPERLESS_AI_LLM_EMBEDDING_BACKEND=`](#PAPERLESS_AI_LLM_EMBEDDING_BACKEND) {#PAPERLESS_AI_LLM_EMBEDDING_BACKEND} + +: The embedding backend to use for RAG. This can be either "openai" or "huggingface". + + Defaults to None. + +#### [`PAPERLESS_AI_LLM_EMBEDDING_MODEL=`](#PAPERLESS_AI_LLM_EMBEDDING_MODEL) {#PAPERLESS_AI_LLM_EMBEDDING_MODEL} + +: The model to use for the embedding backend for RAG. This can be set to any of the embedding models supported by the current embedding backend. If not supplied, defaults to "text-embedding-3-small" for OpenAI and "sentence-transformers/all-MiniLM-L6-v2" for Huggingface. + + Defaults to None. + +#### [`PAPERLESS_AI_LLM_BACKEND=`](#PAPERLESS_AI_LLM_BACKEND) {#PAPERLESS_AI_LLM_BACKEND} + +: The AI backend to use. This can be either "openai" or "ollama". If set to "ollama", the AI +features will be run locally on your machine. If set to "openai", the AI features will be run +using the OpenAI API. This setting is required to be set to use the AI features. + + Defaults to None. + + !!! note + + The OpenAI API is a paid service. You will need to set up an OpenAI account and + will be charged for usage incurred by Paperless-ngx features and your document data + will (of course) be sent to the OpenAI API. Paperless-ngx does not endorse the use of the + OpenAI API in any way. + + Refer to the OpenAI terms of service, and use at your own risk. + +#### [`PAPERLESS_AI_LLM_MODEL=`](#PAPERLESS_AI_LLM_MODEL) {#PAPERLESS_AI_LLM_MODEL} + +: The model to use for the AI backend, i.e. "gpt-3.5-turbo", "gpt-4" or any of the models supported by the +current backend. If not supplied, defaults to "gpt-3.5-turbo" for OpenAI and "llama3.1" for Ollama. + + Defaults to None. + +#### [`PAPERLESS_AI_LLM_API_KEY=`](#PAPERLESS_AI_LLM_API_KEY) {#PAPERLESS_AI_LLM_API_KEY} + +: The API key to use for the AI backend. This is required for the OpenAI backend (optional for others). + + Defaults to None. + +#### [`PAPERLESS_AI_LLM_ENDPOINT=`](#PAPERLESS_AI_LLM_ENDPOINT) {#PAPERLESS_AI_LLM_ENDPOINT} + +: The endpoint / url to use for the AI backend. This is required for the Ollama backend (optional for others). + + Defaults to None. + +#### [`PAPERLESS_AI_LLM_INDEX_TASK_CRON=`](#PAPERLESS_AI_LLM_INDEX_TASK_CRON) {#PAPERLESS_AI_LLM_INDEX_TASK_CRON} + +: Configures the schedule to update the AI embeddings of text content and metadata for all documents. Only performed if +AI is enabled and the LLM embedding backend is set. + + Defaults to `10 2 * * *`, once per day. diff --git a/docs/index.md b/docs/index.md index c84cd0ce4..1d72f8f6c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -31,6 +31,7 @@ physical documents into a searchable online archive so you can keep, well, _less - _New!_ Supports remote OCR with Azure AI (opt-in). - Documents are saved as PDF/A format which is designed for long term storage, alongside the unaltered originals. - Uses machine-learning to automatically add tags, correspondents and document types to your documents. +- **New**: Paperless-ngx can now leverage AI (Large Language Models or LLMs) for document suggestions. This is an optional feature that can be enabled (and is disabled by default). - Supports PDF documents, images, plain text files, Office documents (Word, Excel, PowerPoint, and LibreOffice equivalents)[^1] and more. - Paperless stores your documents plain on disk. Filenames and folders are managed by paperless and their format can be configured freely with different configurations assigned to different documents. - **Beautiful, modern web application** that features: diff --git a/docs/migration.md b/docs/migration.md new file mode 100644 index 000000000..1c934e6df --- /dev/null +++ b/docs/migration.md @@ -0,0 +1,25 @@ +# v3 Migration Guide + +## Consumer Settings Changes + +The v3 consumer command uses a [different library](https://watchfiles.helpmanual.io/) to unify +the watching for new files in the consume directory. For the user, this removes several configuration options related to delays and retries +and replaces with a single unified setting. It also adjusts how the consumer ignore filtering happens, replaced `fnmatch` with `regex` and +separating the directory ignore from the file ignore. + +### Summary + +| Old Setting | New Setting | Notes | +| ------------------------------ | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | +| `CONSUMER_POLLING` | [`CONSUMER_POLLING_INTERVAL`](configuration.md#PAPERLESS_CONSUMER_POLLING_INTERVAL) | Renamed for clarity | +| `CONSUMER_INOTIFY_DELAY` | [`CONSUMER_STABILITY_DELAY`](configuration.md#PAPERLESS_CONSUMER_STABILITY_DELAY) | Unified for all modes | +| `CONSUMER_POLLING_DELAY` | _Removed_ | Use `CONSUMER_STABILITY_DELAY` | +| `CONSUMER_POLLING_RETRY_COUNT` | _Removed_ | Automatic with stability tracking | +| `CONSUMER_IGNORE_PATTERNS` | [`CONSUMER_IGNORE_PATTERNS`](configuration.md#PAPERLESS_CONSUMER_IGNORE_PATTERNS) | **Now regex, not fnmatch**; user patterns are added to (not replacing) default ones | +| _New_ | [`CONSUMER_IGNORE_DIRS`](configuration.md#PAPERLESS_CONSUMER_IGNORE_DIRS) | Additional directories to ignore; user entries are added to (not replacing) defaults | + +## Encryption Support + +Document and thumbnail encryption is no longer supported. This was previously deprecated in [paperless-ng 0.9.3](https://github.com/paperless-ngx/paperless-ngx/blob/dev/docs/changelog.md#paperless-ng-093) + +Users must decrypt their document using the `decrypt_documents` command before upgrading. diff --git a/docs/setup.md b/docs/setup.md index 3e7ac1be3..f0381f076 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -124,8 +124,7 @@ account. The script essentially automatically performs the steps described in [D system notifications with `inotify`. When storing the consumption directory on such a file system, paperless will not pick up new files with the default configuration. You will need to use - [`PAPERLESS_CONSUMER_POLLING`](configuration.md#PAPERLESS_CONSUMER_POLLING), which will disable inotify. See - [here](configuration.md#polling). + [`PAPERLESS_CONSUMER_POLLING_INTERVAL`](configuration.md#PAPERLESS_CONSUMER_POLLING_INTERVAL), which will disable inotify. 5. Run `docker compose pull`. This will pull the image from the GitHub container registry by default but you can change the image to pull from Docker Hub by changing the `image` diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index e20751875..94e12307e 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -46,9 +46,9 @@ run: If you notice that the consumer will only pickup files in the consumption directory at startup, but won't find any other files added later, you will need to enable filesystem polling with the configuration -option [`PAPERLESS_CONSUMER_POLLING`](configuration.md#PAPERLESS_CONSUMER_POLLING). +option [`PAPERLESS_CONSUMER_POLLING_INTERVAL`](configuration.md#PAPERLESS_CONSUMER_POLLING_INTERVAL). -This will disable listening to filesystem changes with inotify and +This will disable automatic listening for filesystem changes and paperless will manually check the consumption directory for changes instead. @@ -234,47 +234,9 @@ FileNotFoundError: [Errno 2] No such file or directory: '/tmp/ocrmypdf.io.yhk3zb This probably indicates paperless tried to consume the same file twice. This can happen for a number of reasons, depending on how documents are -placed into the consume folder. If paperless is using inotify (the -default) to check for documents, try adjusting the -[inotify configuration](configuration.md#inotify). If polling is enabled, try adjusting the -[polling configuration](configuration.md#polling). - -## Consumer fails waiting for file to remain unmodified. - -You might find messages like these in your log files: - -``` -[ERROR] [paperless.management.consumer] Timeout while waiting on file /usr/src/paperless/src/../consume/SCN_0001.pdf to remain unmodified. -``` - -This indicates paperless timed out while waiting for the file to be -completely written to the consume folder. Adjusting -[polling configuration](configuration.md#polling) values should resolve the issue. - -!!! note - - The user will need to manually move the file out of the consume folder - and back in, for the initial failing file to be consumed. - -## Consumer fails reporting "OS reports file as busy still". - -You might find messages like these in your log files: - -``` -[WARNING] [paperless.management.consumer] Not consuming file /usr/src/paperless/src/../consume/SCN_0001.pdf: OS reports file as busy still -``` - -This indicates paperless was unable to open the file, as the OS reported -the file as still being in use. To prevent a crash, paperless did not -try to consume the file. If paperless is using inotify (the default) to -check for documents, try adjusting the -[inotify configuration](configuration.md#inotify). If polling is enabled, try adjusting the -[polling configuration](configuration.md#polling). - -!!! note - - The user will need to manually move the file out of the consume folder - and back in, for the initial failing file to be consumed. +placed into the consume folder, such as how a scanner may modify a file multiple times as it scans. +Try adjusting the +[file stability delay](configuration.md#PAPERLESS_CONSUMER_STABILITY_DELAY) to a larger value. ## Log reports "Creating PaperlessTask failed". diff --git a/docs/usage.md b/docs/usage.md index a307db3cd..f652164da 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -278,6 +278,28 @@ Once setup, navigating to the email settings page in Paperless-ngx will allow yo You can also submit a document using the REST API, see [POSTing documents](api.md#file-uploads) for details. +## Document Suggestions + +Paperless-ngx can suggest tags, correspondents, document types and storage paths for documents based on the content of the document. This is done using a (non-LLM) machine learning model that is trained on the documents in your database. The suggestions are shown in the document detail page and can be accepted or rejected by the user. + +## AI Features + +Paperless-ngx includes several features that use AI to enhance the document management experience. These features are optional and can be enabled or disabled in the settings. If you are using the AI features, you may want to also enable the "LLM index" feature, which supports Retrieval-Augmented Generation (RAG) designed to improve the quality of AI responses. The LLM index feature is not enabled by default and requires additional configuration. + +!!! warning + + Remember that Paperless-ngx will send document content to the AI provider you have configured, so consider the privacy implications of using these features, especially if using a remote model (e.g. OpenAI), instead of the default local model. + +The AI features work by creating an embedding of the text content and metadata of documents, which is then used for various tasks such as similarity search and question answering. This uses the FAISS vector store. + +### AI-Enhanced Suggestions + +If enabled, Paperless-ngx can use an AI LLM model to suggest document titles, dates, tags, correspondents and document types for documents. This feature will always be "opt-in" and does not disable the existing classifier-based suggestion system. Currently, both remote (via the OpenAI API) and local (via Ollama) models are supported, see [configuration](configuration.md#ai) for details. + +### Document Chat + +Paperless-ngx can use an AI LLM model to answer questions about a document or across multiple documents. Again, this feature works best when RAG is enabled. The chat feature is available in the upper app toolbar and will switch between chatting across multiple documents or a single document based on the current view. + ## Sharing documents from Paperless-ngx Paperless-ngx supports sharing documents with other users by assigning them [permissions](#object-permissions) @@ -286,12 +308,14 @@ or using [email](#workflow-action-email) or [webhook](#workflow-action-webhook) ### Share Links -"Share links" are shareable public links to files and can be created and managed under the 'Send' button on the document detail screen. +"Share links" are public links to files (or an archive of files) and can be created and managed under the 'Send' button on the document detail screen or from the bulk editor. -- Share links do not require a user to login and thus link directly to a file. +- Share links do not require a user to login and thus link directly to a file or bundled download. - Links are unique and are of the form `{paperless-url}/share/{randomly-generated-slug}`. - Links can optionally have an expiration time set. - After a link expires or is deleted users will be redirected to the regular paperless-ngx login. +- From the document detail screen you can create a share link for that single document. +- From the bulk editor you can create a **share link bundle** for any selection. Paperless-ngx prepares a ZIP archive in the background and exposes a single share link. You can revisit the "Manage share link bundles" dialog to monitor progress, retry failed bundles, or delete links. !!! tip @@ -543,7 +567,7 @@ This allows for complex logic to be used to generate the title, including [logic and [filters](https://jinja.palletsprojects.com/en/3.1.x/templates/#id11). The template is provided as a string. -Using Jinja2 Templates is also useful for [Date localization](advanced_usage.md#Date-Localization) in the title. +Using Jinja2 Templates is also useful for [Date localization](advanced_usage.md#date-localization) in the title. The available inputs differ depending on the type of workflow trigger. This is because at the time of consumption (when the text is to be set), no automatic tags etc. have been @@ -575,6 +599,7 @@ The following placeholders are only available for "added" or "updated" triggers - `{{created_day}}`: created day - `{{created_time}}`: created time in HH:MM format - `{{doc_url}}`: URL to the document in the web UI. Requires the `PAPERLESS_URL` setting to be set. +- `{{doc_id}}`: Document ID ##### Examples diff --git a/mkdocs.yml b/mkdocs.yml index 05826f25f..69a15193a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -69,8 +69,9 @@ nav: - development.md - 'FAQs': faq.md - troubleshooting.md + - 'Migration to v3': migration.md - changelog.md -copyright: Copyright © 2016 - 2023 Daniel Quinn, Jonas Winkler, and the Paperless-ngx team +copyright: Copyright © 2016 - 2026 Daniel Quinn, Jonas Winkler, and the Paperless-ngx team extra: social: - icon: fontawesome/brands/github diff --git a/paperless-ngx.code-workspace b/paperless-ngx.code-workspace index 85f5a836b..91020c66c 100644 --- a/paperless-ngx.code-workspace +++ b/paperless-ngx.code-workspace @@ -33,6 +33,8 @@ "**/coverage.json": true }, "python.defaultInterpreterPath": ".venv/bin/python3", + "python.analysis.inlayHints.pytestParameters": true, + "python.testing.pytestEnabled": true, }, "extensions": { "recommendations": ["ms-python.python", "charliermarsh.ruff", "editorconfig.editorconfig"], diff --git a/paperless.conf.example b/paperless.conf.example index 1ba21f41d..9974aeab6 100644 --- a/paperless.conf.example +++ b/paperless.conf.example @@ -55,10 +55,10 @@ #PAPERLESS_TASK_WORKERS=1 #PAPERLESS_THREADS_PER_WORKER=1 #PAPERLESS_TIME_ZONE=UTC -#PAPERLESS_CONSUMER_POLLING=10 +#PAPERLESS_CONSUMER_POLLING_INTERVAL=10 #PAPERLESS_CONSUMER_DELETE_DUPLICATES=false #PAPERLESS_CONSUMER_RECURSIVE=false -#PAPERLESS_CONSUMER_IGNORE_PATTERNS=[".DS_STORE/*", "._*", ".stfolder/*", ".stversions/*", ".localized/*", "desktop.ini"] +#PAPERLESS_CONSUMER_IGNORE_PATTERNS=[] # Defaults are built in; add filename regexes, e.g. ["^\\.DS_Store$", "^desktop\\.ini$"] #PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS=false #PAPERLESS_CONSUMER_ENABLE_BARCODES=false #PAPERLESS_CONSUMER_BARCODE_STRING=PATCHT @@ -66,6 +66,7 @@ #PAPERLESS_CONSUMER_BARCODE_DPI=300 #PAPERLESS_CONSUMER_ENABLE_TAG_BARCODE=false #PAPERLESS_CONSUMER_TAG_BARCODE_MAPPING={"TAG:(.*)": "\\g<1>"} +#PAPERLESS_CONSUMER_TAG_BARCODE_SPLIT=false #PAPERLESS_CONSUMER_ENABLE_COLLATE_DOUBLE_SIDED=false #PAPERLESS_CONSUMER_COLLATE_DOUBLE_SIDED_SUBDIR_NAME=double-sided #PAPERLESS_CONSUMER_COLLATE_DOUBLE_SIDED_TIFF_SUPPORT=false diff --git a/pyproject.toml b/pyproject.toml index fb47e55f1..b25c4ea34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "paperless-ngx" -version = "2.20.3" +version = "2.20.5" description = "A community-supported supercharged document management system: scan, index and archive all your physical documents" readme = "README.md" requires-python = ">=3.10" @@ -19,16 +19,16 @@ dependencies = [ "azure-ai-documentintelligence>=1.0.2", "babel>=2.17", "bleach~=6.3.0", - "celery[redis]~=5.5.1", + "celery[redis]~=5.6.2", "channels~=4.2", "channels-redis~=4.2", "concurrent-log-handler~=0.9.25", "dateparser~=1.2", # WARNING: django does not use semver. # Only patch versions are guaranteed to not introduce breaking changes. - "django~=5.2.5", - "django-allauth[mfa,socialaccount]~=65.12.1", - "django-auditlog~=3.3.0", + "django~=5.2.10", + "django-allauth[mfa,socialaccount]~=65.13.1", + "django-auditlog~=3.4.1", "django-cachalot~=2.8.0", "django-celery-results~=2.6.0", "django-compression-middleware~=0.5.0", @@ -44,16 +44,23 @@ dependencies = [ "drf-spectacular~=0.28", "drf-spectacular-sidecar~=2025.10.1", "drf-writable-nested~=0.7.1", + "faiss-cpu>=1.10", "filelock~=3.20.0", "flower~=2.0.1", - "gotenberg-client~=0.12.0", + "gotenberg-client~=0.13.1", "httpx-oauth~=0.16", "imap-tools~=1.11.0", - "inotifyrecursive~=0.3", "jinja2~=3.1.5", "langdetect~=1.0.9", + "llama-index-core>=0.14.12", + "llama-index-embeddings-huggingface>=0.6.1", + "llama-index-embeddings-openai>=0.5.1", + "llama-index-llms-ollama>=0.9.1", + "llama-index-llms-openai>=0.6.13", + "llama-index-vector-stores-faiss>=0.5.2", "nltk~=3.9.1", - "ocrmypdf~=16.12.0", + "ocrmypdf~=16.13.0", + "openai>=1.76", "pathvalidate~=3.3.1", "pdf2image~=1.17.0", "python-dateutil~=2.9.0", @@ -66,11 +73,13 @@ dependencies = [ "redis[hiredis]~=5.2.1", "regex>=2025.9.18", "scikit-learn~=1.7.0", + "sentence-transformers>=4.1", "setproctitle~=1.3.4", "tika-client~=0.10.0", + "torch~=2.9.1", "tqdm~=4.67.1", - "watchdog~=6.0", - "whitenoise~=6.9", + "watchfiles>=1.1.1", + "whitenoise~=6.11", "whoosh-reloaded>=2.7.5", "zxing-cpp~=2.3.0", ] @@ -79,13 +88,13 @@ optional-dependencies.mariadb = [ "mysqlclient~=2.2.7", ] optional-dependencies.postgres = [ - "psycopg[c,pool]==3.2.12", + "psycopg[c,pool]==3.3", # Direct dependency for proper resolution of the pre-built wheels - "psycopg-c==3.2.12", - "psycopg-pool==3.2.7", + "psycopg-c==3.3", + "psycopg-pool==3.3", ] optional-dependencies.webserver = [ - "granian[uvloop]~=2.5.1", + "granian[uvloop]~=2.6.0", ] [dependency-groups] @@ -105,19 +114,20 @@ testing = [ "daphne", "factory-boy~=3.3.1", "imagehash", - "pytest~=8.4.1", + "pytest~=9.0.0", "pytest-cov~=7.0.0", "pytest-django~=4.11.1", - "pytest-env", + "pytest-env~=1.2.0", "pytest-httpx", - "pytest-mock", - "pytest-rerunfailures", + "pytest-mock~=3.15.1", + #"pytest-randomly~=4.0.1", + "pytest-rerunfailures~=16.1", "pytest-sugar", - "pytest-xdist", + "pytest-xdist~=3.8.0", ] lint = [ - "pre-commit~=4.4.0", + "pre-commit~=4.5.1", "pre-commit-uv~=4.2.0", "ruff~=0.14.0", ] @@ -142,7 +152,7 @@ typing = [ ] [tool.uv] -required-version = ">=0.5.14" +required-version = ">=0.9.0" package = false environments = [ "sys_platform == 'darwin'", @@ -152,14 +162,23 @@ environments = [ [tool.uv.sources] # Markers are chosen to select these almost exclusively when building the Docker image psycopg-c = [ - { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-bookworm-3.2.12/psycopg_c-3.2.12-cp312-cp312-linux_x86_64.whl", marker = "sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.12'" }, - { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-bookworm-3.2.12/psycopg_c-3.2.12-cp312-cp312-linux_aarch64.whl", marker = "sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.12'" }, + { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-trixie-3.3.0/psycopg_c-3.3.0-cp312-cp312-linux_x86_64.whl", marker = "sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.12'" }, + { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-trixie-3.3.0/psycopg_c-3.3.0-cp312-cp312-linux_aarch64.whl", marker = "sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.12'" }, ] zxing-cpp = [ { url = "https://github.com/paperless-ngx/builder/releases/download/zxing-2.3.0/zxing_cpp-2.3.0-cp312-cp312-linux_x86_64.whl", marker = "sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.12'" }, { url = "https://github.com/paperless-ngx/builder/releases/download/zxing-2.3.0/zxing_cpp-2.3.0-cp312-cp312-linux_aarch64.whl", marker = "sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.12'" }, ] +torch = [ + { index = "pytorch-cpu" }, +] + +[[tool.uv.index]] +name = "pytorch-cpu" +url = "https://download.pytorch.org/whl/cpu" +explicit = true + [tool.ruff] target-version = "py310" line-length = 88 @@ -239,14 +258,18 @@ lint.isort.force-single-line = true [tool.codespell] write-changes = true -ignore-words-list = "criterias,afterall,valeu,ureue,equest,ure,assertIn,Oktober" +ignore-words-list = "criterias,afterall,valeu,ureue,equest,ure,assertIn,Oktober,commitish" skip = "src-ui/src/locale/*,src-ui/pnpm-lock.yaml,src-ui/e2e/*,src/paperless_mail/tests/samples/*,src/documents/tests/samples/*,*.po,*.json" -[tool.pytest.ini_options] -minversion = "8.0" -pythonpath = [ - "src", -] +[tool.pytest] +minversion = "9.0" +pythonpath = [ "src" ] + +strict_config = true +strict_markers = true +strict_parametrization_ids = true +strict_xfail = true + testpaths = [ "src/documents/tests/", "src/paperless/tests/", @@ -255,7 +278,9 @@ testpaths = [ "src/paperless_tika/tests", "src/paperless_text/tests/", "src/paperless_remote/tests/", + "src/paperless_ai/tests", ] + addopts = [ "--pythonwarnings=all", "--cov", @@ -263,15 +288,26 @@ addopts = [ "--cov-report=xml", "--numprocesses=auto", "--maxprocesses=16", - "--quiet", + "--dist=loadscope", "--durations=50", + "--durations-min=0.5", "--junitxml=junit.xml", - "-o junit_family=legacy", + "-o", + "junit_family=legacy", ] + norecursedirs = [ "src/locale/", ".venv/", "src-ui/" ] DJANGO_SETTINGS_MODULE = "paperless.settings" +markers = [ + "live: Integration tests requiring external services (Gotenberg, Tika, nginx, etc)", + "nginx: Tests that make HTTP requests to the local nginx service", + "gotenberg: Tests requiring Gotenberg service", + "tika: Tests requiring Tika service", + "greenmail: Tests requiring Greenmail service", +] + [tool.pytest_env] PAPERLESS_DISABLE_DBHANDLER = "true" PAPERLESS_CACHE_BACKEND = "django.core.cache.backends.locmem.LocMemCache" diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index a27f4f72e..63fdc9d90 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -297,11 +297,11 @@ src/app/components/app-frame/app-frame.component.html - 84 + 87 src/app/components/app-frame/app-frame.component.html - 86 + 89 src/app/components/dashboard/dashboard.component.html @@ -315,12 +315,28 @@ 152 - src/app/components/app-frame/app-frame.component.html - 91 + src/app/components/admin/settings/settings.component.html + 193 + + + src/app/components/admin/settings/settings.component.html + 197 src/app/components/app-frame/app-frame.component.html - 93 + 94 + + + src/app/components/app-frame/app-frame.component.html + 96 + + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 85 + + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 36 src/app/components/document-list/document-list.component.ts @@ -332,19 +348,19 @@ src/app/components/manage/management-list/management-list.component.html - 151 + 182 src/app/components/manage/management-list/management-list.component.html - 151 + 182 src/app/components/manage/management-list/management-list.component.html - 151 + 182 src/app/components/manage/management-list/management-list.component.html - 151 + 182 @@ -359,15 +375,15 @@ src/app/components/app-frame/app-frame.component.html - 51 + 54 src/app/components/app-frame/app-frame.component.html - 255 + 258 src/app/components/app-frame/app-frame.component.html - 257 + 260 @@ -385,7 +401,7 @@ src/app/components/document-detail/document-detail.component.html - 119 + 109 @@ -530,22 +546,22 @@ Discard src/app/components/admin/config/config.component.html - 53 + 57 src/app/components/document-detail/document-detail.component.html - 380 + 437 Save src/app/components/admin/config/config.component.html - 56 + 60 src/app/components/admin/settings/settings.component.html - 362 + 400 src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.html @@ -593,7 +609,7 @@ src/app/components/document-detail/document-detail.component.html - 373 + 430 src/app/components/document-list/bulk-editor/custom-fields-bulk-edit-dialog/custom-fields-bulk-edit-dialog.component.html @@ -612,42 +628,42 @@ Error retrieving config src/app/components/admin/config/config.component.ts - 103 + 105 Invalid JSON src/app/components/admin/config/config.component.ts - 129 + 131 Configuration updated src/app/components/admin/config/config.component.ts - 173 + 175 An error occurred updating configuration src/app/components/admin/config/config.component.ts - 178 + 180 File successfully updated src/app/components/admin/config/config.component.ts - 200 + 202 An error occurred uploading file src/app/components/admin/config/config.component.ts - 205 + 207 @@ -658,11 +674,11 @@ src/app/components/app-frame/app-frame.component.html - 290 + 293 src/app/components/app-frame/app-frame.component.html - 293 + 296 @@ -761,7 +777,7 @@ src/app/components/document-detail/document-detail.component.html - 393 + 450 src/app/components/document-list/document-list.component.html @@ -789,19 +805,19 @@ src/app/components/manage/management-list/management-list.component.html - 52 + 83 src/app/components/manage/management-list/management-list.component.html - 52 + 83 src/app/components/manage/management-list/management-list.component.html - 52 + 83 src/app/components/manage/management-list/management-list.component.html - 52 + 83 src/app/components/manage/saved-views/saved-views.component.html @@ -914,92 +930,132 @@ 99,100 - - Items per page - - src/app/components/admin/settings/settings.component.html - 107 - - Sidebar src/app/components/admin/settings/settings.component.html - 123 + 107 Use 'slim' sidebar (icons only) src/app/components/admin/settings/settings.component.html - 127 + 111 Dark mode src/app/components/admin/settings/settings.component.html - 134 + 118 Use system settings src/app/components/admin/settings/settings.component.html - 137 + 121 Enable dark mode src/app/components/admin/settings/settings.component.html - 138 + 122 Invert thumbnails in dark mode src/app/components/admin/settings/settings.component.html - 139 + 123 Theme Color src/app/components/admin/settings/settings.component.html - 145 + 129 Reset src/app/components/admin/settings/settings.component.html - 152 + 136 + + + + Global search + + src/app/components/admin/settings/settings.component.html + 142 + + + src/app/components/app-frame/global-search/global-search.component.ts + 122 + + + + Do not include advanced search results + + src/app/components/admin/settings/settings.component.html + 145 + + + + Full search links to + + src/app/components/admin/settings/settings.component.html + 151 + + + + Title and content search + + src/app/components/admin/settings/settings.component.html + 155 + + + + Advanced search + + src/app/components/admin/settings/settings.component.html + 156 + + + src/app/components/app-frame/global-search/global-search.component.html + 24 + + + src/app/components/document-list/filter-editor/filter-editor.component.ts + 208 Update checking src/app/components/admin/settings/settings.component.html - 157 + 161 Enable update checking src/app/components/admin/settings/settings.component.html - 160 + 164 What's this? src/app/components/admin/settings/settings.component.html - 161 + 165 src/app/components/common/page-header/page-header.component.html - 9 + 18 src/app/components/common/permissions-select/permissions-select.component.html @@ -1014,29 +1070,29 @@ Update checking works by pinging the public GitHub API for the latest release to determine whether a new version is available. Actual updating of the app must still be performed manually. src/app/components/admin/settings/settings.component.html - 165,167 + 169,171 No tracking data is collected by the app in any way. src/app/components/admin/settings/settings.component.html - 169 + 173 Saved Views src/app/components/admin/settings/settings.component.html - 175 + 179 src/app/components/app-frame/app-frame.component.html - 215 + 218 src/app/components/app-frame/app-frame.component.html - 217 + 220 src/app/components/manage/saved-views/saved-views.component.html @@ -1047,152 +1103,170 @@ Show warning when closing saved views with unsaved changes src/app/components/admin/settings/settings.component.html - 178 + 182 Show document counts in sidebar saved views src/app/components/admin/settings/settings.component.html - 179 + 183 + + + + Items per page + + src/app/components/admin/settings/settings.component.html + 200 Document editing src/app/components/admin/settings/settings.component.html - 185 + 212 Use PDF viewer provided by the browser src/app/components/admin/settings/settings.component.html - 189 + 215 This is usually faster for displaying large PDF documents, but it might not work on some browsers. src/app/components/admin/settings/settings.component.html - 189 + 215 Default zoom src/app/components/admin/settings/settings.component.html - 195 + 221 Fit width src/app/components/admin/settings/settings.component.html - 199 + 225 Fit page src/app/components/admin/settings/settings.component.html - 200 + 226 Only applies to the Paperless-ngx PDF viewer. src/app/components/admin/settings/settings.component.html - 202 + 228 Automatically remove inbox tag(s) on save src/app/components/admin/settings/settings.component.html - 208 + 234 Show document thumbnail during loading src/app/components/admin/settings/settings.component.html - 214 + 240 - - Global search + + Built-in fields to show: src/app/components/admin/settings/settings.component.html - 218 - - - src/app/components/app-frame/global-search/global-search.component.ts - 122 + 246 - - Do not include advanced search results + + Uncheck fields to hide them on the document details page. src/app/components/admin/settings/settings.component.html - 221 - - - - Full search links to - - src/app/components/admin/settings/settings.component.html - 227 - - - - Title and content search - - src/app/components/admin/settings/settings.component.html - 231 - - - - Advanced search - - src/app/components/admin/settings/settings.component.html - 232 - - - src/app/components/app-frame/global-search/global-search.component.html - 24 - - - src/app/components/document-list/filter-editor/filter-editor.component.ts - 208 + 258 Bulk editing src/app/components/admin/settings/settings.component.html - 237 + 264 Show confirmation dialogs src/app/components/admin/settings/settings.component.html - 240 + 267 Apply on close src/app/components/admin/settings/settings.component.html - 241 + 268 + + + + PDF Editor + + src/app/components/admin/settings/settings.component.html + 272 + + + src/app/components/document-detail/document-detail.component.html + 66 + + + src/app/components/document-detail/document-detail.component.ts + 1472 + + + + Default editing mode + + src/app/components/admin/settings/settings.component.html + 275 + + + + Create new document(s) + + src/app/components/admin/settings/settings.component.html + 279 + + + src/app/components/common/pdf-editor/pdf-editor.component.html + 82 + + + + Update existing document + + src/app/components/admin/settings/settings.component.html + 280 + + + src/app/components/common/pdf-editor/pdf-editor.component.html + 87 Notes src/app/components/admin/settings/settings.component.html - 245 + 285 src/app/components/document-list/document-list.component.html @@ -1211,14 +1285,14 @@ Enable notes src/app/components/admin/settings/settings.component.html - 248 + 288 Permissions src/app/components/admin/settings/settings.component.html - 259 + 297 src/app/components/common/edit-dialog/group-edit-dialog/group-edit-dialog.component.html @@ -1234,7 +1308,7 @@ src/app/components/document-detail/document-detail.component.html - 349 + 375 src/app/components/document-list/bulk-editor/bulk-editor.component.html @@ -1262,47 +1336,47 @@ src/app/components/manage/management-list/management-list.component.html - 7 + 38 src/app/components/manage/management-list/management-list.component.html - 7 + 38 src/app/components/manage/management-list/management-list.component.html - 7 + 38 src/app/components/manage/management-list/management-list.component.html - 7 + 38 Default Permissions src/app/components/admin/settings/settings.component.html - 262 + 300 Settings apply to this user account for objects (Tags, Mail Rules, etc. but not documents) created via the web UI. src/app/components/admin/settings/settings.component.html - 266,268 + 304,306 Default Owner src/app/components/admin/settings/settings.component.html - 273 + 311 Objects without an owner can be viewed and edited by all users src/app/components/admin/settings/settings.component.html - 277 + 315 src/app/components/common/input/permissions/permissions-form/permissions-form.component.html @@ -1313,18 +1387,18 @@ Default View Permissions src/app/components/admin/settings/settings.component.html - 282 + 320 Users: src/app/components/admin/settings/settings.component.html - 287 + 325 src/app/components/admin/settings/settings.component.html - 314 + 352 src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html @@ -1355,11 +1429,11 @@ Groups: src/app/components/admin/settings/settings.component.html - 297 + 335 src/app/components/admin/settings/settings.component.html - 324 + 362 src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html @@ -1390,14 +1464,14 @@ Default Edit Permissions src/app/components/admin/settings/settings.component.html - 309 + 347 Edit permissions also grant viewing permissions src/app/components/admin/settings/settings.component.html - 333 + 371 src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html @@ -1416,7 +1490,7 @@ Notifications src/app/components/admin/settings/settings.component.html - 341 + 379 src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.html @@ -1427,49 +1501,49 @@ Document processing src/app/components/admin/settings/settings.component.html - 344 + 382 Show notifications when new documents are detected src/app/components/admin/settings/settings.component.html - 348 + 386 Show notifications when document processing completes successfully src/app/components/admin/settings/settings.component.html - 349 + 387 Show notifications when document processing fails src/app/components/admin/settings/settings.component.html - 350 + 388 Suppress notifications on dashboard src/app/components/admin/settings/settings.component.html - 351 + 389 This will suppress all messages about document processing status on the dashboard. src/app/components/admin/settings/settings.component.html - 351 + 389 Cancel src/app/components/admin/settings/settings.component.html - 361 + 399 src/app/components/common/confirm-dialog/confirm-dialog.component.ts @@ -1540,21 +1614,160 @@ Use system language src/app/components/admin/settings/settings.component.ts - 78 + 79 Use date format of display language src/app/components/admin/settings/settings.component.ts - 81 + 82 + + + + Archive serial number + + src/app/components/admin/settings/settings.component.ts + 96 + + + src/app/components/document-detail/document-detail.component.html + 150 + + + + Correspondent + + src/app/components/admin/settings/settings.component.ts + 98 + + + src/app/components/document-detail/document-detail.component.html + 155 + + + src/app/components/document-list/bulk-editor/bulk-editor.component.html + 19 + + + src/app/components/document-list/document-list.component.html + 211 + + + src/app/components/document-list/filter-editor/filter-editor.component.html + 50 + + + src/app/data/document.ts + 46 + + + src/app/data/document.ts + 89 + + + + Document type + + src/app/components/admin/settings/settings.component.ts + 99 + + + src/app/components/document-detail/document-detail.component.html + 159 + + + src/app/components/document-list/bulk-editor/bulk-editor.component.html + 33 + + + src/app/components/document-list/document-list.component.html + 251 + + + src/app/components/document-list/filter-editor/filter-editor.component.html + 61 + + + src/app/data/document.ts + 50 + + + src/app/data/document.ts + 91 + + + + Storage path + + src/app/components/admin/settings/settings.component.ts + 100 + + + src/app/components/document-detail/document-detail.component.html + 163 + + + src/app/components/document-list/bulk-editor/bulk-editor.component.html + 47 + + + src/app/components/document-list/document-list.component.html + 260 + + + src/app/components/document-list/filter-editor/filter-editor.component.html + 72 + + + src/app/data/document.ts + 54 + + + + Tags + + src/app/components/admin/settings/settings.component.ts + 101 + + + src/app/components/app-frame/app-frame.component.html + 188 + + + src/app/components/app-frame/app-frame.component.html + 191 + + + src/app/components/common/input/tags/tags.component.ts + 80 + + + src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html + 94 + + + src/app/components/document-list/bulk-editor/bulk-editor.component.html + 5 + + + src/app/components/document-list/document-list.component.html + 224 + + + src/app/components/document-list/filter-editor/filter-editor.component.html + 39 + + + src/app/data/document.ts + 42 Error retrieving users src/app/components/admin/settings/settings.component.ts - 226 + 252 src/app/components/admin/users-groups/users-groups.component.ts @@ -1565,7 +1778,7 @@ Error retrieving groups src/app/components/admin/settings/settings.component.ts - 245 + 271 src/app/components/admin/users-groups/users-groups.component.ts @@ -1576,32 +1789,32 @@ Settings were saved successfully. src/app/components/admin/settings/settings.component.ts - 548 + 588 Settings were saved successfully. Reload is required to apply some changes. src/app/components/admin/settings/settings.component.ts - 552 + 592 Reload now src/app/components/admin/settings/settings.component.ts - 553 + 593 An error occurred while saving settings. src/app/components/admin/settings/settings.component.ts - 563 + 603 src/app/components/app-frame/app-frame.component.ts - 180 + 182 @@ -1612,11 +1825,11 @@ src/app/components/app-frame/app-frame.component.html - 278 + 281 src/app/components/app-frame/app-frame.component.html - 280 + 283 @@ -1633,22 +1846,6 @@ src/app/components/document-list/document-list.component.html 153 - - src/app/components/manage/management-list/management-list.component.html - 4 - - - src/app/components/manage/management-list/management-list.component.html - 4 - - - src/app/components/manage/management-list/management-list.component.html - 4 - - - src/app/components/manage/management-list/management-list.component.html - 4 - Filter by @@ -1733,35 +1930,35 @@ src/app/components/manage/management-list/management-list.component.html - 21 + 52 src/app/components/manage/management-list/management-list.component.html - 21 + 52 src/app/components/manage/management-list/management-list.component.html - 21 + 52 src/app/components/manage/management-list/management-list.component.html - 21 + 52 src/app/components/manage/management-list/management-list.component.html - 38 + 69 src/app/components/manage/management-list/management-list.component.html - 38 + 69 src/app/components/manage/management-list/management-list.component.html - 38 + 69 src/app/components/manage/management-list/management-list.component.html - 38 + 69 src/app/components/manage/workflows/workflows.component.html @@ -1782,6 +1979,10 @@ src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts 94 + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 32 + src/app/components/document-list/document-list.component.html 269 @@ -1831,6 +2032,10 @@ src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html 67 + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 38 + src/app/components/document-detail/document-detail.component.html 50 @@ -1853,19 +2058,19 @@ src/app/components/manage/management-list/management-list.component.html - 44 + 75 src/app/components/manage/management-list/management-list.component.html - 44 + 75 src/app/components/manage/management-list/management-list.component.html - 44 + 75 src/app/components/manage/management-list/management-list.component.html - 44 + 75 src/app/components/manage/saved-views/saved-views.component.html @@ -1883,11 +2088,18 @@ 97 + + Duplicate(s) detected + + src/app/components/admin/tasks/tasks.component.html + 103 + + Dismiss src/app/components/admin/tasks/tasks.component.html - 110 + 116 src/app/components/admin/tasks/tasks.component.ts @@ -1898,49 +2110,49 @@ Open Document src/app/components/admin/tasks/tasks.component.html - 115 + 121 {VAR_PLURAL, plural, =1 {One task} other { total tasks}} src/app/components/admin/tasks/tasks.component.html - 134 + 140  ( selected) src/app/components/admin/tasks/tasks.component.html - 136 + 142 Failed src/app/components/admin/tasks/tasks.component.html - 148,150 + 154,156 Complete src/app/components/admin/tasks/tasks.component.html - 156,158 + 162,164 Started src/app/components/admin/tasks/tasks.component.html - 164,166 + 170,172 Queued src/app/components/admin/tasks/tasks.component.html - 172,174 + 178,180 @@ -2028,11 +2240,11 @@ src/app/components/app-frame/app-frame.component.html - 238 + 241 src/app/components/app-frame/app-frame.component.html - 241 + 244 @@ -2156,7 +2368,7 @@ src/app/components/document-list/bulk-editor/bulk-editor.component.html - 145 + 167 src/app/components/manage/custom-fields/custom-fields.component.html @@ -2184,55 +2396,55 @@ src/app/components/manage/management-list/management-list.component.html - 10 + 41 src/app/components/manage/management-list/management-list.component.html - 10 + 41 src/app/components/manage/management-list/management-list.component.html - 10 + 41 src/app/components/manage/management-list/management-list.component.html - 10 + 41 src/app/components/manage/management-list/management-list.component.html - 121 + 152 src/app/components/manage/management-list/management-list.component.html - 121 + 152 src/app/components/manage/management-list/management-list.component.html - 121 + 152 src/app/components/manage/management-list/management-list.component.html - 121 + 152 src/app/components/manage/management-list/management-list.component.html - 140 + 171 src/app/components/manage/management-list/management-list.component.html - 140 + 171 src/app/components/manage/management-list/management-list.component.html - 140 + 171 src/app/components/manage/management-list/management-list.component.html - 140 + 171 src/app/components/manage/management-list/management-list.component.ts - 247 + 249 src/app/components/manage/saved-views/saved-views.component.html @@ -2266,11 +2478,11 @@ src/app/components/manage/management-list/management-list.component.ts - 243 + 245 src/app/components/manage/management-list/management-list.component.ts - 366 + 386 @@ -2312,7 +2524,7 @@ src/app/components/manage/management-list/management-list.component.ts - 368 + 388 src/app/components/manage/workflows/workflows.component.ts @@ -2397,11 +2609,11 @@ src/app/components/app-frame/app-frame.component.html - 269 + 272 src/app/components/app-frame/app-frame.component.html - 271 + 274 @@ -2503,35 +2715,35 @@ src/app/components/manage/management-list/management-list.component.html - 120 + 151 src/app/components/manage/management-list/management-list.component.html - 120 + 151 src/app/components/manage/management-list/management-list.component.html - 120 + 151 src/app/components/manage/management-list/management-list.component.html - 120 + 151 src/app/components/manage/management-list/management-list.component.html - 137 + 168 src/app/components/manage/management-list/management-list.component.html - 137 + 168 src/app/components/manage/management-list/management-list.component.html - 137 + 168 src/app/components/manage/management-list/management-list.component.html - 137 + 168 src/app/components/manage/workflows/workflows.component.html @@ -2607,23 +2819,23 @@ src/app/components/document-detail/document-detail.component.ts - 1029 + 1108 src/app/components/document-detail/document-detail.component.ts - 1394 + 1473 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 798 + 802 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 831 + 835 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 850 + 854 src/app/components/manage/custom-fields/custom-fields.component.ts @@ -2639,7 +2851,7 @@ src/app/components/manage/management-list/management-list.component.ts - 370 + 390 src/app/components/manage/workflows/workflows.component.ts @@ -2713,133 +2925,98 @@ Logged in as src/app/components/app-frame/app-frame.component.html - 43 + 46 My Profile src/app/components/app-frame/app-frame.component.html - 47 + 50 Logout src/app/components/app-frame/app-frame.component.html - 54 + 57 Documentation src/app/components/app-frame/app-frame.component.html - 59 - - - src/app/components/app-frame/app-frame.component.html - 299 + 62 src/app/components/app-frame/app-frame.component.html 302 + + src/app/components/app-frame/app-frame.component.html + 305 + Saved views src/app/components/app-frame/app-frame.component.html - 101 + 104 src/app/components/app-frame/app-frame.component.html - 106 + 109 Open documents src/app/components/app-frame/app-frame.component.html - 141 + 144 Close all src/app/components/app-frame/app-frame.component.html - 161 + 164 src/app/components/app-frame/app-frame.component.html - 163 + 166 Manage src/app/components/app-frame/app-frame.component.html - 172 + 175 Correspondents src/app/components/app-frame/app-frame.component.html - 178 + 181 src/app/components/app-frame/app-frame.component.html - 180 + 183 src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html 107 - - Tags - - src/app/components/app-frame/app-frame.component.html - 185 - - - src/app/components/app-frame/app-frame.component.html - 188 - - - src/app/components/common/input/tags/tags.component.ts - 80 - - - src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html - 94 - - - src/app/components/document-list/bulk-editor/bulk-editor.component.html - 5 - - - src/app/components/document-list/document-list.component.html - 224 - - - src/app/components/document-list/filter-editor/filter-editor.component.html - 39 - - - src/app/data/document.ts - 42 - - Document Types src/app/components/app-frame/app-frame.component.html - 194 + 197 src/app/components/app-frame/app-frame.component.html - 196 + 199 src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html @@ -2850,11 +3027,11 @@ Storage Paths src/app/components/app-frame/app-frame.component.html - 201 + 204 src/app/components/app-frame/app-frame.component.html - 203 + 206 src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html @@ -2865,11 +3042,11 @@ Custom Fields src/app/components/app-frame/app-frame.component.html - 208 + 211 src/app/components/app-frame/app-frame.component.html - 210 + 213 src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.html @@ -2884,11 +3061,11 @@ Workflows src/app/components/app-frame/app-frame.component.html - 224 + 227 src/app/components/app-frame/app-frame.component.html - 226 + 229 src/app/components/manage/workflows/workflows.component.html @@ -2899,92 +3076,92 @@ Mail src/app/components/app-frame/app-frame.component.html - 231 + 234 src/app/components/app-frame/app-frame.component.html - 234 + 237 Administration src/app/components/app-frame/app-frame.component.html - 249 + 252 Configuration src/app/components/app-frame/app-frame.component.html - 262 + 265 src/app/components/app-frame/app-frame.component.html - 264 + 267 GitHub src/app/components/app-frame/app-frame.component.html - 309 + 312 is available. src/app/components/app-frame/app-frame.component.html - 318,319 + 321,322 Click to view. src/app/components/app-frame/app-frame.component.html - 319 + 322 Paperless-ngx can automatically check for updates src/app/components/app-frame/app-frame.component.html - 323 + 326 How does this work? src/app/components/app-frame/app-frame.component.html - 330,332 + 333,335 Update available src/app/components/app-frame/app-frame.component.html - 343 + 346 Sidebar views updated src/app/components/app-frame/app-frame.component.ts - 264 + 270 Error updating sidebar views src/app/components/app-frame/app-frame.component.ts - 267 + 273 An error occurred while saving update checking settings. src/app/components/app-frame/app-frame.component.ts - 288 + 294 @@ -3048,7 +3225,7 @@ src/app/components/document-list/bulk-editor/bulk-editor.component.html - 117 + 139 src/app/components/document-list/document-card-large/document-card-large.component.html @@ -3186,6 +3363,20 @@ 20 + + Ask a question about this document... + + src/app/components/chat/chat/chat.component.ts + 37 + + + + Ask a question about a document... + + src/app/components/chat/chat/chat.component.ts + 38 + + Clear @@ -3223,31 +3414,31 @@ src/app/components/document-detail/document-detail.component.ts - 982 + 1061 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 441 + 445 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 481 + 485 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 519 + 523 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 557 + 561 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 619 + 623 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 752 + 756 @@ -3328,7 +3519,7 @@ src/app/components/document-detail/document-detail.component.ts - 1445 + 1524 @@ -3339,7 +3530,7 @@ src/app/components/document-detail/document-detail.component.ts - 1446 + 1525 @@ -3350,7 +3541,7 @@ src/app/components/document-detail/document-detail.component.ts - 1447 + 1526 @@ -3439,7 +3630,7 @@ src/app/components/common/dates-dropdown/dates-dropdown.component.ts - 111 + 113 src/app/components/common/input/date/date.component.html @@ -3472,9 +3663,13 @@ src/app/components/common/input/date/date.component.html 22 + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 155 + src/app/components/document-detail/document-detail.component.html - 113 + 103 src/app/guards/dirty-saved-view.guard.ts @@ -3559,6 +3754,22 @@ src/app/components/document-list/document-list.component.html 30 + + src/app/components/manage/management-list/management-list.component.html + 32 + + + src/app/components/manage/management-list/management-list.component.html + 32 + + + src/app/components/manage/management-list/management-list.component.html + 32 + + + src/app/components/manage/management-list/management-list.component.html + 32 + Not @@ -3683,14 +3894,14 @@ This month src/app/components/common/dates-dropdown/dates-dropdown.component.ts - 106 + 107 Yesterday src/app/components/common/dates-dropdown/dates-dropdown.component.ts - 116 + 118 src/app/pipes/custom-date.pipe.ts @@ -3701,28 +3912,28 @@ Previous week src/app/components/common/dates-dropdown/dates-dropdown.component.ts - 121 + 123 Previous month src/app/components/common/dates-dropdown/dates-dropdown.component.ts - 135 + 137 Previous quarter src/app/components/common/dates-dropdown/dates-dropdown.component.ts - 141 + 143 Previous year src/app/components/common/dates-dropdown/dates-dropdown.component.ts - 155 + 157 @@ -4247,6 +4458,10 @@ src/app/components/common/system-status-dialog/system-status-dialog.component.html 264 + + src/app/components/common/system-status-dialog/system-status-dialog.component.html + 302 + src/app/components/common/toast/toast.component.html 30 @@ -4422,7 +4637,7 @@ src/app/components/manage/storage-path-list/storage-path-list.component.ts - 51 + 53 @@ -4440,7 +4655,7 @@ src/app/components/document-detail/document-detail.component.html - 315 + 341 @@ -4497,7 +4712,7 @@ src/app/components/manage/tag-list/tag-list.component.ts - 51 + 53 @@ -4551,11 +4766,11 @@ src/app/components/document-detail/document-detail.component.html - 98 + 88 src/app/components/document-list/bulk-editor/bulk-editor.component.html - 101 + 124 @@ -5239,84 +5454,105 @@ Has any of these tags src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts - 203 + 209 Has all of these tags src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts - 210 + 216 Does not have these tags src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts - 217 + 223 + + + + Has any of these correspondents + + src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts + 230 Has correspondent src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts - 224 + 238 Does not have correspondents src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts - 232 + 246 Has document type src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts - 240 + 254 + + + + Has any of these document types + + src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts + 262 Does not have document types src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts - 248 + 270 Has storage path src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts - 256 + 278 + + + + Has any of these storage paths + + src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts + 286 Does not have storage paths src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts - 264 + 294 Matches custom field query src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts - 272 + 302 Create new workflow src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts - 474 + 531 Edit workflow src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts - 478 + 535 @@ -5419,19 +5655,19 @@ src/app/components/manage/management-list/management-list.component.html - 13 + 44 src/app/components/manage/management-list/management-list.component.html - 13 + 44 src/app/components/manage/management-list/management-list.component.html - 13 + 44 src/app/components/manage/management-list/management-list.component.html - 13 + 44 @@ -5626,7 +5862,7 @@ Show password src/app/components/common/input/password/password.component.html - 6 + 12 @@ -5709,11 +5945,37 @@ 55 + + Suggestion: + + src/app/components/common/input/text/text.component.html + 20 + + + + Copied! + + src/app/components/common/page-header/page-header.component.html + 8 + + + src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html + 54 + + + src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html + 164 + + + src/app/components/common/share-links-dialog/share-links-dialog.component.html + 39 + + Read more src/app/components/common/page-header/page-header.component.html - 15 + 24 src/app/components/common/permissions-select/permissions-select.component.html @@ -5794,20 +6056,6 @@ 70 - - Create new document(s) - - src/app/components/common/pdf-editor/pdf-editor.component.html - 82 - - - - Update existing document - - src/app/components/common/pdf-editor/pdf-editor.component.html - 87 - - Copy metadata @@ -5986,7 +6234,7 @@ src/app/components/common/system-status-dialog/system-status-dialog.component.html - 284 + 321 src/app/components/manage/mail/mail.component.html @@ -6012,21 +6260,6 @@ 47 - - Copied! - - src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 54 - - - src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 164 - - - src/app/components/common/share-links-dialog/share-links-dialog.component.html - 39 - - Warning: changing the token cannot be undone @@ -6203,6 +6436,301 @@ 326 + + Selected documents: + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 10 + + + + + more… + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 22 + + + + Expires + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 31 + + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 87 + + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 35 + + + src/app/components/common/share-links-dialog/share-links-dialog.component.html + 52 + + + + Share archive version (if available) + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 47 + + + + Share link bundle requested + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 54 + + + + You can copy the share link below or open the manager to monitor progress. The link will start working once the bundle is ready. + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 55,57 + + + + Status + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 60 + + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 33 + + + src/app/components/common/system-status-dialog/system-status-dialog.component.html + 58 + + + src/app/components/common/toast/toast.component.html + 28 + + + src/app/components/manage/mail/mail.component.html + 114 + + + src/app/components/manage/mail/processed-mail-dialog/processed-mail-dialog.component.html + 35 + + + src/app/components/manage/workflows/workflows.component.html + 19 + + + + Slug + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 64 + + + + Link + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 66 + + + + Copy link + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 81 + + + + Never + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 93 + + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 101 + + + src/app/data/share-link.ts + 17 + + + + File version + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 96 + + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 37 + + + + Size + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 99 + + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 34 + + + + A zip file containing the selected documents will be created for this share link bundle. This process happens in the background and may take some time, especially for large bundles. + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 109 + + + + Manage share link bundles + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 113 + + + src/app/components/document-list/bulk-editor/bulk-editor.component.html + 119 + + + + Create share link bundle + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.ts + 61 + + + + Create link + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.ts + 62 + + + + Share link copied to clipboard. + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.ts + 96 + + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.ts + 112 + + + + Loading share link bundles… + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 10 + + + + Status updates every few seconds while bundles are being prepared. + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 21 + + + + No share link bundles currently exist. + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 25 + + + + Built: + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 48 + + + + View error details + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 62 + + + + Copy share link + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 113 + + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 122 + + + + Retry + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 132 + + + + Delete share link bundle + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 141 + + + + Share link bundles + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.ts + 42 + + + + Failed to load share link bundles. + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.ts + 66 + + + + Error retrieving share link bundles. + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.ts + 68 + + + + Share link bundle deleted. + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.ts + 121 + + + + Error deleting share link bundle. + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.ts + 127 + + + + Share link bundle rebuild requested. + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.ts + 139 + + + + Error requesting rebuild. + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.ts + 144 + + No existing links @@ -6224,82 +6752,90 @@ 48 - - Expires - - src/app/components/common/share-links-dialog/share-links-dialog.component.html - 52 - - - - 1 day - - src/app/components/common/share-links-dialog/share-links-dialog.component.ts - 25 - - - src/app/components/common/share-links-dialog/share-links-dialog.component.ts - 102 - - - - 7 days - - src/app/components/common/share-links-dialog/share-links-dialog.component.ts - 26 - - - - 30 days - - src/app/components/common/share-links-dialog/share-links-dialog.component.ts - 27 - - - - Never - - src/app/components/common/share-links-dialog/share-links-dialog.component.ts - 28 - - Share Links src/app/components/common/share-links-dialog/share-links-dialog.component.ts - 32 + 31 src/app/components/document-detail/document-detail.component.html - 94 + 84 Error retrieving links src/app/components/common/share-links-dialog/share-links-dialog.component.ts - 83 + 82 + + + + 1 day + + src/app/components/common/share-links-dialog/share-links-dialog.component.ts + 101 + + + src/app/data/share-link.ts + 14 days src/app/components/common/share-links-dialog/share-links-dialog.component.ts - 102 + 101 Error deleting link src/app/components/common/share-links-dialog/share-links-dialog.component.ts - 131 + 130 Error creating link src/app/components/common/share-links-dialog/share-links-dialog.component.ts - 159 + 158 + + + + Suggest + + src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.html + 8 + + + + Show suggestions + + src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.html + 17 + + + + No novel suggestions + + src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.html + 24 + + + + + + src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.html + 30 + + + src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.html + 36 + + + src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.html + 42 @@ -6358,29 +6894,6 @@ 52 - - Status - - src/app/components/common/system-status-dialog/system-status-dialog.component.html - 58 - - - src/app/components/common/toast/toast.component.html - 28 - - - src/app/components/manage/mail/mail.component.html - 114 - - - src/app/components/manage/mail/processed-mail-dialog/processed-mail-dialog.component.html - 35 - - - src/app/components/manage/workflows/workflows.component.html - 19 - - Migration Status @@ -6458,6 +6971,10 @@ src/app/components/common/system-status-dialog/system-status-dialog.component.html 245 + + src/app/components/common/system-status-dialog/system-status-dialog.component.html + 293 + Last Updated @@ -6493,6 +7010,10 @@ src/app/components/common/system-status-dialog/system-status-dialog.component.html 252 + + src/app/components/common/system-status-dialog/system-status-dialog.component.html + 300 + WebSocket Connection @@ -6508,6 +7029,13 @@ 261 + + AI Index + + src/app/components/common/system-status-dialog/system-status-dialog.component.html + 270 + + Copy Raw Error @@ -6745,7 +7273,7 @@ src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 386 + 390 this string is used to separate processing, failed and added on the file upload widget @@ -6801,6 +7329,22 @@ src/app/components/document-list/document-list.component.html 27 + + src/app/components/manage/management-list/management-list.component.html + 29 + + + src/app/components/manage/management-list/management-list.component.html + 29 + + + src/app/components/manage/management-list/management-list.component.html + 29 + + + src/app/components/manage/management-list/management-list.component.html + 29 + of @@ -6859,17 +7403,6 @@ 69 - - PDF Editor - - src/app/components/document-detail/document-detail.component.html - 66 - - - src/app/components/document-detail/document-detail.component.ts - 1393 - - Remove Password @@ -6881,28 +7414,32 @@ Send src/app/components/document-detail/document-detail.component.html - 90 + 80 + + + src/app/components/document-list/bulk-editor/bulk-editor.component.html + 111 Previous src/app/components/document-detail/document-detail.component.html - 116 + 106 Details src/app/components/document-detail/document-detail.component.html - 129 + 145 Title src/app/components/document-detail/document-detail.component.html - 132 + 148 src/app/components/document-list/document-list.component.html @@ -6921,102 +7458,18 @@ 90 - - Archive serial number - - src/app/components/document-detail/document-detail.component.html - 133 - - Date created src/app/components/document-detail/document-detail.component.html - 134 - - - - Correspondent - - src/app/components/document-detail/document-detail.component.html - 136 - - - src/app/components/document-list/bulk-editor/bulk-editor.component.html - 19 - - - src/app/components/document-list/document-list.component.html - 211 - - - src/app/components/document-list/filter-editor/filter-editor.component.html - 50 - - - src/app/data/document.ts - 46 - - - src/app/data/document.ts - 89 - - - - Document type - - src/app/components/document-detail/document-detail.component.html - 138 - - - src/app/components/document-list/bulk-editor/bulk-editor.component.html - 33 - - - src/app/components/document-list/document-list.component.html - 251 - - - src/app/components/document-list/filter-editor/filter-editor.component.html - 61 - - - src/app/data/document.ts - 50 - - - src/app/data/document.ts - 91 - - - - Storage path - - src/app/components/document-detail/document-detail.component.html - 140 - - - src/app/components/document-list/bulk-editor/bulk-editor.component.html - 47 - - - src/app/components/document-list/document-list.component.html - 260 - - - src/app/components/document-list/filter-editor/filter-editor.component.html - 72 - - - src/app/data/document.ts - 54 + 152 Default src/app/components/document-detail/document-detail.component.html - 141 + 164 src/app/components/manage/saved-views/saved-views.component.html @@ -7027,14 +7480,14 @@ Content src/app/components/document-detail/document-detail.component.html - 245 + 271 Metadata src/app/components/document-detail/document-detail.component.html - 254 + 280 src/app/components/document-detail/metadata-collapse/metadata-collapse.component.ts @@ -7045,175 +7498,196 @@ Date modified src/app/components/document-detail/document-detail.component.html - 261 + 287 Date added src/app/components/document-detail/document-detail.component.html - 265 + 291 Media filename src/app/components/document-detail/document-detail.component.html - 269 + 295 Original filename src/app/components/document-detail/document-detail.component.html - 273 + 299 Original MD5 checksum src/app/components/document-detail/document-detail.component.html - 277 + 303 Original file size src/app/components/document-detail/document-detail.component.html - 281 + 307 Original mime type src/app/components/document-detail/document-detail.component.html - 285 + 311 Archive MD5 checksum src/app/components/document-detail/document-detail.component.html - 290 + 316 Archive file size src/app/components/document-detail/document-detail.component.html - 296 + 322 Original document metadata src/app/components/document-detail/document-detail.component.html - 305 + 331 Archived document metadata src/app/components/document-detail/document-detail.component.html - 308 + 334 Notes src/app/components/document-detail/document-detail.component.html - 327,330 + 353,356 History src/app/components/document-detail/document-detail.component.html - 338 + 364 + + + + Duplicates + + src/app/components/document-detail/document-detail.component.html + 386,390 + + + + Duplicate documents detected: + + src/app/components/document-detail/document-detail.component.html + 392 + + + + In trash + + src/app/components/document-detail/document-detail.component.html + 403 Save & next src/app/components/document-detail/document-detail.component.html - 375 + 432 Save & close src/app/components/document-detail/document-detail.component.html - 378 + 435 Document loading... src/app/components/document-detail/document-detail.component.html - 388 + 445 Enter Password src/app/components/document-detail/document-detail.component.html - 442 + 499 An error occurred loading content: src/app/components/document-detail/document-detail.component.ts - 417,419 + 428,430 Document changes detected src/app/components/document-detail/document-detail.component.ts - 451 + 467 The version of this document in your browser session appears older than the existing version. src/app/components/document-detail/document-detail.component.ts - 452 + 468 Saving the document here may overwrite other changes that were made. To restore the existing version, discard your changes or close the document. src/app/components/document-detail/document-detail.component.ts - 453 + 469 Ok src/app/components/document-detail/document-detail.component.ts - 455 + 471 Next document src/app/components/document-detail/document-detail.component.ts - 581 + 597 Previous document src/app/components/document-detail/document-detail.component.ts - 591 + 607 Close document src/app/components/document-detail/document-detail.component.ts - 599 + 615 src/app/services/open-documents.service.ts @@ -7224,202 +7698,202 @@ Save document src/app/components/document-detail/document-detail.component.ts - 606 + 622 Save and close / next src/app/components/document-detail/document-detail.component.ts - 615 + 631 Error retrieving metadata src/app/components/document-detail/document-detail.component.ts - 670 + 686 Error retrieving suggestions. src/app/components/document-detail/document-detail.component.ts - 699 + 741 Document "" saved successfully. src/app/components/document-detail/document-detail.component.ts - 871 + 950 src/app/components/document-detail/document-detail.component.ts - 895 + 974 Error saving document "" src/app/components/document-detail/document-detail.component.ts - 901 + 980 Error saving document src/app/components/document-detail/document-detail.component.ts - 951 + 1030 Do you really want to move the document "" to the trash? src/app/components/document-detail/document-detail.component.ts - 983 + 1062 Documents can be restored prior to permanent deletion. src/app/components/document-detail/document-detail.component.ts - 984 + 1063 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 754 + 758 Move to trash src/app/components/document-detail/document-detail.component.ts - 986 + 1065 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 756 + 760 Error deleting document src/app/components/document-detail/document-detail.component.ts - 1005 + 1084 Reprocess confirm src/app/components/document-detail/document-detail.component.ts - 1025 + 1104 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 794 + 798 This operation will permanently recreate the archive file for this document. src/app/components/document-detail/document-detail.component.ts - 1026 + 1105 The archive file will be re-generated with the current settings. src/app/components/document-detail/document-detail.component.ts - 1027 + 1106 Reprocess operation for "" will begin in the background. Close and re-open or reload this document after the operation has completed to see new content. src/app/components/document-detail/document-detail.component.ts - 1037 + 1116 Error executing operation src/app/components/document-detail/document-detail.component.ts - 1048 + 1127 Error downloading document src/app/components/document-detail/document-detail.component.ts - 1097 + 1176 Page Fit src/app/components/document-detail/document-detail.component.ts - 1174 + 1253 PDF edit operation for "" will begin in the background. src/app/components/document-detail/document-detail.component.ts - 1412 + 1491 Error executing PDF edit operation src/app/components/document-detail/document-detail.component.ts - 1424 + 1503 Please enter the current password before attempting to remove it. src/app/components/document-detail/document-detail.component.ts - 1435 + 1514 Password removal operation for "" will begin in the background. src/app/components/document-detail/document-detail.component.ts - 1467 + 1546 Error executing password removal operation src/app/components/document-detail/document-detail.component.ts - 1481 + 1560 Print failed. src/app/components/document-detail/document-detail.component.ts - 1518 + 1597 Error loading document for printing. src/app/components/document-detail/document-detail.component.ts - 1530 + 1609 An error occurred loading tiff: src/app/components/document-detail/document-detail.component.ts - 1595 + 1674 src/app/components/document-detail/document-detail.component.ts - 1599 + 1678 @@ -7523,57 +7997,64 @@ 97 + + Create a share link bundle + + src/app/components/document-list/bulk-editor/bulk-editor.component.html + 116 + + Include: src/app/components/document-list/bulk-editor/bulk-editor.component.html - 123 + 145 Archived files src/app/components/document-list/bulk-editor/bulk-editor.component.html - 127 + 149 Original files src/app/components/document-list/bulk-editor/bulk-editor.component.html - 131 + 153 Use formatted filename src/app/components/document-list/bulk-editor/bulk-editor.component.html - 136 + 158 Error executing bulk operation src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 290 + 294 "" src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 378 + 382 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 384 + 388 "" and "" src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 380 + 384 This is for messages like 'modify "tag1" and "tag2"' @@ -7581,7 +8062,7 @@ and "" src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 388,390 + 392,394 this is for messages like 'modify "tag1", "tag2" and "tag3"' @@ -7589,14 +8070,14 @@ Confirm tags assignment src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 405 + 409 This operation will add the tag "" to selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 411 + 415 @@ -7605,14 +8086,14 @@ )"/> to selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 416,418 + 420,422 This operation will remove the tag "" from selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 424 + 428 @@ -7621,7 +8102,7 @@ )"/> from selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 429,431 + 433,435 @@ -7632,84 +8113,84 @@ )"/> on selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 433,437 + 437,441 Confirm correspondent assignment src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 474 + 478 This operation will assign the correspondent "" to selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 476 + 480 This operation will remove the correspondent from selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 478 + 482 Confirm document type assignment src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 512 + 516 This operation will assign the document type "" to selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 514 + 518 This operation will remove the document type from selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 516 + 520 Confirm storage path assignment src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 550 + 554 This operation will assign the storage path "" to selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 552 + 556 This operation will remove the storage path from selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 554 + 558 Confirm custom field assignment src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 583 + 587 This operation will assign the custom field "" to selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 589 + 593 @@ -7718,14 +8199,14 @@ )"/> to selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 594,596 + 598,600 This operation will remove the custom field "" from selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 602 + 606 @@ -7734,7 +8215,7 @@ )"/> from selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 607,609 + 611,613 @@ -7745,77 +8226,91 @@ )"/> on selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 611,615 + 615,619 Move selected document(s) to the trash? src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 753 + 757 This operation will permanently recreate the archive files for selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 795 + 799 The archive files will be re-generated with the current settings. src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 796 + 800 Rotate confirm src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 828 + 832 This operation will permanently rotate the original version of document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 829 + 833 Merge confirm src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 848 + 852 This operation will merge selected documents into a new document. src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 849 + 853 Merged document will be queued for consumption. src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 868 + 872 Custom fields updated. src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 892 + 896 Error updating custom fields. src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 901 + 905 + + + + Share link bundle creation requested. + + src/app/components/document-list/bulk-editor/bulk-editor.component.ts + 945 + + + + Share link bundle creation is not available yet. + + src/app/components/document-list/bulk-editor/bulk-editor.component.ts + 952 @@ -7974,6 +8469,22 @@ src/app/components/document-list/document-list.component.html 5 + + src/app/components/manage/management-list/management-list.component.html + 6 + + + src/app/components/manage/management-list/management-list.component.html + 6 + + + src/app/components/manage/management-list/management-list.component.html + 6 + + + src/app/components/manage/management-list/management-list.component.html + 6 + src/app/data/custom-field.ts 51 @@ -7985,6 +8496,22 @@ src/app/components/document-list/document-list.component.html 11 + + src/app/components/manage/management-list/management-list.component.html + 12 + + + src/app/components/manage/management-list/management-list.component.html + 12 + + + src/app/components/manage/management-list/management-list.component.html + 12 + + + src/app/components/manage/management-list/management-list.component.html + 12 + Select page @@ -7996,6 +8523,22 @@ src/app/components/document-list/document-list.component.ts 315 + + src/app/components/manage/management-list/management-list.component.html + 13 + + + src/app/components/manage/management-list/management-list.component.html + 13 + + + src/app/components/manage/management-list/management-list.component.html + 13 + + + src/app/components/manage/management-list/management-list.component.html + 13 + Select all @@ -8007,6 +8550,22 @@ src/app/components/document-list/document-list.component.ts 308 + + src/app/components/manage/management-list/management-list.component.html + 14 + + + src/app/components/manage/management-list/management-list.component.html + 14 + + + src/app/components/manage/management-list/management-list.component.html + 14 + + + src/app/components/manage/management-list/management-list.component.html + 14 + Select: @@ -8014,6 +8573,22 @@ src/app/components/document-list/document-list.component.html 18 + + src/app/components/manage/management-list/management-list.component.html + 20 + + + src/app/components/manage/management-list/management-list.component.html + 20 + + + src/app/components/manage/management-list/management-list.component.html + 20 + + + src/app/components/manage/management-list/management-list.component.html + 20 + None @@ -8021,9 +8596,25 @@ src/app/components/document-list/document-list.component.html 23 + + src/app/components/manage/management-list/management-list.component.html + 25 + + + src/app/components/manage/management-list/management-list.component.html + 25 + + + src/app/components/manage/management-list/management-list.component.html + 25 + + + src/app/components/manage/management-list/management-list.component.html + 25 + src/app/components/manage/management-list/management-list.component.ts - 124 + 125 src/app/data/matching-model.ts @@ -8224,7 +8815,7 @@ src/app/data/paperless-config.ts - 91 + 104 @@ -8589,28 +9180,28 @@ correspondent src/app/components/manage/correspondent-list/correspondent-list.component.ts - 49 + 51 correspondents src/app/components/manage/correspondent-list/correspondent-list.component.ts - 50 + 52 Last used src/app/components/manage/correspondent-list/correspondent-list.component.ts - 55 + 57 Do you really want to delete the correspondent ""? src/app/components/manage/correspondent-list/correspondent-list.component.ts - 80 + 82 @@ -8642,19 +9233,19 @@ src/app/components/manage/management-list/management-list.component.html - 129 + 160 src/app/components/manage/management-list/management-list.component.html - 129 + 160 src/app/components/manage/management-list/management-list.component.html - 129 + 160 src/app/components/manage/management-list/management-list.component.html - 129 + 160 @@ -8696,21 +9287,21 @@ document type src/app/components/manage/document-type-list/document-type-list.component.ts - 45 + 47 document types src/app/components/manage/document-type-list/document-type-list.component.ts - 46 + 48 Do you really want to delete the document type ""? src/app/components/manage/document-type-list/document-type-list.component.ts - 51 + 53 @@ -8981,7 +9572,7 @@ src/app/components/manage/management-list/management-list.component.ts - 353 + 373 @@ -9023,83 +9614,83 @@ Filter by: src/app/components/manage/management-list/management-list.component.html - 20 + 51 src/app/components/manage/management-list/management-list.component.html - 20 + 51 src/app/components/manage/management-list/management-list.component.html - 20 + 51 src/app/components/manage/management-list/management-list.component.html - 20 + 51 Matching src/app/components/manage/management-list/management-list.component.html - 39 + 70 src/app/components/manage/management-list/management-list.component.html - 39 + 70 src/app/components/manage/management-list/management-list.component.html - 39 + 70 src/app/components/manage/management-list/management-list.component.html - 39 + 70 Document count src/app/components/manage/management-list/management-list.component.html - 40 + 71 src/app/components/manage/management-list/management-list.component.html - 40 + 71 src/app/components/manage/management-list/management-list.component.html - 40 + 71 src/app/components/manage/management-list/management-list.component.html - 40 + 71 {VAR_PLURAL, plural, =1 {One } other { total }} src/app/components/manage/management-list/management-list.component.html - 67 + 98 src/app/components/manage/management-list/management-list.component.html - 67 + 98 src/app/components/manage/management-list/management-list.component.html - 67 + 98 src/app/components/manage/management-list/management-list.component.html - 67 + 98 Automatic src/app/components/manage/management-list/management-list.component.ts - 122 + 123 src/app/data/matching-model.ts @@ -9110,70 +9701,70 @@ Successfully created . src/app/components/manage/management-list/management-list.component.ts - 200 + 202 Error occurred while creating . src/app/components/manage/management-list/management-list.component.ts - 205 + 207 Successfully updated "". src/app/components/manage/management-list/management-list.component.ts - 220 + 222 Error occurred while saving . src/app/components/manage/management-list/management-list.component.ts - 225 + 227 Associated documents will not be deleted. src/app/components/manage/management-list/management-list.component.ts - 245 + 247 Error while deleting element src/app/components/manage/management-list/management-list.component.ts - 261 + 263 Permissions updated successfully src/app/components/manage/management-list/management-list.component.ts - 346 + 366 This operation will permanently delete all objects. src/app/components/manage/management-list/management-list.component.ts - 367 + 387 Objects deleted successfully src/app/components/manage/management-list/management-list.component.ts - 381 + 401 Error deleting objects src/app/components/manage/management-list/management-list.component.ts - 387 + 407 @@ -9250,42 +9841,42 @@ storage path src/app/components/manage/storage-path-list/storage-path-list.component.ts - 45 + 47 storage paths src/app/components/manage/storage-path-list/storage-path-list.component.ts - 46 + 48 Do you really want to delete the storage path ""? src/app/components/manage/storage-path-list/storage-path-list.component.ts - 62 + 64 tag src/app/components/manage/tag-list/tag-list.component.ts - 45 + 47 tags src/app/components/manage/tag-list/tag-list.component.ts - 46 + 48 Do you really want to delete the tag ""? src/app/components/manage/tag-list/tag-list.component.ts - 62 + 64 @@ -9622,196 +10213,322 @@ General Settings src/app/data/paperless-config.ts - 50 + 51 OCR Settings src/app/data/paperless-config.ts - 51 + 52 Barcode Settings src/app/data/paperless-config.ts - 52 + 53 + + + + AI Settings + + src/app/data/paperless-config.ts + 54 Output Type src/app/data/paperless-config.ts - 76 + 89 Language src/app/data/paperless-config.ts - 84 + 97 Mode src/app/data/paperless-config.ts - 98 + 111 Skip Archive File src/app/data/paperless-config.ts - 106 + 119 Image DPI src/app/data/paperless-config.ts - 114 + 127 Clean src/app/data/paperless-config.ts - 121 + 134 Deskew src/app/data/paperless-config.ts - 129 + 142 Rotate Pages src/app/data/paperless-config.ts - 136 + 149 Rotate Pages Threshold src/app/data/paperless-config.ts - 143 + 156 Max Image Pixels src/app/data/paperless-config.ts - 150 + 163 Color Conversion Strategy src/app/data/paperless-config.ts - 157 + 170 OCR Arguments src/app/data/paperless-config.ts - 165 + 178 Application Logo src/app/data/paperless-config.ts - 172 + 185 Application Title src/app/data/paperless-config.ts - 179 + 192 Enable Barcodes src/app/data/paperless-config.ts - 186 + 199 Enable TIFF Support src/app/data/paperless-config.ts - 193 + 206 Barcode String src/app/data/paperless-config.ts - 200 + 213 Retain Split Pages src/app/data/paperless-config.ts - 207 + 220 Enable ASN src/app/data/paperless-config.ts - 214 + 227 ASN Prefix src/app/data/paperless-config.ts - 221 + 234 Upscale src/app/data/paperless-config.ts - 228 + 241 DPI src/app/data/paperless-config.ts - 235 + 248 Max Pages src/app/data/paperless-config.ts - 242 + 255 Enable Tag Detection src/app/data/paperless-config.ts - 249 + 262 Tag Mapping src/app/data/paperless-config.ts - 256 + 269 + + + + Split on Tag Barcodes + + src/app/data/paperless-config.ts + 276 + + + + AI Enabled + + src/app/data/paperless-config.ts + 283 + + + + Consider privacy implications when enabling AI features, especially if using a remote model. + + src/app/data/paperless-config.ts + 287 + + + + LLM Embedding Backend + + src/app/data/paperless-config.ts + 291 + + + + LLM Embedding Model + + src/app/data/paperless-config.ts + 299 + + + + LLM Backend + + src/app/data/paperless-config.ts + 306 + + + + LLM Model + + src/app/data/paperless-config.ts + 314 + + + + LLM API Key + + src/app/data/paperless-config.ts + 321 + + + + LLM Endpoint + + src/app/data/paperless-config.ts + 328 + + + + Pending + + src/app/data/share-link-bundle.ts + 41 + + + + Processing + + src/app/data/share-link-bundle.ts + 42 + + + + Ready + + src/app/data/share-link-bundle.ts + 43 + + + + Failed + + src/app/data/share-link-bundle.ts + 44 + + + + Archive + + src/app/data/share-link-bundle.ts + 51 + + + + Original + + src/app/data/share-link-bundle.ts + 52 + + + + 7 days + + src/app/data/share-link.ts + 15 + + + + 30 days + + src/app/data/share-link.ts + 16 diff --git a/src-ui/package.json b/src-ui/package.json index 7b5954b53..0536c0489 100644 --- a/src-ui/package.json +++ b/src-ui/package.json @@ -1,6 +1,6 @@ { "name": "paperless-ngx-ui", - "version": "2.20.3", + "version": "2.20.5", "scripts": { "preinstall": "npx only-allow pnpm", "ng": "ng", diff --git a/src-ui/src/app/components/admin/config/config.component.html b/src-ui/src/app/components/admin/config/config.component.html index e1d7340a6..1d38a5d32 100644 --- a/src-ui/src/app/components/admin/config/config.component.html +++ b/src-ui/src/app/components/admin/config/config.component.html @@ -35,8 +35,12 @@ @case (ConfigOptionType.String) { } @case (ConfigOptionType.JSON) { } @case (ConfigOptionType.File) { } + @case (ConfigOptionType.Password) { } } + @if (option.note) { +
{{option.note}}
+ } diff --git a/src-ui/src/app/components/admin/config/config.component.ts b/src-ui/src/app/components/admin/config/config.component.ts index eee617310..b0dcba57b 100644 --- a/src-ui/src/app/components/admin/config/config.component.ts +++ b/src-ui/src/app/components/admin/config/config.component.ts @@ -29,6 +29,7 @@ import { SettingsService } from 'src/app/services/settings.service' import { ToastService } from 'src/app/services/toast.service' import { FileComponent } from '../../common/input/file/file.component' import { NumberComponent } from '../../common/input/number/number.component' +import { PasswordComponent } from '../../common/input/password/password.component' import { SelectComponent } from '../../common/input/select/select.component' import { SwitchComponent } from '../../common/input/switch/switch.component' import { TextComponent } from '../../common/input/text/text.component' @@ -46,6 +47,7 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading TextComponent, NumberComponent, FileComponent, + PasswordComponent, AsyncPipe, NgbNavModule, FormsModule, diff --git a/src-ui/src/app/components/admin/settings/settings.component.html b/src-ui/src/app/components/admin/settings/settings.component.html index b228dac32..caec63e30 100644 --- a/src-ui/src/app/components/admin/settings/settings.component.html +++ b/src-ui/src/app/components/admin/settings/settings.component.html @@ -103,22 +103,6 @@
-
- Items per page -
-
- - - -
-
- -
Sidebar
@@ -153,8 +137,28 @@
+ +
+
Global search
+
+
+ +
+
-
Update checking
+
+
+ Full search links to +
+
+ +
+
+ +
Update checking
@@ -179,11 +183,33 @@
-
-
-
Document editing
+
+ + + +
  • + Documents + +
    +
    +
    Documents
    +
    +
    + Items per page +
    +
    + +
    +
    + +
    Document editing
    @@ -209,31 +235,32 @@
    -
    +
    -
    Global search
    -
    -
    - -
    -
    -
    -
    - Full search links to -
    -
    - +
    +

    Built-in fields to show:

    + @for (option of documentDetailFieldOptions; track option.id) { +
    + + +
    + } +

    Uncheck fields to hide them on the document details page.

    +
    +
    Bulk editing
    @@ -242,16 +269,27 @@
    +
    PDF Editor
    +
    +
    + Default editing mode +
    +
    + +
    +
    +
    Notes
    -
    -
  • diff --git a/src-ui/src/app/components/admin/settings/settings.component.spec.ts b/src-ui/src/app/components/admin/settings/settings.component.spec.ts index cc5c96640..bda1a30c9 100644 --- a/src-ui/src/app/components/admin/settings/settings.component.spec.ts +++ b/src-ui/src/app/components/admin/settings/settings.component.spec.ts @@ -91,6 +91,9 @@ const status: SystemStatus = { sanity_check_status: SystemStatusItemStatus.ERROR, sanity_check_last_run: new Date().toISOString(), sanity_check_error: 'Error running sanity check.', + llmindex_status: SystemStatusItemStatus.DISABLED, + llmindex_last_modified: new Date().toISOString(), + llmindex_error: null, }, } @@ -198,9 +201,9 @@ describe('SettingsComponent', () => { const navigateSpy = jest.spyOn(router, 'navigate') const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavLink)) tabButtons[1].nativeElement.dispatchEvent(new MouseEvent('click')) - expect(navigateSpy).toHaveBeenCalledWith(['settings', 'permissions']) + expect(navigateSpy).toHaveBeenCalledWith(['settings', 'documents']) tabButtons[2].nativeElement.dispatchEvent(new MouseEvent('click')) - expect(navigateSpy).toHaveBeenCalledWith(['settings', 'notifications']) + expect(navigateSpy).toHaveBeenCalledWith(['settings', 'permissions']) const initSpy = jest.spyOn(component, 'initialize') component.isDirty = true // mock dirty @@ -210,8 +213,8 @@ describe('SettingsComponent', () => { expect(initSpy).not.toHaveBeenCalled() navigateSpy.mockResolvedValueOnce(true) // nav accepted even though dirty - tabButtons[1].nativeElement.dispatchEvent(new MouseEvent('click')) - expect(navigateSpy).toHaveBeenCalledWith(['settings', 'notifications']) + tabButtons[2].nativeElement.dispatchEvent(new MouseEvent('click')) + expect(navigateSpy).toHaveBeenCalledWith(['settings', 'permissions']) expect(initSpy).toHaveBeenCalled() }) @@ -223,7 +226,7 @@ describe('SettingsComponent', () => { activatedRoute.snapshot.fragment = '#notifications' const scrollSpy = jest.spyOn(viewportScroller, 'scrollToAnchor') component.ngOnInit() - expect(component.activeNavID).toEqual(3) // Notifications + expect(component.activeNavID).toEqual(4) // Notifications component.ngAfterViewInit() expect(scrollSpy).toHaveBeenCalledWith('#notifications') }) @@ -248,7 +251,7 @@ describe('SettingsComponent', () => { expect(toastErrorSpy).toHaveBeenCalled() expect(storeSpy).toHaveBeenCalled() expect(appearanceSettingsSpy).not.toHaveBeenCalled() - expect(setSpy).toHaveBeenCalledTimes(30) + expect(setSpy).toHaveBeenCalledTimes(32) // succeed storeSpy.mockReturnValueOnce(of(true)) @@ -363,4 +366,22 @@ describe('SettingsComponent', () => { settingsService.settingsSaved.emit(true) expect(maybeRefreshSpy).toHaveBeenCalled() }) + + it('should support toggling document detail fields', () => { + completeSetup() + const field = 'storage_path' + expect( + component.settingsForm.get('documentDetailsHiddenFields').value.length + ).toEqual(0) + component.toggleDocumentDetailField(field, false) + expect( + component.settingsForm.get('documentDetailsHiddenFields').value.length + ).toEqual(1) + expect(component.isDocumentDetailFieldShown(field)).toBeFalsy() + component.toggleDocumentDetailField(field, true) + expect( + component.settingsForm.get('documentDetailsHiddenFields').value.length + ).toEqual(0) + expect(component.isDocumentDetailFieldShown(field)).toBeTruthy() + }) }) diff --git a/src-ui/src/app/components/admin/settings/settings.component.ts b/src-ui/src/app/components/admin/settings/settings.component.ts index 614d2fcd0..a2cfae819 100644 --- a/src-ui/src/app/components/admin/settings/settings.component.ts +++ b/src-ui/src/app/components/admin/settings/settings.component.ts @@ -64,15 +64,16 @@ import { PermissionsGroupComponent } from '../../common/input/permissions/permis import { PermissionsUserComponent } from '../../common/input/permissions/permissions-user/permissions-user.component' import { SelectComponent } from '../../common/input/select/select.component' import { PageHeaderComponent } from '../../common/page-header/page-header.component' +import { PdfEditorEditMode } from '../../common/pdf-editor/pdf-editor-edit-mode' import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component' -import { ZoomSetting } from '../../document-detail/document-detail.component' +import { ZoomSetting } from '../../document-detail/zoom-setting' import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component' enum SettingsNavIDs { General = 1, - Permissions = 2, - Notifications = 3, - SavedViews = 4, + Documents = 2, + Permissions = 3, + Notifications = 4, } const systemLanguage = { code: '', name: $localize`Use system language` } @@ -81,6 +82,25 @@ const systemDateFormat = { name: $localize`Use date format of display language`, } +export enum DocumentDetailFieldID { + ArchiveSerialNumber = 'archive_serial_number', + Correspondent = 'correspondent', + DocumentType = 'document_type', + StoragePath = 'storage_path', + Tags = 'tags', +} + +const documentDetailFieldOptions = [ + { + id: DocumentDetailFieldID.ArchiveSerialNumber, + label: $localize`Archive serial number`, + }, + { id: DocumentDetailFieldID.Correspondent, label: $localize`Correspondent` }, + { id: DocumentDetailFieldID.DocumentType, label: $localize`Document type` }, + { id: DocumentDetailFieldID.StoragePath, label: $localize`Storage path` }, + { id: DocumentDetailFieldID.Tags, label: $localize`Tags` }, +] + @Component({ selector: 'pngx-settings', templateUrl: './settings.component.html', @@ -144,8 +164,10 @@ export class SettingsComponent defaultPermsEditGroups: new FormControl(null), useNativePdfViewer: new FormControl(null), pdfViewerDefaultZoom: new FormControl(null), + pdfEditorDefaultEditMode: new FormControl(null), documentEditingRemoveInboxTags: new FormControl(null), documentEditingOverlayThumbnail: new FormControl(null), + documentDetailsHiddenFields: new FormControl([]), searchDbOnly: new FormControl(null), searchLink: new FormControl(null), @@ -176,6 +198,10 @@ export class SettingsComponent public readonly ZoomSetting = ZoomSetting + public readonly PdfEditorEditMode = PdfEditorEditMode + + public readonly documentDetailFieldOptions = documentDetailFieldOptions + get systemStatusHasErrors(): boolean { return ( this.systemStatus.database.status === SystemStatusItemStatus.ERROR || @@ -292,6 +318,9 @@ export class SettingsComponent pdfViewerDefaultZoom: this.settings.get( SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING ), + pdfEditorDefaultEditMode: this.settings.get( + SETTINGS_KEYS.PDF_EDITOR_DEFAULT_EDIT_MODE + ), displayLanguage: this.settings.getLanguage(), dateLocale: this.settings.get(SETTINGS_KEYS.DATE_LOCALE), dateFormat: this.settings.get(SETTINGS_KEYS.DATE_FORMAT), @@ -336,6 +365,9 @@ export class SettingsComponent documentEditingOverlayThumbnail: this.settings.get( SETTINGS_KEYS.DOCUMENT_EDITING_OVERLAY_THUMBNAIL ), + documentDetailsHiddenFields: this.settings.get( + SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS + ), searchDbOnly: this.settings.get(SETTINGS_KEYS.SEARCH_DB_ONLY), searchLink: this.settings.get(SETTINGS_KEYS.SEARCH_FULL_TYPE), } @@ -458,6 +490,10 @@ export class SettingsComponent SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING, this.settingsForm.value.pdfViewerDefaultZoom ) + this.settings.set( + SETTINGS_KEYS.PDF_EDITOR_DEFAULT_EDIT_MODE, + this.settingsForm.value.pdfEditorDefaultEditMode + ) this.settings.set( SETTINGS_KEYS.DATE_LOCALE, this.settingsForm.value.dateLocale @@ -526,6 +562,10 @@ export class SettingsComponent SETTINGS_KEYS.DOCUMENT_EDITING_OVERLAY_THUMBNAIL, this.settingsForm.value.documentEditingOverlayThumbnail ) + this.settings.set( + SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS, + this.settingsForm.value.documentDetailsHiddenFields + ) this.settings.set( SETTINGS_KEYS.SEARCH_DB_ONLY, this.settingsForm.value.searchDbOnly @@ -587,6 +627,26 @@ export class SettingsComponent this.settingsForm.get('themeColor').patchValue('') } + isDocumentDetailFieldShown(fieldId: string): boolean { + const hiddenFields = + this.settingsForm.value.documentDetailsHiddenFields || [] + return !hiddenFields.includes(fieldId) + } + + toggleDocumentDetailField(fieldId: string, checked: boolean) { + const hiddenFields = new Set( + this.settingsForm.value.documentDetailsHiddenFields || [] + ) + if (checked) { + hiddenFields.delete(fieldId) + } else { + hiddenFields.add(fieldId) + } + this.settingsForm + .get('documentDetailsHiddenFields') + .setValue(Array.from(hiddenFields)) + } + showSystemStatus() { const modal: NgbModalRef = this.modalService.open( SystemStatusDialogComponent, diff --git a/src-ui/src/app/components/admin/tasks/tasks.component.html b/src-ui/src/app/components/admin/tasks/tasks.component.html index 084195221..ad625789c 100644 --- a/src-ui/src/app/components/admin/tasks/tasks.component.html +++ b/src-ui/src/app/components/admin/tasks/tasks.component.html @@ -97,6 +97,12 @@
    (click for full output) } + @if (task.duplicate_documents?.length > 0) { +
    + + Duplicate(s) detected +
    + } } diff --git a/src-ui/src/app/components/app-frame/app-frame.component.html b/src-ui/src/app/components/app-frame/app-frame.component.html index 673eaf03b..62a2e16cc 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.html +++ b/src-ui/src/app/components/app-frame/app-frame.component.html @@ -30,6 +30,9 @@
      + @if (aiEnabled) { + + } diff --git a/src-ui/src/app/components/chat/chat/chat.component.scss b/src-ui/src/app/components/chat/chat/chat.component.scss new file mode 100644 index 000000000..4b00cce1b --- /dev/null +++ b/src-ui/src/app/components/chat/chat/chat.component.scss @@ -0,0 +1,37 @@ +.dropdown-menu { + width: var(--pngx-toast-max-width); +} + +.chat-messages { + max-height: 350px; + overflow-y: auto; +} + +.dropdown-toggle::after { + display: none; +} + +.dropdown-item { + white-space: initial; +} + +@media screen and (max-width: 400px) { + :host ::ng-deep .dropdown-menu-end { + right: -3rem; + } +} + +.blinking-cursor { + font-weight: bold; + font-size: 1.2em; + animation: blink 1s step-end infinite; +} + +@keyframes blink { + from, to { + opacity: 0; + } + 50% { + opacity: 1; + } +} diff --git a/src-ui/src/app/components/chat/chat/chat.component.spec.ts b/src-ui/src/app/components/chat/chat/chat.component.spec.ts new file mode 100644 index 000000000..0ccb04a99 --- /dev/null +++ b/src-ui/src/app/components/chat/chat/chat.component.spec.ts @@ -0,0 +1,132 @@ +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http' +import { provideHttpClientTesting } from '@angular/common/http/testing' +import { ElementRef } from '@angular/core' +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { NavigationEnd, Router } from '@angular/router' +import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' +import { Subject } from 'rxjs' +import { ChatService } from 'src/app/services/chat.service' +import { ChatComponent } from './chat.component' + +describe('ChatComponent', () => { + let component: ChatComponent + let fixture: ComponentFixture + let chatService: ChatService + let router: Router + let routerEvents$: Subject + let mockStream$: Subject + + beforeEach(async () => { + TestBed.configureTestingModule({ + imports: [NgxBootstrapIconsModule.pick(allIcons), ChatComponent], + providers: [ + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + ], + }).compileComponents() + + fixture = TestBed.createComponent(ChatComponent) + router = TestBed.inject(Router) + routerEvents$ = new Subject() + jest + .spyOn(router, 'events', 'get') + .mockReturnValue(routerEvents$.asObservable()) + chatService = TestBed.inject(ChatService) + mockStream$ = new Subject() + jest + .spyOn(chatService, 'streamChat') + .mockReturnValue(mockStream$.asObservable()) + component = fixture.componentInstance + + jest.useFakeTimers() + + fixture.detectChanges() + + component.scrollAnchor.nativeElement.scrollIntoView = jest.fn() + }) + + it('should update documentId on initialization', () => { + jest.spyOn(router, 'url', 'get').mockReturnValue('/documents/123') + component.ngOnInit() + expect(component.documentId).toBe(123) + }) + + it('should update documentId on navigation', () => { + component.ngOnInit() + routerEvents$.next(new NavigationEnd(1, '/documents/456', '/documents/456')) + expect(component.documentId).toBe(456) + }) + + it('should return correct placeholder based on documentId', () => { + component.documentId = 123 + expect(component.placeholder).toBe('Ask a question about this document...') + component.documentId = undefined + expect(component.placeholder).toBe('Ask a question about a document...') + }) + + it('should send a message and handle streaming response', () => { + component.input = 'Hello' + component.sendMessage() + + expect(component.messages.length).toBe(2) + expect(component.messages[0].content).toBe('Hello') + expect(component.loading).toBe(true) + + mockStream$.next('Hi') + expect(component.messages[1].content).toBe('H') + mockStream$.next('Hi there') + // advance time to process the typewriter effect + jest.advanceTimersByTime(1000) + expect(component.messages[1].content).toBe('Hi there') + + mockStream$.complete() + expect(component.loading).toBe(false) + expect(component.messages[1].isStreaming).toBe(false) + }) + + it('should handle errors during streaming', () => { + component.input = 'Hello' + component.sendMessage() + + mockStream$.error('Error') + expect(component.messages[1].content).toContain( + '⚠️ Error receiving response.' + ) + expect(component.loading).toBe(false) + }) + + it('should enqueue typewriter chunks correctly', () => { + const message = { content: '', role: 'assistant', isStreaming: true } + component.enqueueTypewriter(null, message as any) // coverage for null + component.enqueueTypewriter('Hello', message as any) + expect(component['typewriterBuffer'].length).toBe(4) + }) + + it('should scroll to bottom after sending a message', () => { + const scrollSpy = jest.spyOn( + ChatComponent.prototype as any, + 'scrollToBottom' + ) + component.input = 'Test' + component.sendMessage() + expect(scrollSpy).toHaveBeenCalled() + }) + + it('should focus chat input when dropdown is opened', () => { + const focus = jest.fn() + component.chatInput = { + nativeElement: { focus: focus }, + } as unknown as ElementRef + + component.onOpenChange(true) + jest.advanceTimersByTime(15) + expect(focus).toHaveBeenCalled() + }) + + it('should send message on Enter key press', () => { + jest.spyOn(component, 'sendMessage') + const event = new KeyboardEvent('keydown', { key: 'Enter' }) + component.searchInputKeyDown(event) + expect(component.sendMessage).toHaveBeenCalled() + }) +}) diff --git a/src-ui/src/app/components/chat/chat/chat.component.ts b/src-ui/src/app/components/chat/chat/chat.component.ts new file mode 100644 index 000000000..50d27e0b1 --- /dev/null +++ b/src-ui/src/app/components/chat/chat/chat.component.ts @@ -0,0 +1,140 @@ +import { Component, ElementRef, inject, OnInit, ViewChild } from '@angular/core' +import { FormsModule, ReactiveFormsModule } from '@angular/forms' +import { NavigationEnd, Router } from '@angular/router' +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap' +import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' +import { filter, map } from 'rxjs' +import { ChatMessage, ChatService } from 'src/app/services/chat.service' + +@Component({ + selector: 'pngx-chat', + imports: [ + FormsModule, + ReactiveFormsModule, + NgxBootstrapIconsModule, + NgbDropdownModule, + ], + templateUrl: './chat.component.html', + styleUrl: './chat.component.scss', +}) +export class ChatComponent implements OnInit { + public messages: ChatMessage[] = [] + public loading = false + public input: string = '' + public documentId!: number + + private chatService: ChatService = inject(ChatService) + private router: Router = inject(Router) + + @ViewChild('scrollAnchor') scrollAnchor!: ElementRef + @ViewChild('chatInput') chatInput!: ElementRef + + private typewriterBuffer: string[] = [] + private typewriterActive = false + + public get placeholder(): string { + return this.documentId + ? $localize`Ask a question about this document...` + : $localize`Ask a question about a document...` + } + + ngOnInit(): void { + this.updateDocumentId(this.router.url) + this.router.events + .pipe( + filter((event) => event instanceof NavigationEnd), + map((event) => (event as NavigationEnd).url) + ) + .subscribe((url) => { + this.updateDocumentId(url) + }) + } + + private updateDocumentId(url: string): void { + const docIdRe = url.match(/^\/documents\/(\d+)/) + this.documentId = docIdRe ? +docIdRe[1] : undefined + } + + sendMessage(): void { + if (!this.input.trim()) return + + const userMessage: ChatMessage = { role: 'user', content: this.input } + this.messages.push(userMessage) + this.scrollToBottom() + + const assistantMessage: ChatMessage = { + role: 'assistant', + content: '', + isStreaming: true, + } + this.messages.push(assistantMessage) + this.loading = true + + let lastPartialLength = 0 + + this.chatService.streamChat(this.documentId, this.input).subscribe({ + next: (chunk) => { + const delta = chunk.substring(lastPartialLength) + lastPartialLength = chunk.length + this.enqueueTypewriter(delta, assistantMessage) + }, + error: () => { + assistantMessage.content += '\n\n⚠️ Error receiving response.' + assistantMessage.isStreaming = false + this.loading = false + }, + complete: () => { + assistantMessage.isStreaming = false + this.loading = false + this.scrollToBottom() + }, + }) + + this.input = '' + } + + enqueueTypewriter(chunk: string, message: ChatMessage): void { + if (!chunk) return + + this.typewriterBuffer.push(...chunk.split('')) + + if (!this.typewriterActive) { + this.typewriterActive = true + this.playTypewriter(message) + } + } + + playTypewriter(message: ChatMessage): void { + if (this.typewriterBuffer.length === 0) { + this.typewriterActive = false + return + } + + const nextChar = this.typewriterBuffer.shift() + message.content += nextChar + this.scrollToBottom() + + setTimeout(() => this.playTypewriter(message), 10) // 10ms per character + } + + private scrollToBottom(): void { + setTimeout(() => { + this.scrollAnchor?.nativeElement?.scrollIntoView({ behavior: 'smooth' }) + }, 50) + } + + public onOpenChange(open: boolean): void { + if (open) { + setTimeout(() => { + this.chatInput.nativeElement.focus() + }, 10) + } + } + + public searchInputKeyDown(event: KeyboardEvent) { + if (event.key === 'Enter') { + event.preventDefault() + this.sendMessage() + } + } +} diff --git a/src-ui/src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.html b/src-ui/src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.html index f58cfeeb9..f06f37dd0 100644 --- a/src-ui/src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.html +++ b/src-ui/src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.html @@ -1,7 +1,7 @@ -
      -
      diff --git a/src-ui/src/app/components/common/dates-dropdown/dates-dropdown.component.html b/src-ui/src/app/components/common/dates-dropdown/dates-dropdown.component.html index 74b49bbdb..2057a79ff 100644 --- a/src-ui/src/app/components/common/dates-dropdown/dates-dropdown.component.html +++ b/src-ui/src/app/components/common/dates-dropdown/dates-dropdown.component.html @@ -164,9 +164,11 @@ {{ item.name }} @if (item.dateEnd) { - {{ item.date | customDate:'MMM d' }} – {{ item.dateEnd | customDate:'mediumDate' }} + {{ item.date | customDate:'mediumDate' }} – {{ item.dateEnd | customDate:'mediumDate' }} + } @else if (item.dateTilNow) { + {{ item.dateTilNow | customDate:'mediumDate' }} – now } @else { - {{ item.date | customDate:'mediumDate' }} – now + {{ item.date | customDate:'mediumDate' }} }
      diff --git a/src-ui/src/app/components/common/dates-dropdown/dates-dropdown.component.ts b/src-ui/src/app/components/common/dates-dropdown/dates-dropdown.component.ts index e07b08959..42bd3b0e4 100644 --- a/src-ui/src/app/components/common/dates-dropdown/dates-dropdown.component.ts +++ b/src-ui/src/app/components/common/dates-dropdown/dates-dropdown.component.ts @@ -79,32 +79,34 @@ export class DatesDropdownComponent implements OnInit, OnDestroy { { id: RelativeDate.WITHIN_1_WEEK, name: $localize`Within 1 week`, - date: new Date().setDate(new Date().getDate() - 7), + dateTilNow: new Date().setDate(new Date().getDate() - 7), }, { id: RelativeDate.WITHIN_1_MONTH, name: $localize`Within 1 month`, - date: new Date().setMonth(new Date().getMonth() - 1), + dateTilNow: new Date().setMonth(new Date().getMonth() - 1), }, { id: RelativeDate.WITHIN_3_MONTHS, name: $localize`Within 3 months`, - date: new Date().setMonth(new Date().getMonth() - 3), + dateTilNow: new Date().setMonth(new Date().getMonth() - 3), }, { id: RelativeDate.WITHIN_1_YEAR, name: $localize`Within 1 year`, - date: new Date().setFullYear(new Date().getFullYear() - 1), + dateTilNow: new Date().setFullYear(new Date().getFullYear() - 1), }, { id: RelativeDate.THIS_YEAR, name: $localize`This year`, date: new Date('1/1/' + new Date().getFullYear()), + dateEnd: new Date('12/31/' + new Date().getFullYear()), }, { id: RelativeDate.THIS_MONTH, name: $localize`This month`, date: new Date().setDate(1), + dateEnd: new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0), }, { id: RelativeDate.TODAY, diff --git a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.spec.ts b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.spec.ts index aa52592b1..ac8a5d2c7 100644 --- a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.spec.ts +++ b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.spec.ts @@ -252,7 +252,7 @@ describe('WorkflowEditDialogComponent', () => { expect(component.object.actions.length).toEqual(2) }) - it('should update order and remove ids from actions on drag n drop', () => { + it('should update order on drag n drop', () => { const action1 = workflow.actions[0] const action2 = workflow.actions[1] component.object = workflow @@ -261,8 +261,6 @@ describe('WorkflowEditDialogComponent', () => { WorkflowAction[] >) expect(component.object.actions).toEqual([action2, action1]) - expect(action1.id).toBeNull() - expect(action2.id).toBeNull() }) it('should not include auto matching in algorithms', () => { @@ -414,6 +412,9 @@ describe('WorkflowEditDialogComponent', () => { return newFilter } + const correspondentAny = addFilterOfType(TriggerFilterType.CorrespondentAny) + correspondentAny.get('values').setValue([11]) + const correspondentIs = addFilterOfType(TriggerFilterType.CorrespondentIs) correspondentIs.get('values').setValue(1) @@ -423,12 +424,18 @@ describe('WorkflowEditDialogComponent', () => { const documentTypeIs = addFilterOfType(TriggerFilterType.DocumentTypeIs) documentTypeIs.get('values').setValue(1) + const documentTypeAny = addFilterOfType(TriggerFilterType.DocumentTypeAny) + documentTypeAny.get('values').setValue([12]) + const documentTypeNot = addFilterOfType(TriggerFilterType.DocumentTypeNot) documentTypeNot.get('values').setValue([1]) const storagePathIs = addFilterOfType(TriggerFilterType.StoragePathIs) storagePathIs.get('values').setValue(1) + const storagePathAny = addFilterOfType(TriggerFilterType.StoragePathAny) + storagePathAny.get('values').setValue([13]) + const storagePathNot = addFilterOfType(TriggerFilterType.StoragePathNot) storagePathNot.get('values').setValue([1]) @@ -443,10 +450,13 @@ describe('WorkflowEditDialogComponent', () => { expect(formValues.triggers[0].filter_has_tags).toEqual([1]) expect(formValues.triggers[0].filter_has_all_tags).toEqual([2, 3]) expect(formValues.triggers[0].filter_has_not_tags).toEqual([4]) + expect(formValues.triggers[0].filter_has_any_correspondents).toEqual([11]) expect(formValues.triggers[0].filter_has_correspondent).toEqual(1) expect(formValues.triggers[0].filter_has_not_correspondents).toEqual([1]) + expect(formValues.triggers[0].filter_has_any_document_types).toEqual([12]) expect(formValues.triggers[0].filter_has_document_type).toEqual(1) expect(formValues.triggers[0].filter_has_not_document_types).toEqual([1]) + expect(formValues.triggers[0].filter_has_any_storage_paths).toEqual([13]) expect(formValues.triggers[0].filter_has_storage_path).toEqual(1) expect(formValues.triggers[0].filter_has_not_storage_paths).toEqual([1]) expect(formValues.triggers[0].filter_custom_field_query).toEqual( @@ -509,16 +519,22 @@ describe('WorkflowEditDialogComponent', () => { setFilter(TriggerFilterType.TagsAll, 11) setFilter(TriggerFilterType.TagsNone, 12) + setFilter(TriggerFilterType.CorrespondentAny, 16) setFilter(TriggerFilterType.CorrespondentNot, 13) + setFilter(TriggerFilterType.DocumentTypeAny, 17) setFilter(TriggerFilterType.DocumentTypeNot, 14) + setFilter(TriggerFilterType.StoragePathAny, 18) setFilter(TriggerFilterType.StoragePathNot, 15) const formValues = component['getFormValues']() expect(formValues.triggers[0].filter_has_all_tags).toEqual([11]) expect(formValues.triggers[0].filter_has_not_tags).toEqual([12]) + expect(formValues.triggers[0].filter_has_any_correspondents).toEqual([16]) expect(formValues.triggers[0].filter_has_not_correspondents).toEqual([13]) + expect(formValues.triggers[0].filter_has_any_document_types).toEqual([17]) expect(formValues.triggers[0].filter_has_not_document_types).toEqual([14]) + expect(formValues.triggers[0].filter_has_any_storage_paths).toEqual([18]) expect(formValues.triggers[0].filter_has_not_storage_paths).toEqual([15]) }) @@ -642,8 +658,11 @@ describe('WorkflowEditDialogComponent', () => { filter_has_tags: [], filter_has_all_tags: [], filter_has_not_tags: [], + filter_has_any_correspondents: [], filter_has_not_correspondents: [], + filter_has_any_document_types: [], filter_has_not_document_types: [], + filter_has_any_storage_paths: [], filter_has_not_storage_paths: [], filter_has_correspondent: null, filter_has_document_type: null, @@ -701,11 +720,14 @@ describe('WorkflowEditDialogComponent', () => { trigger.filter_has_tags = [1] trigger.filter_has_all_tags = [2, 3] trigger.filter_has_not_tags = [4] + trigger.filter_has_any_correspondents = [10] as any trigger.filter_has_correspondent = 5 as any trigger.filter_has_not_correspondents = [6] as any trigger.filter_has_document_type = 7 as any + trigger.filter_has_any_document_types = [11] as any trigger.filter_has_not_document_types = [8] as any trigger.filter_has_storage_path = 9 as any + trigger.filter_has_any_storage_paths = [12] as any trigger.filter_has_not_storage_paths = [10] as any trigger.filter_custom_field_query = JSON.stringify([ 'AND', @@ -716,8 +738,8 @@ describe('WorkflowEditDialogComponent', () => { component.ngOnInit() const triggerGroup = component.triggerFields.at(0) as FormGroup const filters = component.getFiltersFormArray(triggerGroup) - expect(filters.length).toBe(10) - const customFieldFilter = filters.at(9) as FormGroup + expect(filters.length).toBe(13) + const customFieldFilter = filters.at(12) as FormGroup expect(customFieldFilter.get('type').value).toBe( TriggerFilterType.CustomFieldQuery ) @@ -726,12 +748,27 @@ describe('WorkflowEditDialogComponent', () => { }) it('should expose select metadata helpers', () => { + expect(component.isSelectMultiple(TriggerFilterType.CorrespondentAny)).toBe( + true + ) expect(component.isSelectMultiple(TriggerFilterType.CorrespondentNot)).toBe( true ) expect(component.isSelectMultiple(TriggerFilterType.CorrespondentIs)).toBe( false ) + expect(component.isSelectMultiple(TriggerFilterType.DocumentTypeAny)).toBe( + true + ) + expect(component.isSelectMultiple(TriggerFilterType.DocumentTypeIs)).toBe( + false + ) + expect(component.isSelectMultiple(TriggerFilterType.StoragePathAny)).toBe( + true + ) + expect(component.isSelectMultiple(TriggerFilterType.StoragePathIs)).toBe( + false + ) component.correspondents = [{ id: 1, name: 'C1' } as any] component.documentTypes = [{ id: 2, name: 'DT' } as any] @@ -743,9 +780,15 @@ describe('WorkflowEditDialogComponent', () => { expect( component.getFilterSelectItems(TriggerFilterType.DocumentTypeIs) ).toEqual(component.documentTypes) + expect( + component.getFilterSelectItems(TriggerFilterType.DocumentTypeAny) + ).toEqual(component.documentTypes) expect( component.getFilterSelectItems(TriggerFilterType.StoragePathIs) ).toEqual(component.storagePaths) + expect( + component.getFilterSelectItems(TriggerFilterType.StoragePathAny) + ).toEqual(component.storagePaths) expect(component.getFilterSelectItems(TriggerFilterType.TagsAll)).toEqual( [] ) diff --git a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts index f6d9e60f5..94d8318e0 100644 --- a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts +++ b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts @@ -145,10 +145,13 @@ export enum TriggerFilterType { TagsAny = 'tags_any', TagsAll = 'tags_all', TagsNone = 'tags_none', + CorrespondentAny = 'correspondent_any', CorrespondentIs = 'correspondent_is', CorrespondentNot = 'correspondent_not', + DocumentTypeAny = 'document_type_any', DocumentTypeIs = 'document_type_is', DocumentTypeNot = 'document_type_not', + StoragePathAny = 'storage_path_any', StoragePathIs = 'storage_path_is', StoragePathNot = 'storage_path_not', CustomFieldQuery = 'custom_field_query', @@ -172,8 +175,11 @@ type TriggerFilterAggregate = { filter_has_tags: number[] filter_has_all_tags: number[] filter_has_not_tags: number[] + filter_has_any_correspondents: number[] filter_has_not_correspondents: number[] + filter_has_any_document_types: number[] filter_has_not_document_types: number[] + filter_has_any_storage_paths: number[] filter_has_not_storage_paths: number[] filter_has_correspondent: number | null filter_has_document_type: number | null @@ -219,6 +225,14 @@ const TRIGGER_FILTER_DEFINITIONS: TriggerFilterDefinition[] = [ allowMultipleEntries: false, allowMultipleValues: true, }, + { + id: TriggerFilterType.CorrespondentAny, + name: $localize`Has any of these correspondents`, + inputType: 'select', + allowMultipleEntries: false, + allowMultipleValues: true, + selectItems: 'correspondents', + }, { id: TriggerFilterType.CorrespondentIs, name: $localize`Has correspondent`, @@ -243,6 +257,14 @@ const TRIGGER_FILTER_DEFINITIONS: TriggerFilterDefinition[] = [ allowMultipleValues: false, selectItems: 'documentTypes', }, + { + id: TriggerFilterType.DocumentTypeAny, + name: $localize`Has any of these document types`, + inputType: 'select', + allowMultipleEntries: false, + allowMultipleValues: true, + selectItems: 'documentTypes', + }, { id: TriggerFilterType.DocumentTypeNot, name: $localize`Does not have document types`, @@ -259,6 +281,14 @@ const TRIGGER_FILTER_DEFINITIONS: TriggerFilterDefinition[] = [ allowMultipleValues: false, selectItems: 'storagePaths', }, + { + id: TriggerFilterType.StoragePathAny, + name: $localize`Has any of these storage paths`, + inputType: 'select', + allowMultipleEntries: false, + allowMultipleValues: true, + selectItems: 'storagePaths', + }, { id: TriggerFilterType.StoragePathNot, name: $localize`Does not have storage paths`, @@ -306,6 +336,15 @@ const FILTER_HANDLERS: Record = { extract: (trigger) => trigger.filter_has_not_tags, hasValue: (value) => Array.isArray(value) && value.length > 0, }, + [TriggerFilterType.CorrespondentAny]: { + apply: (aggregate, values) => { + aggregate.filter_has_any_correspondents = Array.isArray(values) + ? [...values] + : [values] + }, + extract: (trigger) => trigger.filter_has_any_correspondents, + hasValue: (value) => Array.isArray(value) && value.length > 0, + }, [TriggerFilterType.CorrespondentIs]: { apply: (aggregate, values) => { aggregate.filter_has_correspondent = Array.isArray(values) @@ -333,6 +372,15 @@ const FILTER_HANDLERS: Record = { extract: (trigger) => trigger.filter_has_document_type, hasValue: (value) => value !== null && value !== undefined, }, + [TriggerFilterType.DocumentTypeAny]: { + apply: (aggregate, values) => { + aggregate.filter_has_any_document_types = Array.isArray(values) + ? [...values] + : [values] + }, + extract: (trigger) => trigger.filter_has_any_document_types, + hasValue: (value) => Array.isArray(value) && value.length > 0, + }, [TriggerFilterType.DocumentTypeNot]: { apply: (aggregate, values) => { aggregate.filter_has_not_document_types = Array.isArray(values) @@ -351,6 +399,15 @@ const FILTER_HANDLERS: Record = { extract: (trigger) => trigger.filter_has_storage_path, hasValue: (value) => value !== null && value !== undefined, }, + [TriggerFilterType.StoragePathAny]: { + apply: (aggregate, values) => { + aggregate.filter_has_any_storage_paths = Array.isArray(values) + ? [...values] + : [values] + }, + extract: (trigger) => trigger.filter_has_any_storage_paths, + hasValue: (value) => Array.isArray(value) && value.length > 0, + }, [TriggerFilterType.StoragePathNot]: { apply: (aggregate, values) => { aggregate.filter_has_not_storage_paths = Array.isArray(values) @@ -642,8 +699,11 @@ export class WorkflowEditDialogComponent filter_has_tags: [], filter_has_all_tags: [], filter_has_not_tags: [], + filter_has_any_correspondents: [], filter_has_not_correspondents: [], + filter_has_any_document_types: [], filter_has_not_document_types: [], + filter_has_any_storage_paths: [], filter_has_not_storage_paths: [], filter_has_correspondent: null, filter_has_document_type: null, @@ -670,10 +730,16 @@ export class WorkflowEditDialogComponent trigger.filter_has_tags = aggregate.filter_has_tags trigger.filter_has_all_tags = aggregate.filter_has_all_tags trigger.filter_has_not_tags = aggregate.filter_has_not_tags + trigger.filter_has_any_correspondents = + aggregate.filter_has_any_correspondents trigger.filter_has_not_correspondents = aggregate.filter_has_not_correspondents + trigger.filter_has_any_document_types = + aggregate.filter_has_any_document_types trigger.filter_has_not_document_types = aggregate.filter_has_not_document_types + trigger.filter_has_any_storage_paths = + aggregate.filter_has_any_storage_paths trigger.filter_has_not_storage_paths = aggregate.filter_has_not_storage_paths trigger.filter_has_correspondent = @@ -856,8 +922,11 @@ export class WorkflowEditDialogComponent case TriggerFilterType.TagsAny: case TriggerFilterType.TagsAll: case TriggerFilterType.TagsNone: + case TriggerFilterType.CorrespondentAny: case TriggerFilterType.CorrespondentNot: + case TriggerFilterType.DocumentTypeAny: case TriggerFilterType.DocumentTypeNot: + case TriggerFilterType.StoragePathAny: case TriggerFilterType.StoragePathNot: return true default: @@ -1179,8 +1248,11 @@ export class WorkflowEditDialogComponent filter_has_tags: [], filter_has_all_tags: [], filter_has_not_tags: [], + filter_has_any_correspondents: [], filter_has_not_correspondents: [], + filter_has_any_document_types: [], filter_has_not_document_types: [], + filter_has_any_storage_paths: [], filter_has_not_storage_paths: [], filter_custom_field_query: null, filter_has_correspondent: null, @@ -1283,11 +1355,6 @@ export class WorkflowEditDialogComponent const actionField = this.actionFields.at(event.previousIndex) this.actionFields.removeAt(event.previousIndex) this.actionFields.insert(event.currentIndex, actionField) - // removing id will effectively re-create the actions in this order - this.object.actions.forEach((a) => (a.id = null)) - this.actionFields.controls.forEach((c) => - c.get('id').setValue(null, { emitEvent: false }) - ) } save(): void { diff --git a/src-ui/src/app/components/common/input/password/password.component.html b/src-ui/src/app/components/common/input/password/password.component.html index 9daa4be5f..b5d88f79c 100644 --- a/src-ui/src/app/components/common/input/password/password.component.html +++ b/src-ui/src/app/components/common/input/password/password.component.html @@ -1,17 +1,24 @@ -
      - -
      - - @if (showReveal) { - +
      +
      +
      + @if (title) { + + } +
      +
      +
      + + @if (showReveal) { + + } +
      +
      + {{error}} +
      + @if (hint) { + }
      -
      - {{error}} -
      - @if (hint) { - - }
      diff --git a/src-ui/src/app/components/common/input/tags/tags.component.html b/src-ui/src/app/components/common/input/tags/tags.component.html index f04863f40..960245984 100644 --- a/src-ui/src/app/components/common/input/tags/tags.component.html +++ b/src-ui/src/app/components/common/input/tags/tags.component.html @@ -28,7 +28,7 @@ -
      +
      @if (item.id && tags) { @if (getTag(item.id)?.parent) { diff --git a/src-ui/src/app/components/common/input/tags/tags.component.scss b/src-ui/src/app/components/common/input/tags/tags.component.scss index 52292d5cb..2f06247bd 100644 --- a/src-ui/src/app/components/common/input/tags/tags.component.scss +++ b/src-ui/src/app/components/common/input/tags/tags.component.scss @@ -22,8 +22,8 @@ } // Dropdown hierarchy reveal for ng-select options -::ng-deep .ng-dropdown-panel .ng-option { - overflow-x: scroll; +:host ::ng-deep .ng-dropdown-panel .ng-option { + overflow-x: auto !important; .tag-option-row { font-size: 1rem; @@ -41,12 +41,12 @@ } } -::ng-deep .ng-dropdown-panel .ng-option:hover .hierarchy-reveal, -::ng-deep .ng-dropdown-panel .ng-option.ng-option-marked .hierarchy-reveal { +:host ::ng-deep .ng-dropdown-panel .ng-option:hover .hierarchy-reveal, +:host ::ng-deep .ng-dropdown-panel .ng-option.ng-option-marked .hierarchy-reveal { max-width: 1000px; } ::ng-deep .ng-dropdown-panel .ng-option:hover .hierarchy-indicator, -::ng-deep .ng-dropdown-panel .ng-option.ng-option-marked .hierarchy-indicator { +:host ::ng-deep .ng-dropdown-panel .ng-option.ng-option-marked .hierarchy-indicator { background: transparent; } diff --git a/src-ui/src/app/components/common/input/text/text.component.html b/src-ui/src/app/components/common/input/text/text.component.html index dcc43f72c..5bf4dd93a 100644 --- a/src-ui/src/app/components/common/input/text/text.component.html +++ b/src-ui/src/app/components/common/input/text/text.component.html @@ -15,6 +15,12 @@ @if (hint) { } + @if (getSuggestion()?.length > 0) { + + Suggestion:  + {{getSuggestion()}}  + + }
      {{error}}
      diff --git a/src-ui/src/app/components/common/input/text/text.component.spec.ts b/src-ui/src/app/components/common/input/text/text.component.spec.ts index c5662b341..539c1eb6b 100644 --- a/src-ui/src/app/components/common/input/text/text.component.spec.ts +++ b/src-ui/src/app/components/common/input/text/text.component.spec.ts @@ -26,10 +26,20 @@ describe('TextComponent', () => { it('should support use of input field', () => { expect(component.value).toBeUndefined() - // TODO: why doesn't this work? - // input.value = 'foo' - // input.dispatchEvent(new Event('change')) - // fixture.detectChanges() - // expect(component.value).toEqual('foo') + input.value = 'foo' + input.dispatchEvent(new Event('input')) + fixture.detectChanges() + expect(component.value).toBe('foo') + }) + + it('should support suggestion', () => { + component.value = 'foo' + component.suggestion = 'foo' + expect(component.getSuggestion()).toBe('') + component.value = 'bar' + expect(component.getSuggestion()).toBe('foo') + component.applySuggestion() + fixture.detectChanges() + expect(component.value).toBe('foo') }) }) diff --git a/src-ui/src/app/components/common/input/text/text.component.ts b/src-ui/src/app/components/common/input/text/text.component.ts index 283a8eb71..22b1fed4a 100644 --- a/src-ui/src/app/components/common/input/text/text.component.ts +++ b/src-ui/src/app/components/common/input/text/text.component.ts @@ -4,6 +4,7 @@ import { NG_VALUE_ACCESSOR, ReactiveFormsModule, } from '@angular/forms' +import { RouterLink } from '@angular/router' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { AbstractInputComponent } from '../abstract-input' @@ -18,7 +19,12 @@ import { AbstractInputComponent } from '../abstract-input' selector: 'pngx-input-text', templateUrl: './text.component.html', styleUrls: ['./text.component.scss'], - imports: [FormsModule, ReactiveFormsModule, NgxBootstrapIconsModule], + imports: [ + FormsModule, + ReactiveFormsModule, + NgxBootstrapIconsModule, + RouterLink, + ], }) export class TextComponent extends AbstractInputComponent { @Input() @@ -27,7 +33,19 @@ export class TextComponent extends AbstractInputComponent { @Input() placeholder: string = '' + @Input() + suggestion: string = '' + constructor() { super() } + + getSuggestion() { + return this.value !== this.suggestion ? this.suggestion : '' + } + + applySuggestion() { + this.value = this.suggestion + this.onChange(this.value) + } } diff --git a/src-ui/src/app/components/common/page-header/page-header.component.html b/src-ui/src/app/components/common/page-header/page-header.component.html index 283218219..488fff59d 100644 --- a/src-ui/src/app/components/common/page-header/page-header.component.html +++ b/src-ui/src/app/components/common/page-header/page-header.component.html @@ -1,9 +1,18 @@
      -

      - {{title}} +

      + {{title}} + @if (id) { + + @if (copied) { +  Copied! + } @else { + ID: {{id}} + } + + } @if (subTitle) { - {{subTitle}} + {{subTitle}} } @if (info) { +

      + + diff --git a/src-ui/src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.scss b/src-ui/src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src-ui/src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.spec.ts b/src-ui/src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.spec.ts new file mode 100644 index 000000000..da4d93c6a --- /dev/null +++ b/src-ui/src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.spec.ts @@ -0,0 +1,161 @@ +import { Clipboard } from '@angular/cdk/clipboard' +import { + ComponentFixture, + TestBed, + fakeAsync, + tick, +} from '@angular/core/testing' +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' +import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' +import { FileVersion } from 'src/app/data/share-link' +import { + ShareLinkBundleStatus, + ShareLinkBundleSummary, +} from 'src/app/data/share-link-bundle' +import { ToastService } from 'src/app/services/toast.service' +import { environment } from 'src/environments/environment' +import { ShareLinkBundleDialogComponent } from './share-link-bundle-dialog.component' + +class MockToastService { + showInfo = jest.fn() + showError = jest.fn() +} + +describe('ShareLinkBundleDialogComponent', () => { + let component: ShareLinkBundleDialogComponent + let fixture: ComponentFixture + let clipboard: Clipboard + let toastService: MockToastService + let activeModal: NgbActiveModal + let originalApiBaseUrl: string + + beforeEach(() => { + originalApiBaseUrl = environment.apiBaseUrl + toastService = new MockToastService() + + TestBed.configureTestingModule({ + imports: [ + ShareLinkBundleDialogComponent, + NgxBootstrapIconsModule.pick(allIcons), + ], + providers: [ + NgbActiveModal, + { provide: ToastService, useValue: toastService }, + ], + }) + + fixture = TestBed.createComponent(ShareLinkBundleDialogComponent) + component = fixture.componentInstance + clipboard = TestBed.inject(Clipboard) + activeModal = TestBed.inject(NgbActiveModal) + fixture.detectChanges() + }) + + afterEach(() => { + jest.clearAllTimers() + environment.apiBaseUrl = originalApiBaseUrl + }) + + it('builds payload and emits confirm on submit', () => { + const confirmSpy = jest.spyOn(component.confirmClicked, 'emit') + component.documents = [ + { id: 1, title: 'Doc 1' } as any, + { id: 2, title: 'Doc 2' } as any, + ] + component.form.setValue({ + shareArchiveVersion: false, + expirationDays: 3, + }) + + component.submit() + + expect(component.payload).toEqual({ + document_ids: [1, 2], + file_version: FileVersion.Original, + expiration_days: 3, + }) + expect(component.buttonsEnabled).toBe(false) + expect(confirmSpy).toHaveBeenCalled() + + component.form.setValue({ + shareArchiveVersion: true, + expirationDays: 7, + }) + component.submit() + + expect(component.payload).toEqual({ + document_ids: [1, 2], + file_version: FileVersion.Archive, + expiration_days: 7, + }) + }) + + it('ignores submit when bundle already created', () => { + component.createdBundle = { id: 1 } as ShareLinkBundleSummary + const confirmSpy = jest.spyOn(component, 'confirm') + component.submit() + expect(confirmSpy).not.toHaveBeenCalled() + }) + + it('limits preview to ten documents', () => { + const docs = Array.from({ length: 12 }).map((_, index) => ({ + id: index + 1, + })) + component.documents = docs as any + + expect(component.selectionCount).toBe(12) + expect(component.documentPreview).toHaveLength(10) + expect(component.documentPreview[0].id).toBe(1) + }) + + it('copies share link and resets state after timeout', fakeAsync(() => { + const copySpy = jest.spyOn(clipboard, 'copy').mockReturnValue(true) + const bundle = { + slug: 'bundle-slug', + status: ShareLinkBundleStatus.Ready, + } as ShareLinkBundleSummary + + component.copy(bundle) + + expect(copySpy).toHaveBeenCalledWith(component.getShareUrl(bundle)) + expect(component.copied).toBe(true) + expect(toastService.showInfo).toHaveBeenCalled() + + tick(3000) + expect(component.copied).toBe(false) + })) + + it('generates share URLs based on API base URL', () => { + environment.apiBaseUrl = 'https://example.com/api/' + expect( + component.getShareUrl({ slug: 'abc' } as ShareLinkBundleSummary) + ).toBe('https://example.com/share/abc') + }) + + it('opens manage dialog when callback provided', () => { + const manageSpy = jest.fn() + component.onOpenManage = manageSpy + component.openManage() + expect(manageSpy).toHaveBeenCalled() + }) + + it('falls back to cancel when manage callback missing', () => { + const cancelSpy = jest.spyOn(component, 'cancel') + component.onOpenManage = undefined + component.openManage() + expect(cancelSpy).toHaveBeenCalled() + }) + + it('maps status and file version labels', () => { + expect(component.statusLabel(ShareLinkBundleStatus.Processing)).toContain( + 'Processing' + ) + expect(component.fileVersionLabel(FileVersion.Archive)).toContain('Archive') + }) + + it('closes dialog when cancel invoked', () => { + const closeSpy = jest.spyOn(activeModal, 'close') + component.cancel() + expect(closeSpy).toHaveBeenCalled() + }) +}) diff --git a/src-ui/src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.ts b/src-ui/src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.ts new file mode 100644 index 000000000..37aa70950 --- /dev/null +++ b/src-ui/src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.ts @@ -0,0 +1,118 @@ +import { Clipboard } from '@angular/cdk/clipboard' +import { CommonModule } from '@angular/common' +import { Component, Input, inject } from '@angular/core' +import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms' +import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' +import { Document } from 'src/app/data/document' +import { + FileVersion, + SHARE_LINK_EXPIRATION_OPTIONS, +} from 'src/app/data/share-link' +import { + SHARE_LINK_BUNDLE_FILE_VERSION_LABELS, + SHARE_LINK_BUNDLE_STATUS_LABELS, + ShareLinkBundleCreatePayload, + ShareLinkBundleStatus, + ShareLinkBundleSummary, +} from 'src/app/data/share-link-bundle' +import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe' +import { FileSizePipe } from 'src/app/pipes/file-size.pipe' +import { ToastService } from 'src/app/services/toast.service' +import { environment } from 'src/environments/environment' +import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog.component' + +@Component({ + selector: 'pngx-share-link-bundle-dialog', + templateUrl: './share-link-bundle-dialog.component.html', + imports: [ + CommonModule, + ReactiveFormsModule, + NgxBootstrapIconsModule, + FileSizePipe, + DocumentTitlePipe, + ], + providers: [], +}) +export class ShareLinkBundleDialogComponent extends ConfirmDialogComponent { + private readonly formBuilder = inject(FormBuilder) + private readonly clipboard = inject(Clipboard) + private readonly toastService = inject(ToastService) + + private _documents: Document[] = [] + + selectionCount = 0 + documentPreview: Document[] = [] + form: FormGroup = this.formBuilder.group({ + shareArchiveVersion: true, + expirationDays: [7], + }) + payload: ShareLinkBundleCreatePayload | null = null + + readonly expirationOptions = SHARE_LINK_EXPIRATION_OPTIONS + + createdBundle: ShareLinkBundleSummary | null = null + copied = false + onOpenManage?: () => void + readonly statuses = ShareLinkBundleStatus + + constructor() { + super() + this.loading = false + this.title = $localize`Create share link bundle` + this.btnCaption = $localize`Create link` + } + + @Input() + set documents(docs: Document[]) { + this._documents = docs.concat() + this.selectionCount = this._documents.length + this.documentPreview = this._documents.slice(0, 10) + } + + submit() { + if (this.createdBundle) return + this.payload = { + document_ids: this._documents.map((doc) => doc.id), + file_version: this.form.value.shareArchiveVersion + ? FileVersion.Archive + : FileVersion.Original, + expiration_days: this.form.value.expirationDays, + } + this.buttonsEnabled = false + super.confirm() + } + + getShareUrl(bundle: ShareLinkBundleSummary): string { + const apiURL = new URL(environment.apiBaseUrl) + return `${apiURL.origin}${apiURL.pathname.replace(/\/api\/$/, '/share/')}${ + bundle.slug + }` + } + + copy(bundle: ShareLinkBundleSummary): void { + const success = this.clipboard.copy(this.getShareUrl(bundle)) + if (success) { + this.copied = true + this.toastService.showInfo($localize`Share link copied to clipboard.`) + setTimeout(() => { + this.copied = false + }, 3000) + } + } + + openManage(): void { + if (this.onOpenManage) { + this.onOpenManage() + } else { + this.cancel() + } + } + + statusLabel(status: ShareLinkBundleSummary['status']): string { + return SHARE_LINK_BUNDLE_STATUS_LABELS[status] ?? status + } + + fileVersionLabel(version: FileVersion): string { + return SHARE_LINK_BUNDLE_FILE_VERSION_LABELS[version] ?? version + } +} diff --git a/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html b/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html new file mode 100644 index 000000000..2f2155412 --- /dev/null +++ b/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html @@ -0,0 +1,156 @@ + + + + + diff --git a/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.scss b/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.scss new file mode 100644 index 000000000..c8ffc4d5d --- /dev/null +++ b/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.scss @@ -0,0 +1,4 @@ +:host ::ng-deep .popover { + min-width: 300px; + max-width: 400px; + } diff --git a/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.spec.ts b/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.spec.ts new file mode 100644 index 000000000..113cd65a3 --- /dev/null +++ b/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.spec.ts @@ -0,0 +1,251 @@ +import { Clipboard } from '@angular/cdk/clipboard' +import { + ComponentFixture, + TestBed, + fakeAsync, + tick, +} from '@angular/core/testing' +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' +import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' +import { of, throwError } from 'rxjs' +import { FileVersion } from 'src/app/data/share-link' +import { + ShareLinkBundleStatus, + ShareLinkBundleSummary, +} from 'src/app/data/share-link-bundle' +import { ShareLinkBundleService } from 'src/app/services/rest/share-link-bundle.service' +import { ToastService } from 'src/app/services/toast.service' +import { environment } from 'src/environments/environment' +import { ShareLinkBundleManageDialogComponent } from './share-link-bundle-manage-dialog.component' + +class MockShareLinkBundleService { + listAllBundles = jest.fn() + delete = jest.fn() + rebuildBundle = jest.fn() +} + +class MockToastService { + showInfo = jest.fn() + showError = jest.fn() +} + +describe('ShareLinkBundleManageDialogComponent', () => { + let component: ShareLinkBundleManageDialogComponent + let fixture: ComponentFixture + let service: MockShareLinkBundleService + let toastService: MockToastService + let clipboard: Clipboard + let activeModal: NgbActiveModal + let originalApiBaseUrl: string + + beforeEach(() => { + service = new MockShareLinkBundleService() + toastService = new MockToastService() + originalApiBaseUrl = environment.apiBaseUrl + + service.listAllBundles.mockReturnValue(of([])) + service.delete.mockReturnValue(of(true)) + service.rebuildBundle.mockReturnValue(of(sampleBundle())) + + TestBed.configureTestingModule({ + imports: [ + ShareLinkBundleManageDialogComponent, + NgxBootstrapIconsModule.pick(allIcons), + ], + providers: [ + NgbActiveModal, + { provide: ShareLinkBundleService, useValue: service }, + { provide: ToastService, useValue: toastService }, + ], + }) + + fixture = TestBed.createComponent(ShareLinkBundleManageDialogComponent) + component = fixture.componentInstance + clipboard = TestBed.inject(Clipboard) + activeModal = TestBed.inject(NgbActiveModal) + }) + + afterEach(() => { + component.ngOnDestroy() + fixture.destroy() + environment.apiBaseUrl = originalApiBaseUrl + jest.clearAllMocks() + }) + + const sampleBundle = (overrides: Partial = {}) => + ({ + id: 1, + slug: 'bundle-slug', + created: new Date().toISOString(), + document_count: 1, + documents: [1], + status: ShareLinkBundleStatus.Pending, + file_version: FileVersion.Archive, + last_error: undefined, + ...overrides, + }) as ShareLinkBundleSummary + + it('loads bundles on init and polls periodically', fakeAsync(() => { + const bundles = [sampleBundle({ status: ShareLinkBundleStatus.Ready })] + service.listAllBundles.mockReset() + service.listAllBundles + .mockReturnValueOnce(of(bundles)) + .mockReturnValue(of(bundles)) + + fixture.detectChanges() + tick() + + expect(service.listAllBundles).toHaveBeenCalledTimes(1) + expect(component.bundles).toEqual(bundles) + expect(component.loading).toBe(false) + expect(component.error).toBeNull() + + tick(5000) + expect(service.listAllBundles).toHaveBeenCalledTimes(2) + })) + + it('handles errors when loading bundles', fakeAsync(() => { + service.listAllBundles.mockReset() + service.listAllBundles + .mockReturnValueOnce(throwError(() => new Error('load fail'))) + .mockReturnValue(of([])) + + fixture.detectChanges() + tick() + + expect(component.error).toContain('Failed to load share link bundles.') + expect(toastService.showError).toHaveBeenCalled() + expect(component.loading).toBe(false) + + tick(5000) + expect(service.listAllBundles).toHaveBeenCalledTimes(2) + })) + + it('copies bundle links when ready', fakeAsync(() => { + jest.spyOn(clipboard, 'copy').mockReturnValue(true) + fixture.detectChanges() + tick() + + const readyBundle = sampleBundle({ + slug: 'ready-slug', + status: ShareLinkBundleStatus.Ready, + }) + component.copy(readyBundle) + + expect(clipboard.copy).toHaveBeenCalledWith( + component.getShareUrl(readyBundle) + ) + expect(component.copiedSlug).toBe('ready-slug') + expect(toastService.showInfo).toHaveBeenCalled() + + tick(3000) + expect(component.copiedSlug).toBeNull() + })) + + it('ignores copy requests for non-ready bundles', fakeAsync(() => { + const copySpy = jest.spyOn(clipboard, 'copy') + fixture.detectChanges() + tick() + component.copy(sampleBundle({ status: ShareLinkBundleStatus.Pending })) + expect(copySpy).not.toHaveBeenCalled() + })) + + it('deletes bundles and refreshes list', fakeAsync(() => { + service.listAllBundles.mockReturnValue(of([])) + service.delete.mockReturnValue(of(true)) + + fixture.detectChanges() + tick() + + component.delete(sampleBundle()) + tick() + + expect(service.delete).toHaveBeenCalled() + expect(toastService.showInfo).toHaveBeenCalledWith( + expect.stringContaining('deleted.') + ) + expect(service.listAllBundles).toHaveBeenCalledTimes(2) + expect(component.loading).toBe(false) + })) + + it('handles delete errors gracefully', fakeAsync(() => { + service.listAllBundles.mockReturnValue(of([])) + service.delete.mockReturnValue(throwError(() => new Error('delete fail'))) + + fixture.detectChanges() + tick() + + component.delete(sampleBundle()) + tick() + + expect(toastService.showError).toHaveBeenCalled() + expect(component.loading).toBe(false) + })) + + it('retries bundle build and replaces existing entry', fakeAsync(() => { + service.listAllBundles.mockReturnValue(of([])) + const updated = sampleBundle({ status: ShareLinkBundleStatus.Ready }) + service.rebuildBundle.mockReturnValue(of(updated)) + + fixture.detectChanges() + tick() + + component.bundles = [sampleBundle()] + component.retry(component.bundles[0]) + tick() + + expect(service.rebuildBundle).toHaveBeenCalledWith(updated.id) + expect(component.bundles[0].status).toBe(ShareLinkBundleStatus.Ready) + expect(toastService.showInfo).toHaveBeenCalled() + })) + + it('adds new bundle when retry returns unknown entry', fakeAsync(() => { + service.listAllBundles.mockReturnValue(of([])) + service.rebuildBundle.mockReturnValue( + of(sampleBundle({ id: 99, slug: 'new-slug' })) + ) + + fixture.detectChanges() + tick() + + component.bundles = [sampleBundle()] + component.retry({ id: 99 } as ShareLinkBundleSummary) + tick() + + expect(component.bundles.find((bundle) => bundle.id === 99)).toBeTruthy() + })) + + it('handles retry errors', fakeAsync(() => { + service.listAllBundles.mockReturnValue(of([])) + service.rebuildBundle.mockReturnValue(throwError(() => new Error('fail'))) + + fixture.detectChanges() + tick() + + component.retry(sampleBundle()) + tick() + + expect(toastService.showError).toHaveBeenCalled() + })) + + it('maps helpers and closes dialog', fakeAsync(() => { + service.listAllBundles.mockReturnValue(of([])) + fixture.detectChanges() + tick() + + expect(component.statusLabel(ShareLinkBundleStatus.Processing)).toContain( + 'Processing' + ) + expect(component.fileVersionLabel(FileVersion.Original)).toContain( + 'Original' + ) + + environment.apiBaseUrl = 'https://example.com/api/' + const url = component.getShareUrl(sampleBundle({ slug: 'sluggy' })) + expect(url).toBe('https://example.com/share/sluggy') + + const closeSpy = jest.spyOn(activeModal, 'close') + component.close() + expect(closeSpy).toHaveBeenCalled() + })) +}) diff --git a/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.ts b/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.ts new file mode 100644 index 000000000..6eef144f9 --- /dev/null +++ b/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.ts @@ -0,0 +1,177 @@ +import { Clipboard } from '@angular/cdk/clipboard' +import { CommonModule } from '@angular/common' +import { Component, OnDestroy, OnInit, inject } from '@angular/core' +import { NgbActiveModal, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap' +import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' +import { Subject, catchError, of, switchMap, takeUntil, timer } from 'rxjs' +import { FileVersion } from 'src/app/data/share-link' +import { + SHARE_LINK_BUNDLE_FILE_VERSION_LABELS, + SHARE_LINK_BUNDLE_STATUS_LABELS, + ShareLinkBundleStatus, + ShareLinkBundleSummary, +} from 'src/app/data/share-link-bundle' +import { FileSizePipe } from 'src/app/pipes/file-size.pipe' +import { ShareLinkBundleService } from 'src/app/services/rest/share-link-bundle.service' +import { ToastService } from 'src/app/services/toast.service' +import { environment } from 'src/environments/environment' +import { LoadingComponentWithPermissions } from '../../loading-component/loading.component' +import { ConfirmButtonComponent } from '../confirm-button/confirm-button.component' + +@Component({ + selector: 'pngx-share-link-bundle-manage-dialog', + templateUrl: './share-link-bundle-manage-dialog.component.html', + styleUrls: ['./share-link-bundle-manage-dialog.component.scss'], + imports: [ + ConfirmButtonComponent, + CommonModule, + NgbPopoverModule, + NgxBootstrapIconsModule, + FileSizePipe, + ], +}) +export class ShareLinkBundleManageDialogComponent + extends LoadingComponentWithPermissions + implements OnInit, OnDestroy +{ + private readonly activeModal = inject(NgbActiveModal) + private readonly shareLinkBundleService = inject(ShareLinkBundleService) + private readonly toastService = inject(ToastService) + private readonly clipboard = inject(Clipboard) + + title = $localize`Share link bundles` + + bundles: ShareLinkBundleSummary[] = [] + error: string | null = null + copiedSlug: string | null = null + + readonly statuses = ShareLinkBundleStatus + readonly fileVersions = FileVersion + + private readonly refresh$ = new Subject() + + ngOnInit(): void { + this.refresh$ + .pipe( + switchMap((silent) => { + if (!silent) { + this.loading = true + } + this.error = null + return this.shareLinkBundleService.listAllBundles().pipe( + catchError((error) => { + if (!silent) { + this.loading = false + } + this.error = $localize`Failed to load share link bundles.` + this.toastService.showError( + $localize`Error retrieving share link bundles.`, + error + ) + return of(null) + }) + ) + }), + takeUntil(this.unsubscribeNotifier) + ) + .subscribe((results) => { + if (results) { + this.bundles = results + this.copiedSlug = null + } + this.loading = false + }) + + this.triggerRefresh(false) + timer(5000, 5000) + .pipe(takeUntil(this.unsubscribeNotifier)) + .subscribe(() => this.triggerRefresh(true)) + } + + ngOnDestroy(): void { + super.ngOnDestroy() + } + + getShareUrl(bundle: ShareLinkBundleSummary): string { + const apiURL = new URL(environment.apiBaseUrl) + return `${apiURL.origin}${apiURL.pathname.replace(/\/api\/$/, '/share/')}${ + bundle.slug + }` + } + + copy(bundle: ShareLinkBundleSummary): void { + if (bundle.status !== ShareLinkBundleStatus.Ready) { + return + } + const success = this.clipboard.copy(this.getShareUrl(bundle)) + if (success) { + this.copiedSlug = bundle.slug + setTimeout(() => { + this.copiedSlug = null + }, 3000) + this.toastService.showInfo($localize`Share link copied to clipboard.`) + } + } + + delete(bundle: ShareLinkBundleSummary): void { + this.error = null + this.loading = true + this.shareLinkBundleService.delete(bundle).subscribe({ + next: () => { + this.toastService.showInfo($localize`Share link bundle deleted.`) + this.triggerRefresh(false) + }, + error: (e) => { + this.loading = false + this.toastService.showError( + $localize`Error deleting share link bundle.`, + e + ) + }, + }) + } + + retry(bundle: ShareLinkBundleSummary): void { + this.error = null + this.shareLinkBundleService.rebuildBundle(bundle.id).subscribe({ + next: (updated) => { + this.toastService.showInfo( + $localize`Share link bundle rebuild requested.` + ) + this.replaceBundle(updated) + }, + error: (e) => { + this.toastService.showError($localize`Error requesting rebuild.`, e) + }, + }) + } + + statusLabel(status: ShareLinkBundleStatus): string { + return SHARE_LINK_BUNDLE_STATUS_LABELS[status] ?? status + } + + fileVersionLabel(version: FileVersion): string { + return SHARE_LINK_BUNDLE_FILE_VERSION_LABELS[version] ?? version + } + + close(): void { + this.activeModal.close() + } + + private replaceBundle(updated: ShareLinkBundleSummary): void { + const index = this.bundles.findIndex((bundle) => bundle.id === updated.id) + if (index >= 0) { + this.bundles = [ + ...this.bundles.slice(0, index), + updated, + ...this.bundles.slice(index + 1), + ] + } else { + this.bundles = [updated, ...this.bundles] + } + } + + private triggerRefresh(silent: boolean): void { + this.refresh$.next(silent) + } +} diff --git a/src-ui/src/app/components/common/share-links-dialog/share-links-dialog.component.html b/src-ui/src/app/components/common/share-links-dialog/share-links-dialog.component.html index fe3f9b9c3..e41a897a8 100644 --- a/src-ui/src/app/components/common/share-links-dialog/share-links-dialog.component.html +++ b/src-ui/src/app/components/common/share-links-dialog/share-links-dialog.component.html @@ -51,7 +51,7 @@
      diff --git a/src-ui/src/app/components/common/share-links-dialog/share-links-dialog.component.ts b/src-ui/src/app/components/common/share-links-dialog/share-links-dialog.component.ts index ffe11808c..9df3d438b 100644 --- a/src-ui/src/app/components/common/share-links-dialog/share-links-dialog.component.ts +++ b/src-ui/src/app/components/common/share-links-dialog/share-links-dialog.component.ts @@ -4,7 +4,11 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { first } from 'rxjs' -import { FileVersion, ShareLink } from 'src/app/data/share-link' +import { + FileVersion, + SHARE_LINK_EXPIRATION_OPTIONS, + ShareLink, +} from 'src/app/data/share-link' import { ShareLinkService } from 'src/app/services/rest/share-link.service' import { ToastService } from 'src/app/services/toast.service' import { environment } from 'src/environments/environment' @@ -21,12 +25,7 @@ export class ShareLinksDialogComponent implements OnInit { private toastService = inject(ToastService) private clipboard = inject(Clipboard) - EXPIRATION_OPTIONS = [ - { label: $localize`1 day`, value: 1 }, - { label: $localize`7 days`, value: 7 }, - { label: $localize`30 days`, value: 30 }, - { label: $localize`Never`, value: null }, - ] + readonly expirationOptions = SHARE_LINK_EXPIRATION_OPTIONS @Input() title = $localize`Share Links` diff --git a/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.html b/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.html new file mode 100644 index 000000000..af207c05c --- /dev/null +++ b/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.html @@ -0,0 +1,49 @@ +
      + + + @if (aiEnabled) { +
      + + +
      +
      + @if (!suggestions?.suggested_tags && !suggestions?.suggested_document_types && !suggestions?.suggested_correspondents) { +
      + No novel suggestions +
      + } + @if (suggestions?.suggested_tags.length > 0) { + Tags + @for (tag of suggestions.suggested_tags; track tag) { + + } + } + @if (suggestions?.suggested_document_types.length > 0) { +
      Document Types
      + @for (type of suggestions.suggested_document_types; track type) { + + } + } + @if (suggestions?.suggested_correspondents.length > 0) { +
      Correspondents
      + @for (correspondent of suggestions.suggested_correspondents; track correspondent) { + + } + } +
      +
      +
      + } +
      diff --git a/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.scss b/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.scss new file mode 100644 index 000000000..19aa1dc7d --- /dev/null +++ b/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.scss @@ -0,0 +1,3 @@ +.suggestions-dropdown { + min-width: 250px; +} diff --git a/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.spec.ts b/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.spec.ts new file mode 100644 index 000000000..801a56af3 --- /dev/null +++ b/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.spec.ts @@ -0,0 +1,51 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap' +import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' +import { SuggestionsDropdownComponent } from './suggestions-dropdown.component' + +describe('SuggestionsDropdownComponent', () => { + let component: SuggestionsDropdownComponent + let fixture: ComponentFixture + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + NgbDropdownModule, + NgxBootstrapIconsModule.pick(allIcons), + SuggestionsDropdownComponent, + ], + providers: [], + }) + fixture = TestBed.createComponent(SuggestionsDropdownComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should calculate totalSuggestions', () => { + component.suggestions = { + suggested_correspondents: ['John Doe'], + suggested_tags: ['Tag1', 'Tag2'], + suggested_document_types: ['Type1'], + } + expect(component.totalSuggestions).toBe(4) + }) + + it('should emit getSuggestions when clickSuggest is called and suggestions are null', () => { + jest.spyOn(component.getSuggestions, 'emit') + component.suggestions = null + component.clickSuggest() + expect(component.getSuggestions.emit).toHaveBeenCalled() + }) + + it('should toggle dropdown when clickSuggest is called and suggestions are not null', () => { + component.aiEnabled = true + fixture.detectChanges() + component.suggestions = { + suggested_correspondents: [], + suggested_tags: [], + suggested_document_types: [], + } + component.clickSuggest() + expect(component.dropdown.open).toBeTruthy() + }) +}) diff --git a/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.ts b/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.ts new file mode 100644 index 000000000..b165f0a5e --- /dev/null +++ b/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.ts @@ -0,0 +1,64 @@ +import { + Component, + EventEmitter, + Input, + Output, + ViewChild, +} from '@angular/core' +import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap' +import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' +import { DocumentSuggestions } from 'src/app/data/document-suggestions' +import { pngxPopperOptions } from 'src/app/utils/popper-options' + +@Component({ + selector: 'pngx-suggestions-dropdown', + imports: [NgbDropdownModule, NgxBootstrapIconsModule], + templateUrl: './suggestions-dropdown.component.html', + styleUrl: './suggestions-dropdown.component.scss', +}) +export class SuggestionsDropdownComponent { + public popperOptions = pngxPopperOptions + + @ViewChild('dropdown') dropdown: NgbDropdown + + @Input() + suggestions: DocumentSuggestions = null + + @Input() + aiEnabled: boolean = false + + @Input() + loading: boolean = false + + @Input() + disabled: boolean = false + + @Output() + getSuggestions: EventEmitter = + new EventEmitter() + + @Output() + addTag: EventEmitter = new EventEmitter() + + @Output() + addDocumentType: EventEmitter = new EventEmitter() + + @Output() + addCorrespondent: EventEmitter = new EventEmitter() + + public clickSuggest(): void { + if (!this.suggestions) { + this.getSuggestions.emit(this) + } else { + this.dropdown?.toggle() + } + } + + get totalSuggestions(): number { + return ( + this.suggestions?.suggested_correspondents?.length + + this.suggestions?.suggested_tags?.length + + this.suggestions?.suggested_document_types?.length || 0 + ) + } +} diff --git a/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.html b/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.html index 99fddbf2c..c34f984b2 100644 --- a/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.html +++ b/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.html @@ -266,6 +266,43 @@ } + @if (aiEnabled) { +
      AI Index
      +
      + + @if (currentUserIsSuperUser) { + @if (isRunning(PaperlessTaskName.LLMIndexUpdate)) { +
      + } @else { + + } + } +
      + + @if (status.tasks.llmindex_status === 'OK') { +
      Last Run:
      {{status.tasks.llmindex_last_modified | customDate:'medium'}} + } @else { +
      Error:
      {{status.tasks.llmindex_error}} + } +
      + }
      diff --git a/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.spec.ts b/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.spec.ts index 1785459f4..0fd331b10 100644 --- a/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.spec.ts +++ b/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.spec.ts @@ -68,6 +68,9 @@ const status: SystemStatus = { sanity_check_status: SystemStatusItemStatus.OK, sanity_check_last_run: new Date().toISOString(), sanity_check_error: null, + llmindex_status: SystemStatusItemStatus.OK, + llmindex_last_modified: new Date().toISOString(), + llmindex_error: null, }, } diff --git a/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.ts b/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.ts index f88d56ff6..d53bb74bf 100644 --- a/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.ts +++ b/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.ts @@ -13,9 +13,11 @@ import { SystemStatus, SystemStatusItemStatus, } from 'src/app/data/system-status' +import { SETTINGS_KEYS } from 'src/app/data/ui-settings' import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' import { FileSizePipe } from 'src/app/pipes/file-size.pipe' import { PermissionsService } from 'src/app/services/permissions.service' +import { SettingsService } from 'src/app/services/settings.service' import { SystemStatusService } from 'src/app/services/system-status.service' import { TasksService } from 'src/app/services/tasks.service' import { ToastService } from 'src/app/services/toast.service' @@ -44,6 +46,7 @@ export class SystemStatusDialogComponent implements OnInit, OnDestroy { private toastService = inject(ToastService) private permissionsService = inject(PermissionsService) private websocketStatusService = inject(WebsocketStatusService) + private settingsService = inject(SettingsService) public SystemStatusItemStatus = SystemStatusItemStatus public PaperlessTaskName = PaperlessTaskName @@ -60,6 +63,10 @@ export class SystemStatusDialogComponent implements OnInit, OnDestroy { return this.permissionsService.isSuperUser() } + get aiEnabled(): boolean { + return this.settingsService.get(SETTINGS_KEYS.AI_ENABLED) + } + public ngOnInit() { this.versionMismatch = environment.production && diff --git a/src-ui/src/app/components/document-detail/document-detail.component.html b/src-ui/src/app/components/document-detail/document-detail.component.html index f8a942ba3..306152cc4 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.html +++ b/src-ui/src/app/components/document-detail/document-detail.component.html @@ -1,4 +1,4 @@ - + @if (archiveContentRenderType === ContentRenderType.PDF && !useNativePdfViewer) { @if (previewNumPages) {
      @@ -74,16 +74,6 @@
      - - - -
      + +
      + + +
      + +
      + + +
      +
      +
      @@ -129,17 +145,27 @@ Details
      - - + + @if (!isFieldHidden(DocumentDetailFieldID.ArchiveSerialNumber)) { + + } - - - - + @if (!isFieldHidden(DocumentDetailFieldID.Correspondent)) { + + } + @if (!isFieldHidden(DocumentDetailFieldID.DocumentType)) { + + } + @if (!isFieldHidden(DocumentDetailFieldID.StoragePath)) { + + } + @if (!isFieldHidden(DocumentDetailFieldID.Tags)) { + + } @for (fieldInstance of document?.custom_fields; track fieldInstance.field; let i = $index) {
      @switch (getCustomFieldFromInstance(fieldInstance)?.data_type) { @@ -354,6 +380,37 @@ } + + @if (document?.duplicate_documents?.length) { +
    • + + Duplicates + {{ document.duplicate_documents.length }} + + +
      +
      Duplicate documents detected:
      + +
      +
      +
    • + }
    @@ -361,14 +418,14 @@ -
    +
    -
    +
    @if (hasNext()) { diff --git a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts index b1b3650c6..7be9d9151 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts @@ -48,6 +48,7 @@ import { } from 'src/app/data/filter-rule-type' import { StoragePath } from 'src/app/data/storage-path' import { Tag } from 'src/app/data/tag' +import { SETTINGS_KEYS } from 'src/app/data/ui-settings' import { PermissionsGuard } from 'src/app/guards/permissions.guard' import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe' @@ -68,10 +69,8 @@ import { environment } from 'src/environments/environment' import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component' import { PasswordRemovalConfirmDialogComponent } from '../common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component' import { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/custom-fields-dropdown.component' -import { - DocumentDetailComponent, - ZoomSetting, -} from './document-detail.component' +import { DocumentDetailComponent } from './document-detail.component' +import { ZoomSetting } from './zoom-setting' const doc: Document = { id: 3, @@ -157,6 +156,16 @@ describe('DocumentDetailComponent', () => { { provide: TagService, useValue: { + getCachedMany: (ids: number[]) => + of( + ids.map((id) => ({ + id, + name: `Tag${id}`, + is_inbox_tag: true, + color: '#ff0000', + text_color: '#000000', + })) + ), listAll: () => of({ count: 3, @@ -291,16 +300,16 @@ describe('DocumentDetailComponent', () => { .spyOn(openDocumentsService, 'openDocument') .mockReturnValueOnce(of(true)) fixture.detectChanges() - expect(component.activeNavID).toEqual(5) // DocumentDetailNavIDs.Notes + expect(component.activeNavID).toEqual(component.DocumentDetailNavIDs.Notes) }) it('should change url on tab switch', () => { initNormally() const navigateSpy = jest.spyOn(router, 'navigate') - component.nav.select(5) + component.nav.select(component.DocumentDetailNavIDs.Notes) component.nav.navChange.next({ activeId: 1, - nextId: 5, + nextId: component.DocumentDetailNavIDs.Notes, preventDefault: () => {}, }) fixture.detectChanges() @@ -342,6 +351,18 @@ describe('DocumentDetailComponent', () => { expect(component.document).toEqual(doc) }) + it('should fall back to details tab when duplicates tab is active but no duplicates', () => { + initNormally() + component.activeNavID = component.DocumentDetailNavIDs.Duplicates + const noDupDoc = { ...doc, duplicate_documents: [] } + + component.updateComponent(noDupDoc) + + expect(component.activeNavID).toEqual( + component.DocumentDetailNavIDs.Details + ) + }) + it('should load already-opened document via param', () => { initNormally() jest.spyOn(documentService, 'get').mockReturnValueOnce(of(doc)) @@ -357,6 +378,38 @@ describe('DocumentDetailComponent', () => { expect(component.document).toEqual(doc) }) + it('should update cached open document duplicates when reloading an open doc', () => { + const openDoc = { ...doc, duplicate_documents: [{ id: 1, title: 'Old' }] } + const updatedDuplicates = [ + { id: 2, title: 'Newer duplicate', deleted_at: null }, + ] + jest + .spyOn(activatedRoute, 'paramMap', 'get') + .mockReturnValue(of(convertToParamMap({ id: 3, section: 'details' }))) + jest.spyOn(documentService, 'get').mockReturnValue( + of({ + ...doc, + modified: new Date('2024-01-02T00:00:00Z'), + duplicate_documents: updatedDuplicates, + }) + ) + jest.spyOn(openDocumentsService, 'getOpenDocument').mockReturnValue(openDoc) + const saveSpy = jest.spyOn(openDocumentsService, 'save') + jest.spyOn(openDocumentsService, 'openDocument').mockReturnValue(of(true)) + jest.spyOn(customFieldsService, 'listAll').mockReturnValue( + of({ + count: customFields.length, + all: customFields.map((f) => f.id), + results: customFields, + }) + ) + + fixture.detectChanges() + + expect(openDoc.duplicate_documents).toEqual(updatedDuplicates) + expect(saveSpy).toHaveBeenCalled() + }) + it('should disable form if user cannot edit', () => { currentUserHasObjectPermissions = false initNormally() @@ -383,8 +436,32 @@ describe('DocumentDetailComponent', () => { currentUserCan = true }) - it('should support creating document type', () => { + it('should support creating tag, remove from suggestions', () => { initNormally() + component.suggestions = { + suggested_tags: ['Tag1', 'NewTag12'], + } + let openModal: NgbModalRef + modalService.activeInstances.subscribe((modal) => (openModal = modal[0])) + const modalSpy = jest.spyOn(modalService, 'open') + component.createTag('NewTag12') + expect(modalSpy).toHaveBeenCalled() + openModal.componentInstance.succeeded.next({ + id: 12, + name: 'NewTag12', + is_inbox_tag: true, + color: '#ff0000', + text_color: '#000000', + }) + expect(component.tagsInput.value).toContain(12) + expect(component.suggestions.suggested_tags).not.toContain('NewTag12') + }) + + it('should support creating document type, remove from suggestions', () => { + initNormally() + component.suggestions = { + suggested_document_types: ['DocumentType1', 'NewDocType2'], + } let openModal: NgbModalRef modalService.activeInstances.subscribe((modal) => (openModal = modal[0])) const modalSpy = jest.spyOn(modalService, 'open') @@ -392,10 +469,16 @@ describe('DocumentDetailComponent', () => { expect(modalSpy).toHaveBeenCalled() openModal.componentInstance.succeeded.next({ id: 12, name: 'NewDocType12' }) expect(component.documentForm.get('document_type').value).toEqual(12) + expect(component.suggestions.suggested_document_types).not.toContain( + 'NewDocType2' + ) }) - it('should support creating correspondent', () => { + it('should support creating correspondent, remove from suggestions', () => { initNormally() + component.suggestions = { + suggested_correspondents: ['Correspondent1', 'NewCorrrespondent12'], + } let openModal: NgbModalRef modalService.activeInstances.subscribe((modal) => (openModal = modal[0])) const modalSpy = jest.spyOn(modalService, 'open') @@ -406,6 +489,9 @@ describe('DocumentDetailComponent', () => { name: 'NewCorrrespondent12', }) expect(component.documentForm.get('correspondent').value).toEqual(12) + expect(component.suggestions.suggested_correspondents).not.toContain( + 'NewCorrrespondent12' + ) }) it('should support creating storage path', () => { @@ -928,7 +1014,7 @@ describe('DocumentDetailComponent', () => { it('should display built-in pdf viewer if not disabled', () => { initNormally() component.document.archived_file_name = 'file.pdf' - jest.spyOn(settingsService, 'get').mockReturnValue(false) + settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, false) expect(component.useNativePdfViewer).toBeFalsy() fixture.detectChanges() expect(fixture.debugElement.query(By.css('pdf-viewer'))).not.toBeNull() @@ -937,7 +1023,7 @@ describe('DocumentDetailComponent', () => { it('should display native pdf viewer if enabled', () => { initNormally() component.document.archived_file_name = 'file.pdf' - jest.spyOn(settingsService, 'get').mockReturnValue(true) + settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, true) expect(component.useNativePdfViewer).toBeTruthy() fixture.detectChanges() expect(fixture.debugElement.query(By.css('object'))).not.toBeNull() @@ -996,7 +1082,7 @@ describe('DocumentDetailComponent', () => { expect(component.document.custom_fields).toHaveLength(initialLength - 1) expect(component.customFieldFormFields).toHaveLength(initialLength - 1) expect( - fixture.debugElement.query(By.css('form')).nativeElement.textContent + fixture.debugElement.query(By.css('form ul')).nativeElement.textContent ).not.toContain('Field 1') const patchSpy = jest.spyOn(documentService, 'patch') component.save(true) @@ -1087,10 +1173,22 @@ describe('DocumentDetailComponent', () => { it('should get suggestions', () => { const suggestionsSpy = jest.spyOn(documentService, 'getSuggestions') - suggestionsSpy.mockReturnValue(of({ tags: [42, 43] })) + suggestionsSpy.mockReturnValue( + of({ + tags: [42, 43], + suggested_tags: [], + suggested_document_types: [], + suggested_correspondents: [], + }) + ) initNormally() expect(suggestionsSpy).toHaveBeenCalled() - expect(component.suggestions).toEqual({ tags: [42, 43] }) + expect(component.suggestions).toEqual({ + tags: [42, 43], + suggested_tags: [], + suggested_document_types: [], + suggested_correspondents: [], + }) }) it('should show error if needed for get suggestions', () => { diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts index ce688f4ad..016a94d71 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.ts @@ -8,7 +8,7 @@ import { FormsModule, ReactiveFormsModule, } from '@angular/forms' -import { ActivatedRoute, Router } from '@angular/router' +import { ActivatedRoute, Router, RouterModule } from '@angular/router' import { NgbDateStruct, NgbDropdownModule, @@ -31,6 +31,7 @@ import { map, switchMap, takeUntil, + tap, } from 'rxjs/operators' import { Correspondent } from 'src/app/data/correspondent' import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field' @@ -76,12 +77,14 @@ import { DocumentTypeService } from 'src/app/services/rest/document-type.service import { DocumentService } from 'src/app/services/rest/document.service' import { SavedViewService } from 'src/app/services/rest/saved-view.service' import { StoragePathService } from 'src/app/services/rest/storage-path.service' +import { TagService } from 'src/app/services/rest/tag.service' import { UserService } from 'src/app/services/rest/user.service' import { SettingsService } from 'src/app/services/settings.service' import { ToastService } from 'src/app/services/toast.service' import { getFilenameFromContentDisposition } from 'src/app/utils/http' import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter' import * as UTIF from 'utif' +import { DocumentDetailFieldID } from '../admin/settings/settings.component' import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component' import { PasswordRemovalConfirmDialogComponent } from '../common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component' import { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/custom-fields-dropdown.component' @@ -89,6 +92,7 @@ import { CorrespondentEditDialogComponent } from '../common/edit-dialog/correspo import { DocumentTypeEditDialogComponent } from '../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component' import { EditDialogMode } from '../common/edit-dialog/edit-dialog.component' import { StoragePathEditDialogComponent } from '../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component' +import { TagEditDialogComponent } from '../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component' import { EmailDocumentDialogComponent } from '../common/email-document-dialog/email-document-dialog.component' import { CheckComponent } from '../common/input/check/check.component' import { DateComponent } from '../common/input/date/date.component' @@ -102,15 +106,15 @@ import { TextComponent } from '../common/input/text/text.component' import { TextAreaComponent } from '../common/input/textarea/textarea.component' import { UrlComponent } from '../common/input/url/url.component' import { PageHeaderComponent } from '../common/page-header/page-header.component' -import { - PDFEditorComponent, - PdfEditorEditMode, -} from '../common/pdf-editor/pdf-editor.component' +import { PdfEditorEditMode } from '../common/pdf-editor/pdf-editor-edit-mode' +import { PDFEditorComponent } from '../common/pdf-editor/pdf-editor.component' import { ShareLinksDialogComponent } from '../common/share-links-dialog/share-links-dialog.component' +import { SuggestionsDropdownComponent } from '../common/suggestions-dropdown/suggestions-dropdown.component' import { DocumentHistoryComponent } from '../document-history/document-history.component' import { DocumentNotesComponent } from '../document-notes/document-notes.component' import { ComponentWithPermissions } from '../with-permissions/with-permissions.component' import { MetadataCollapseComponent } from './metadata-collapse/metadata-collapse.component' +import { ZoomSetting } from './zoom-setting' enum DocumentDetailNavIDs { Details = 1, @@ -120,6 +124,7 @@ enum DocumentDetailNavIDs { Notes = 5, Permissions = 6, History = 7, + Duplicates = 8, } enum ContentRenderType { @@ -131,18 +136,6 @@ enum ContentRenderType { TIFF = 'tiff', } -export enum ZoomSetting { - PageFit = 'page-fit', - PageWidth = 'page-width', - Quarter = '.25', - Half = '.5', - ThreeQuarters = '.75', - One = '1', - OneAndHalf = '1.5', - Two = '2', - Three = '3', -} - @Component({ selector: 'pngx-document-detail', templateUrl: './document-detail.component.html', @@ -163,6 +156,7 @@ export enum ZoomSetting { NumberComponent, MonetaryComponent, UrlComponent, + SuggestionsDropdownComponent, CustomDatePipe, FileSizePipe, IfPermissionsDirective, @@ -176,6 +170,7 @@ export enum ZoomSetting { NgxBootstrapIconsModule, PdfViewerModule, TextAreaComponent, + RouterModule, ], }) export class DocumentDetailComponent @@ -184,6 +179,7 @@ export class DocumentDetailComponent { private documentsService = inject(DocumentService) private route = inject(ActivatedRoute) + private tagService = inject(TagService) private correspondentService = inject(CorrespondentService) private documentTypeService = inject(DocumentTypeService) private router = inject(Router) @@ -206,6 +202,8 @@ export class DocumentDetailComponent @ViewChild('inputTitle') titleInput: TextComponent + @ViewChild('tagsInput') tagsInput: TagsComponent + expandOriginalMetadata = false expandArchivedMetadata = false @@ -217,6 +215,7 @@ export class DocumentDetailComponent document: Document metadata: DocumentMetadata suggestions: DocumentSuggestions + suggestionsLoading: boolean = false users: User[] title: string @@ -270,16 +269,18 @@ export class DocumentDetailComponent public readonly DataType = DataType + public readonly DocumentDetailFieldID = DocumentDetailFieldID + @ViewChild('nav') nav: NgbNav @ViewChild('pdfPreview') set pdfPreview(element) { // this gets called when component added or removed from DOM if ( element && element.nativeElement.offsetParent !== null && - this.nav?.activeId == 4 + this.nav?.activeId == DocumentDetailNavIDs.Preview ) { // its visible - setTimeout(() => this.nav?.select(1)) + setTimeout(() => this.nav?.select(DocumentDetailNavIDs.Details)) } } @@ -298,6 +299,10 @@ export class DocumentDetailComponent return this.deviceDetectorService.isMobile() } + get aiEnabled(): boolean { + return this.settings.get(SETTINGS_KEYS.AI_ENABLED) + } + get archiveContentRenderType(): ContentRenderType { return this.document?.archived_file_name ? this.getRenderType('application/pdf') @@ -312,6 +317,12 @@ export class DocumentDetailComponent return this.settings.get(SETTINGS_KEYS.DOCUMENT_EDITING_OVERLAY_THUMBNAIL) } + isFieldHidden(fieldId: DocumentDetailFieldID): boolean { + return this.settings + .get(SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS) + .includes(fieldId) + } + private getRenderType(mimeType: string): ContentRenderType { if (!mimeType) return ContentRenderType.Unknown if (mimeType === 'application/pdf') { @@ -441,6 +452,11 @@ export class DocumentDetailComponent const openDocument = this.openDocumentService.getOpenDocument( this.documentId ) + // update duplicate documents if present + if (openDocument && doc?.duplicate_documents) { + openDocument.duplicate_documents = doc.duplicate_documents + this.openDocumentService.save() + } const useDoc = openDocument || doc if (openDocument) { if ( @@ -682,34 +698,85 @@ export class DocumentDetailComponent PermissionType.Document ) ) { - this.documentsService - .getSuggestions(doc.id) - .pipe( - first(), - takeUntil(this.unsubscribeNotifier), - takeUntil(this.docChangeNotifier) - ) - .subscribe({ - next: (result) => { - this.suggestions = result - }, - error: (error) => { - this.suggestions = null - this.toastService.showError( - $localize`Error retrieving suggestions.`, - error - ) - }, - }) + this.tagService.getCachedMany(doc.tags).subscribe((tags) => { + // only show suggestions if document has inbox tags + if (tags.some((tag) => tag.is_inbox_tag)) { + this.getSuggestions() + } + }) } this.title = this.documentTitlePipe.transform(doc.title) this.prepareForm(doc) + + if ( + this.activeNavID === DocumentDetailNavIDs.Duplicates && + !doc?.duplicate_documents?.length + ) { + this.activeNavID = DocumentDetailNavIDs.Details + } } get customFieldFormFields(): FormArray { return this.documentForm.get('custom_fields') as FormArray } + getSuggestions() { + this.suggestionsLoading = true + this.documentsService + .getSuggestions(this.documentId) + .pipe( + first(), + takeUntil(this.unsubscribeNotifier), + takeUntil(this.docChangeNotifier) + ) + .subscribe({ + next: (result) => { + this.suggestions = result + this.suggestionsLoading = false + }, + error: (error) => { + this.suggestions = null + this.suggestionsLoading = false + this.toastService.showError( + $localize`Error retrieving suggestions.`, + error + ) + }, + }) + } + + createTag(newName: string) { + var modal = this.modalService.open(TagEditDialogComponent, { + backdrop: 'static', + }) + modal.componentInstance.dialogMode = EditDialogMode.CREATE + if (newName) modal.componentInstance.object = { name: newName } + modal.componentInstance.succeeded + .pipe( + tap((newTag: Tag) => { + // remove from suggestions if present + if (this.suggestions) { + this.suggestions = { + ...this.suggestions, + suggested_tags: this.suggestions.suggested_tags.filter( + (tag) => tag !== newTag.name + ), + } + } + }), + switchMap((newTag: Tag) => { + return this.tagService + .listAll() + .pipe(map((tags) => ({ newTag, tags }))) + }), + takeUntil(this.unsubscribeNotifier) + ) + .subscribe(({ newTag, tags }) => { + this.tagsInput.tags = tags.results + this.tagsInput.addTag(newTag.id) + }) + } + createDocumentType(newName: string) { var modal = this.modalService.open(DocumentTypeEditDialogComponent, { backdrop: 'static', @@ -729,6 +796,12 @@ export class DocumentDetailComponent this.documentTypes = documentTypes.results this.documentForm.get('document_type').setValue(newDocumentType.id) this.documentForm.get('document_type').markAsDirty() + if (this.suggestions) { + this.suggestions.suggested_document_types = + this.suggestions.suggested_document_types.filter( + (dt) => dt !== newName + ) + } }) } @@ -753,6 +826,12 @@ export class DocumentDetailComponent this.correspondents = correspondents.results this.documentForm.get('correspondent').setValue(newCorrespondent.id) this.documentForm.get('correspondent').markAsDirty() + if (this.suggestions) { + this.suggestions.suggested_correspondents = + this.suggestions.suggested_correspondents.filter( + (c) => c !== newName + ) + } }) } diff --git a/src-ui/src/app/components/document-detail/zoom-setting.ts b/src-ui/src/app/components/document-detail/zoom-setting.ts new file mode 100644 index 000000000..27d4f1677 --- /dev/null +++ b/src-ui/src/app/components/document-detail/zoom-setting.ts @@ -0,0 +1,11 @@ +export enum ZoomSetting { + PageFit = 'page-fit', + PageWidth = 'page-width', + Quarter = '.25', + Half = '.5', + ThreeQuarters = '.75', + One = '1', + OneAndHalf = '1.5', + Two = '2', + Three = '3', +} diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html index 2323929d1..6f3a84eee 100644 --- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html +++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html @@ -96,14 +96,36 @@ - @if (emailEnabled) { - - }
    +
    + +
    + + + + @if (emailEnabled) { + + } +
    +
    - - - +
    + + + +
    +
    + +
    +
    + Select: +
    +
    + @if (selectedObjects.size > 0) { + + } + + +
    +
    + + + +
    @@ -31,7 +62,7 @@
    - +
    diff --git a/src-ui/src/app/components/manage/management-list/management-list.component.spec.ts b/src-ui/src/app/components/manage/management-list/management-list.component.spec.ts index a9f7a0626..dca1bb2c9 100644 --- a/src-ui/src/app/components/manage/management-list/management-list.component.spec.ts +++ b/src-ui/src/app/components/manage/management-list/management-list.component.spec.ts @@ -163,8 +163,7 @@ describe('ManagementListComponent', () => { const toastInfoSpy = jest.spyOn(toastService, 'showInfo') const reloadSpy = jest.spyOn(component, 'reloadData') - const createButton = fixture.debugElement.queryAll(By.css('button'))[4] - createButton.triggerEventHandler('click') + component.openCreateDialog() expect(modal).not.toBeUndefined() const editDialog = modal.componentInstance as EditDialogComponent @@ -187,8 +186,7 @@ describe('ManagementListComponent', () => { const toastInfoSpy = jest.spyOn(toastService, 'showInfo') const reloadSpy = jest.spyOn(component, 'reloadData') - const editButton = fixture.debugElement.queryAll(By.css('button'))[7] - editButton.triggerEventHandler('click') + component.openEditDialog(tags[0]) expect(modal).not.toBeUndefined() const editDialog = modal.componentInstance as EditDialogComponent @@ -212,8 +210,7 @@ describe('ManagementListComponent', () => { const deleteSpy = jest.spyOn(tagService, 'delete') const reloadSpy = jest.spyOn(component, 'reloadData') - const deleteButton = fixture.debugElement.queryAll(By.css('button'))[8] - deleteButton.triggerEventHandler('click') + component.openDeleteDialog(tags[0]) expect(modal).not.toBeUndefined() const editDialog = modal.componentInstance as ConfirmDialogComponent @@ -230,6 +227,21 @@ describe('ManagementListComponent', () => { expect(reloadSpy).toHaveBeenCalled() }) + it('should use the all list length for collection size when provided', fakeAsync(() => { + jest.spyOn(tagService, 'listFiltered').mockReturnValueOnce( + of({ + count: 1, + all: [1, 2, 3], + results: tags.slice(0, 1), + }) + ) + + component.reloadData() + tick(100) + + expect(component.collectionSize).toBe(3) + })) + it('should support quick filter for objects', () => { const expectedUrl = documentListViewService.getQuickFilterUrl([ { rule_type: FILTER_HAS_TAGS_ALL, value: tags[0].id.toString() }, @@ -264,19 +276,84 @@ describe('ManagementListComponent', () => { expect(component.page).toEqual(1) }) - it('should support toggle all items in view', () => { + it('should support toggle select page in vew', () => { expect(component.selectedObjects.size).toEqual(0) - const toggleAllSpy = jest.spyOn(component, 'toggleAll') + const selectPageSpy = jest.spyOn(component, 'selectPage') const checkButton = fixture.debugElement.queryAll( By.css('input.form-check-input') )[0] - checkButton.nativeElement.dispatchEvent(new Event('click')) + checkButton.nativeElement.dispatchEvent(new Event('change')) checkButton.nativeElement.checked = true - checkButton.nativeElement.dispatchEvent(new Event('click')) - expect(toggleAllSpy).toHaveBeenCalled() + checkButton.nativeElement.dispatchEvent(new Event('change')) + expect(selectPageSpy).toHaveBeenCalled() expect(component.selectedObjects.size).toEqual(tags.length) }) + it('selectNone should clear selection and reset toggle flag', () => { + component.selectedObjects = new Set([tags[0].id, tags[1].id]) + component.togggleAll = true + + component.selectNone() + + expect(component.selectedObjects.size).toBe(0) + expect(component.togggleAll).toBe(false) + }) + + it('selectPage should select current page items or clear selection', () => { + component.selectPage(true) + expect(component.selectedObjects).toEqual(new Set(tags.map((t) => t.id))) + expect(component.togggleAll).toBe(true) + + component.togggleAll = true + component.selectPage(false) + expect(component.selectedObjects.size).toBe(0) + expect(component.togggleAll).toBe(false) + }) + + it('selectAll should use all IDs when collection size exists', () => { + ;(component as any).allIDs = [1, 2, 3, 4] + component.collectionSize = 4 + + component.selectAll() + + expect(component.selectedObjects).toEqual(new Set([1, 2, 3, 4])) + expect(component.togggleAll).toBe(true) + }) + + it('selectAll should clear selection when collection size is zero', () => { + component.selectedObjects = new Set([1]) + component.collectionSize = 0 + component.togggleAll = true + + component.selectAll() + + expect(component.selectedObjects.size).toBe(0) + expect(component.togggleAll).toBe(false) + }) + + it('toggleSelected should toggle object selection and update toggle state', () => { + component.toggleSelected(tags[0]) + expect(component.selectedObjects.has(tags[0].id)).toBe(true) + expect(component.togggleAll).toBe(false) + + component.toggleSelected(tags[1]) + component.toggleSelected(tags[2]) + expect(component.togggleAll).toBe(true) + + component.toggleSelected(tags[1]) + expect(component.selectedObjects.has(tags[1].id)).toBe(false) + expect(component.togggleAll).toBe(false) + }) + + it('areAllPageItemsSelected should return false when page has no selectable items', () => { + component.data = [] + component.selectedObjects.clear() + + expect((component as any).areAllPageItemsSelected()).toBe(false) + + component.data = tags + }) + it('should support bulk edit permissions', () => { const bulkEditPermsSpy = jest.spyOn(tagService, 'bulk_edit_objects') component.toggleSelected(tags[0]) diff --git a/src-ui/src/app/components/manage/management-list/management-list.component.ts b/src-ui/src/app/components/manage/management-list/management-list.component.ts index e8e7a3bb3..daa6a0ea0 100644 --- a/src-ui/src/app/components/manage/management-list/management-list.component.ts +++ b/src-ui/src/app/components/manage/management-list/management-list.component.ts @@ -84,6 +84,7 @@ export abstract class ManagementListComponent public data: T[] = [] private unfilteredData: T[] = [] + private allIDs: number[] = [] public page = 1 @@ -171,7 +172,8 @@ export abstract class ManagementListComponent tap((c) => { this.unfilteredData = c.results this.data = this.filterData(c.results) - this.collectionSize = c.count + this.collectionSize = c.all?.length ?? c.count + this.allIDs = c.all }), delay(100) ) @@ -300,16 +302,6 @@ export abstract class ManagementListComponent return ownsAll } - toggleAll(event: PointerEvent) { - const checked = (event.target as HTMLInputElement).checked - this.togggleAll = checked - if (checked) { - this.selectedObjects = new Set(this.getSelectableIDs(this.data)) - } else { - this.clearSelection() - } - } - protected getSelectableIDs(objects: T[]): number[] { return objects.map((o) => o.id) } @@ -319,10 +311,38 @@ export abstract class ManagementListComponent this.selectedObjects.clear() } + selectNone() { + this.clearSelection() + } + + selectPage(select: boolean) { + if (select) { + this.selectedObjects = new Set(this.getSelectableIDs(this.data)) + this.togggleAll = this.areAllPageItemsSelected() + } else { + this.clearSelection() + } + } + + selectAll() { + if (!this.collectionSize) { + this.clearSelection() + return + } + this.selectedObjects = new Set(this.allIDs) + this.togggleAll = this.areAllPageItemsSelected() + } + toggleSelected(object) { this.selectedObjects.has(object.id) ? this.selectedObjects.delete(object.id) : this.selectedObjects.add(object.id) + this.togggleAll = this.areAllPageItemsSelected() + } + + protected areAllPageItemsSelected(): boolean { + const ids = this.getSelectableIDs(this.data) + return ids.length > 0 && ids.every((id) => this.selectedObjects.has(id)) } setPermissions() { diff --git a/src-ui/src/app/components/manage/storage-path-list/storage-path-list.component.ts b/src-ui/src/app/components/manage/storage-path-list/storage-path-list.component.ts index cac8637d7..3ab940521 100644 --- a/src-ui/src/app/components/manage/storage-path-list/storage-path-list.component.ts +++ b/src-ui/src/app/components/manage/storage-path-list/storage-path-list.component.ts @@ -13,6 +13,7 @@ import { IfPermissionsDirective } from 'src/app/directives/if-permissions.direct import { SortableDirective } from 'src/app/directives/sortable.directive' import { PermissionType } from 'src/app/services/permissions.service' import { StoragePathService } from 'src/app/services/rest/storage-path.service' +import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.component' import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component' import { PageHeaderComponent } from '../../common/page-header/page-header.component' import { ManagementListComponent } from '../management-list/management-list.component' @@ -34,6 +35,7 @@ import { ManagementListComponent } from '../management-list/management-list.comp NgbDropdownModule, NgbPaginationModule, NgxBootstrapIconsModule, + ClearableBadgeComponent, ], }) export class StoragePathListComponent extends ManagementListComponent { diff --git a/src-ui/src/app/components/manage/tag-list/tag-list.component.spec.ts b/src-ui/src/app/components/manage/tag-list/tag-list.component.spec.ts index 9b1923e43..51403379d 100644 --- a/src-ui/src/app/components/manage/tag-list/tag-list.component.spec.ts +++ b/src-ui/src/app/components/manage/tag-list/tag-list.component.spec.ts @@ -138,16 +138,12 @@ describe('TagListComponent', () => { } component.data = [parent as any] - const selectEvent = { target: { checked: true } } as unknown as PointerEvent - component.toggleAll(selectEvent) + component.selectPage(true) expect(component.selectedObjects.has(10)).toBe(true) expect(component.selectedObjects.has(11)).toBe(true) - const deselectEvent = { - target: { checked: false }, - } as unknown as PointerEvent - component.toggleAll(deselectEvent) + component.selectPage(false) expect(component.selectedObjects.size).toBe(0) }) }) diff --git a/src-ui/src/app/components/manage/tag-list/tag-list.component.ts b/src-ui/src/app/components/manage/tag-list/tag-list.component.ts index 544e99b58..87045a50a 100644 --- a/src-ui/src/app/components/manage/tag-list/tag-list.component.ts +++ b/src-ui/src/app/components/manage/tag-list/tag-list.component.ts @@ -13,6 +13,7 @@ import { IfPermissionsDirective } from 'src/app/directives/if-permissions.direct import { SortableDirective } from 'src/app/directives/sortable.directive' import { PermissionType } from 'src/app/services/permissions.service' import { TagService } from 'src/app/services/rest/tag.service' +import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.component' import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component' import { PageHeaderComponent } from '../../common/page-header/page-header.component' import { ManagementListComponent } from '../management-list/management-list.component' @@ -34,6 +35,7 @@ import { ManagementListComponent } from '../management-list/management-list.comp NgbDropdownModule, NgbPaginationModule, NgxBootstrapIconsModule, + ClearableBadgeComponent, ], }) export class TagListComponent extends ManagementListComponent { diff --git a/src-ui/src/app/data/document-suggestions.ts b/src-ui/src/app/data/document-suggestions.ts index 85f9f9d22..447c4402b 100644 --- a/src-ui/src/app/data/document-suggestions.ts +++ b/src-ui/src/app/data/document-suggestions.ts @@ -1,11 +1,17 @@ export interface DocumentSuggestions { + title?: string + tags?: number[] + suggested_tags?: string[] correspondents?: number[] + suggested_correspondents?: string[] document_types?: number[] + suggested_document_types?: string[] storage_paths?: number[] + suggested_storage_paths?: string[] dates?: string[] // ISO-formatted date string e.g. 2022-11-03 } diff --git a/src-ui/src/app/data/document.ts b/src-ui/src/app/data/document.ts index 8aae31945..03d3bf09b 100644 --- a/src-ui/src/app/data/document.ts +++ b/src-ui/src/app/data/document.ts @@ -159,6 +159,8 @@ export interface Document extends ObjectWithPermissions { page_count?: number + duplicate_documents?: Document[] + // Frontend only __changedFields?: string[] } diff --git a/src-ui/src/app/data/paperless-config.ts b/src-ui/src/app/data/paperless-config.ts index 3afca66ff..ce4faff81 100644 --- a/src-ui/src/app/data/paperless-config.ts +++ b/src-ui/src/app/data/paperless-config.ts @@ -44,12 +44,24 @@ export enum ConfigOptionType { Boolean = 'boolean', JSON = 'json', File = 'file', + Password = 'password', } export const ConfigCategory = { General: $localize`General Settings`, OCR: $localize`OCR Settings`, Barcode: $localize`Barcode Settings`, + AI: $localize`AI Settings`, +} + +export const LLMEmbeddingBackendConfig = { + OPENAI: 'openai', + HUGGINGFACE: 'huggingface', +} + +export const LLMBackendConfig = { + OPENAI: 'openai', + OLLAMA: 'ollama', } export interface ConfigOption { @@ -59,6 +71,7 @@ export interface ConfigOption { choices?: Array<{ id: string; name: string }> config_key?: string category: string + note?: string } function mapToItems(enumObj: Object): Array<{ id: string; name: string }> { @@ -258,6 +271,65 @@ export const PaperlessConfigOptions: ConfigOption[] = [ config_key: 'PAPERLESS_CONSUMER_TAG_BARCODE_MAPPING', category: ConfigCategory.Barcode, }, + { + key: 'barcode_tag_split', + title: $localize`Split on Tag Barcodes`, + type: ConfigOptionType.Boolean, + config_key: 'PAPERLESS_CONSUMER_TAG_BARCODE_SPLIT', + category: ConfigCategory.Barcode, + }, + { + key: 'ai_enabled', + title: $localize`AI Enabled`, + type: ConfigOptionType.Boolean, + config_key: 'PAPERLESS_AI_ENABLED', + category: ConfigCategory.AI, + note: $localize`Consider privacy implications when enabling AI features, especially if using a remote model.`, + }, + { + key: 'llm_embedding_backend', + title: $localize`LLM Embedding Backend`, + type: ConfigOptionType.Select, + choices: mapToItems(LLMEmbeddingBackendConfig), + config_key: 'PAPERLESS_AI_LLM_EMBEDDING_BACKEND', + category: ConfigCategory.AI, + }, + { + key: 'llm_embedding_model', + title: $localize`LLM Embedding Model`, + type: ConfigOptionType.String, + config_key: 'PAPERLESS_AI_LLM_EMBEDDING_MODEL', + category: ConfigCategory.AI, + }, + { + key: 'llm_backend', + title: $localize`LLM Backend`, + type: ConfigOptionType.Select, + choices: mapToItems(LLMBackendConfig), + config_key: 'PAPERLESS_AI_LLM_BACKEND', + category: ConfigCategory.AI, + }, + { + key: 'llm_model', + title: $localize`LLM Model`, + type: ConfigOptionType.String, + config_key: 'PAPERLESS_AI_LLM_MODEL', + category: ConfigCategory.AI, + }, + { + key: 'llm_api_key', + title: $localize`LLM API Key`, + type: ConfigOptionType.Password, + config_key: 'PAPERLESS_AI_LLM_API_KEY', + category: ConfigCategory.AI, + }, + { + key: 'llm_endpoint', + title: $localize`LLM Endpoint`, + type: ConfigOptionType.String, + config_key: 'PAPERLESS_AI_LLM_ENDPOINT', + category: ConfigCategory.AI, + }, ] export interface PaperlessConfig extends ObjectWithId { @@ -287,4 +359,12 @@ export interface PaperlessConfig extends ObjectWithId { barcode_max_pages: number barcode_enable_tag: boolean barcode_tag_mapping: object + barcode_tag_split: boolean + ai_enabled: boolean + llm_embedding_backend: string + llm_embedding_model: string + llm_backend: string + llm_model: string + llm_api_key: string + llm_endpoint: string } diff --git a/src-ui/src/app/data/paperless-task.ts b/src-ui/src/app/data/paperless-task.ts index 1bec277eb..19dd3921e 100644 --- a/src-ui/src/app/data/paperless-task.ts +++ b/src-ui/src/app/data/paperless-task.ts @@ -1,3 +1,4 @@ +import { Document } from './document' import { ObjectWithId } from './object-with-id' export enum PaperlessTaskType { @@ -11,6 +12,7 @@ export enum PaperlessTaskName { TrainClassifier = 'train_classifier', SanityCheck = 'check_sanity', IndexOptimize = 'index_optimize', + LLMIndexUpdate = 'llmindex_update', } export enum PaperlessTaskStatus { @@ -41,5 +43,7 @@ export interface PaperlessTask extends ObjectWithId { related_document?: number + duplicate_documents?: Document[] + owner?: number } diff --git a/src-ui/src/app/data/share-link-bundle.ts b/src-ui/src/app/data/share-link-bundle.ts new file mode 100644 index 000000000..fe6134997 --- /dev/null +++ b/src-ui/src/app/data/share-link-bundle.ts @@ -0,0 +1,53 @@ +import { FileVersion } from './share-link' + +export enum ShareLinkBundleStatus { + Pending = 'pending', + Processing = 'processing', + Ready = 'ready', + Failed = 'failed', +} + +export type ShareLinkBundleError = { + bundle_id: number + message?: string + exception_type?: string + timestamp?: string +} + +export interface ShareLinkBundleSummary { + id: number + slug: string + created: string // Date + expiration?: string // Date + documents: number[] + document_count: number + file_version: FileVersion + status: ShareLinkBundleStatus + built_at?: string + size_bytes?: number + last_error?: ShareLinkBundleError +} + +export interface ShareLinkBundleCreatePayload { + document_ids: number[] + file_version: FileVersion + expiration_days: number | null +} + +export const SHARE_LINK_BUNDLE_STATUS_LABELS: Record< + ShareLinkBundleStatus, + string +> = { + [ShareLinkBundleStatus.Pending]: $localize`Pending`, + [ShareLinkBundleStatus.Processing]: $localize`Processing`, + [ShareLinkBundleStatus.Ready]: $localize`Ready`, + [ShareLinkBundleStatus.Failed]: $localize`Failed`, +} + +export const SHARE_LINK_BUNDLE_FILE_VERSION_LABELS: Record< + FileVersion, + string +> = { + [FileVersion.Archive]: $localize`Archive`, + [FileVersion.Original]: $localize`Original`, +} diff --git a/src-ui/src/app/data/share-link.ts b/src-ui/src/app/data/share-link.ts index debc8c111..d9710bd47 100644 --- a/src-ui/src/app/data/share-link.ts +++ b/src-ui/src/app/data/share-link.ts @@ -5,6 +5,18 @@ export enum FileVersion { Original = 'original', } +export interface ShareLinkExpirationOption { + label: string + value: number | null +} + +export const SHARE_LINK_EXPIRATION_OPTIONS: ShareLinkExpirationOption[] = [ + { label: $localize`1 day`, value: 1 }, + { label: $localize`7 days`, value: 7 }, + { label: $localize`30 days`, value: 30 }, + { label: $localize`Never`, value: null }, +] + export interface ShareLink extends ObjectWithPermissions { created: string // Date diff --git a/src-ui/src/app/data/system-status.ts b/src-ui/src/app/data/system-status.ts index 334dc54f8..7dcbffa20 100644 --- a/src-ui/src/app/data/system-status.ts +++ b/src-ui/src/app/data/system-status.ts @@ -7,6 +7,7 @@ export enum SystemStatusItemStatus { OK = 'OK', ERROR = 'ERROR', WARNING = 'WARNING', + DISABLED = 'DISABLED', } export interface SystemStatus { @@ -43,6 +44,9 @@ export interface SystemStatus { sanity_check_status: SystemStatusItemStatus sanity_check_last_run: string // ISO date string sanity_check_error: string + llmindex_status: SystemStatusItemStatus + llmindex_last_modified: string // ISO date string + llmindex_error: string } websocket_connected?: SystemStatusItemStatus // added client-side } diff --git a/src-ui/src/app/data/ui-settings.ts b/src-ui/src/app/data/ui-settings.ts index 6ace74810..f3c908dc3 100644 --- a/src-ui/src/app/data/ui-settings.ts +++ b/src-ui/src/app/data/ui-settings.ts @@ -1,3 +1,5 @@ +import { PdfEditorEditMode } from '../components/common/pdf-editor/pdf-editor-edit-mode' +import { ZoomSetting } from '../components/document-detail/zoom-setting' import { User } from './user' export interface UiSettings { @@ -70,12 +72,17 @@ export const SETTINGS_KEYS = { 'general-settings:document-editing:remove-inbox-tags', DOCUMENT_EDITING_OVERLAY_THUMBNAIL: 'general-settings:document-editing:overlay-thumbnail', + DOCUMENT_DETAILS_HIDDEN_FIELDS: + 'general-settings:document-details:hidden-fields', SEARCH_DB_ONLY: 'general-settings:search:db-only', SEARCH_FULL_TYPE: 'general-settings:search:more-link', + PDF_EDITOR_DEFAULT_EDIT_MODE: + 'general-settings:document-editing:default-edit-mode', EMPTY_TRASH_DELAY: 'trash_delay', GMAIL_OAUTH_URL: 'gmail_oauth_url', OUTLOOK_OAUTH_URL: 'outlook_oauth_url', EMAIL_ENABLED: 'email_enabled', + AI_ENABLED: 'ai_enabled', } export const SETTINGS: UiSetting[] = [ @@ -254,6 +261,11 @@ export const SETTINGS: UiSetting[] = [ type: 'boolean', default: true, }, + { + key: SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS, + type: 'array', + default: [], + }, { key: SETTINGS_KEYS.SEARCH_DB_ONLY, type: 'boolean', @@ -287,6 +299,16 @@ export const SETTINGS: UiSetting[] = [ { key: SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING, type: 'string', - default: 'page-width', // ZoomSetting from 'document-detail.component' + default: ZoomSetting.PageWidth, + }, + { + key: SETTINGS_KEYS.AI_ENABLED, + type: 'boolean', + default: false, + }, + { + key: SETTINGS_KEYS.PDF_EDITOR_DEFAULT_EDIT_MODE, + type: 'string', + default: PdfEditorEditMode.Create, }, ] diff --git a/src-ui/src/app/data/workflow-trigger.ts b/src-ui/src/app/data/workflow-trigger.ts index 888b18cc3..2bc89f188 100644 --- a/src-ui/src/app/data/workflow-trigger.ts +++ b/src-ui/src/app/data/workflow-trigger.ts @@ -44,10 +44,16 @@ export interface WorkflowTrigger extends ObjectWithId { filter_has_not_tags?: number[] // Tag.id[] + filter_has_any_correspondents?: number[] // Correspondent.id[] + filter_has_not_correspondents?: number[] // Correspondent.id[] + filter_has_any_document_types?: number[] // DocumentType.id[] + filter_has_not_document_types?: number[] // DocumentType.id[] + filter_has_any_storage_paths?: number[] // StoragePath.id[] + filter_has_not_storage_paths?: number[] // StoragePath.id[] filter_custom_field_query?: string diff --git a/src-ui/src/app/interceptors/api-version.interceptor.spec.ts b/src-ui/src/app/interceptors/api-version.interceptor.spec.ts index c4ddd9349..154c50bc9 100644 --- a/src-ui/src/app/interceptors/api-version.interceptor.spec.ts +++ b/src-ui/src/app/interceptors/api-version.interceptor.spec.ts @@ -1,30 +1,41 @@ -import { HttpEvent, HttpRequest } from '@angular/common/http' +import { + HttpClient, + provideHttpClient, + withInterceptors, +} from '@angular/common/http' +import { + HttpTestingController, + provideHttpClientTesting, +} from '@angular/common/http/testing' import { TestBed } from '@angular/core/testing' -import { of } from 'rxjs' import { environment } from 'src/environments/environment' -import { ApiVersionInterceptor } from './api-version.interceptor' +import { withApiVersionInterceptor } from './api-version.interceptor' describe('ApiVersionInterceptor', () => { - let interceptor: ApiVersionInterceptor + let httpClient: HttpClient + let httpMock: HttpTestingController beforeEach(() => { TestBed.configureTestingModule({ - providers: [ApiVersionInterceptor], + providers: [ + provideHttpClient(withInterceptors([withApiVersionInterceptor])), + provideHttpClientTesting(), + ], }) - interceptor = TestBed.inject(ApiVersionInterceptor) + httpClient = TestBed.inject(HttpClient) + httpMock = TestBed.inject(HttpTestingController) }) it('should add api version to headers', () => { - interceptor.intercept(new HttpRequest('GET', 'https://example.com'), { - handle: (request) => { - const header = request.headers['lazyUpdate'][0] - expect(header.name).toEqual('Accept') - expect(header.value).toEqual( - `application/json; version=${environment.apiVersion}` - ) - return of({} as HttpEvent) - }, - }) + httpClient.get('https://example.com').subscribe() + const request = httpMock.expectOne('https://example.com') + const header = request.request.headers['lazyUpdate'][0] + + expect(header.name).toEqual('Accept') + expect(header.value).toEqual( + `application/json; version=${environment.apiVersion}` + ) + request.flush({}) }) }) diff --git a/src-ui/src/app/interceptors/api-version.interceptor.ts b/src-ui/src/app/interceptors/api-version.interceptor.ts index f6ec6798d..974184675 100644 --- a/src-ui/src/app/interceptors/api-version.interceptor.ts +++ b/src-ui/src/app/interceptors/api-version.interceptor.ts @@ -1,27 +1,20 @@ import { HttpEvent, - HttpHandler, - HttpInterceptor, + HttpHandlerFn, + HttpInterceptorFn, HttpRequest, } from '@angular/common/http' -import { Injectable } from '@angular/core' import { Observable } from 'rxjs' import { environment } from 'src/environments/environment' -@Injectable() -export class ApiVersionInterceptor implements HttpInterceptor { - constructor() {} - - intercept( - request: HttpRequest, - next: HttpHandler - ): Observable> { - request = request.clone({ - setHeaders: { - Accept: `application/json; version=${environment.apiVersion}`, - }, - }) - - return next.handle(request) - } +export const withApiVersionInterceptor: HttpInterceptorFn = ( + request: HttpRequest, + next: HttpHandlerFn +): Observable> => { + request = request.clone({ + setHeaders: { + Accept: `application/json; version=${environment.apiVersion}`, + }, + }) + return next(request) } diff --git a/src-ui/src/app/interceptors/csrf.interceptor.spec.ts b/src-ui/src/app/interceptors/csrf.interceptor.spec.ts index fb2e1a2fa..2a6ec4175 100644 --- a/src-ui/src/app/interceptors/csrf.interceptor.spec.ts +++ b/src-ui/src/app/interceptors/csrf.interceptor.spec.ts @@ -1,35 +1,52 @@ -import { HttpEvent, HttpRequest } from '@angular/common/http' +import { + HttpClient, + provideHttpClient, + withInterceptors, +} from '@angular/common/http' +import { + HttpTestingController, + provideHttpClientTesting, +} from '@angular/common/http/testing' import { TestBed } from '@angular/core/testing' import { Meta } from '@angular/platform-browser' import { CookieService } from 'ngx-cookie-service' -import { of } from 'rxjs' -import { CsrfInterceptor } from './csrf.interceptor' +import { withCsrfInterceptor } from './csrf.interceptor' describe('CsrfInterceptor', () => { - let interceptor: CsrfInterceptor let meta: Meta let cookieService: CookieService + let httpClient: HttpClient + let httpMock: HttpTestingController beforeEach(() => { TestBed.configureTestingModule({ - providers: [CsrfInterceptor, Meta, CookieService], + providers: [ + Meta, + CookieService, + provideHttpClient(withInterceptors([withCsrfInterceptor])), + provideHttpClientTesting(), + ], }) meta = TestBed.inject(Meta) cookieService = TestBed.inject(CookieService) - interceptor = TestBed.inject(CsrfInterceptor) + httpClient = TestBed.inject(HttpClient) + httpMock = TestBed.inject(HttpTestingController) }) it('should get csrf token', () => { meta.addTag({ name: 'cookie_prefix', content: 'ngx-' }, true) + const cookieServiceSpy = jest.spyOn(cookieService, 'get') cookieServiceSpy.mockReturnValue('csrftoken') - interceptor.intercept(new HttpRequest('GET', 'https://example.com'), { - handle: (request) => { - expect(request.headers['lazyUpdate'][0]['name']).toEqual('X-CSRFToken') - return of({} as HttpEvent) - }, - }) + + httpClient.get('https://example.com').subscribe() + const request = httpMock.expectOne('https://example.com') + + expect(request.request.headers['lazyUpdate'][0]['name']).toEqual( + 'X-CSRFToken' + ) expect(cookieServiceSpy).toHaveBeenCalled() + request.flush({}) }) }) diff --git a/src-ui/src/app/interceptors/csrf.interceptor.ts b/src-ui/src/app/interceptors/csrf.interceptor.ts index 2f590c5eb..e7fad4481 100644 --- a/src-ui/src/app/interceptors/csrf.interceptor.ts +++ b/src-ui/src/app/interceptors/csrf.interceptor.ts @@ -1,36 +1,32 @@ import { HttpEvent, - HttpHandler, - HttpInterceptor, + HttpHandlerFn, + HttpInterceptorFn, HttpRequest, } from '@angular/common/http' -import { Injectable, inject } from '@angular/core' +import { inject } from '@angular/core' import { Meta } from '@angular/platform-browser' import { CookieService } from 'ngx-cookie-service' import { Observable } from 'rxjs' -@Injectable() -export class CsrfInterceptor implements HttpInterceptor { - private cookieService = inject(CookieService) - private meta = inject(Meta) +export const withCsrfInterceptor: HttpInterceptorFn = ( + request: HttpRequest, + next: HttpHandlerFn +): Observable> => { + const cookieService: CookieService = inject(CookieService) + const meta: Meta = inject(Meta) - intercept( - request: HttpRequest, - next: HttpHandler - ): Observable> { - let prefix = '' - if (this.meta.getTag('name=cookie_prefix')) { - prefix = this.meta.getTag('name=cookie_prefix').content - } - let csrfToken = this.cookieService.get(`${prefix}csrftoken`) - if (csrfToken) { - request = request.clone({ - setHeaders: { - 'X-CSRFToken': csrfToken, - }, - }) - } - - return next.handle(request) + let prefix = '' + if (meta.getTag('name=cookie_prefix')) { + prefix = meta.getTag('name=cookie_prefix').content } + let csrfToken = cookieService.get(`${prefix}csrftoken`) + if (csrfToken) { + request = request.clone({ + setHeaders: { + 'X-CSRFToken': csrfToken, + }, + }) + } + return next(request) } diff --git a/src-ui/src/app/services/chat.service.spec.ts b/src-ui/src/app/services/chat.service.spec.ts new file mode 100644 index 000000000..b8ca957cb --- /dev/null +++ b/src-ui/src/app/services/chat.service.spec.ts @@ -0,0 +1,58 @@ +import { + HttpEventType, + provideHttpClient, + withInterceptorsFromDi, +} from '@angular/common/http' +import { + HttpTestingController, + provideHttpClientTesting, +} from '@angular/common/http/testing' +import { TestBed } from '@angular/core/testing' +import { environment } from 'src/environments/environment' +import { ChatService } from './chat.service' + +describe('ChatService', () => { + let service: ChatService + let httpMock: HttpTestingController + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [], + providers: [ + ChatService, + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + ], + }) + service = TestBed.inject(ChatService) + httpMock = TestBed.inject(HttpTestingController) + }) + + afterEach(() => { + httpMock.verify() + }) + + it('should stream chat messages', (done) => { + const documentId = 1 + const prompt = 'Hello, world!' + const mockResponse = 'Partial response text' + const apiUrl = `${environment.apiBaseUrl}documents/chat/` + + service.streamChat(documentId, prompt).subscribe((chunk) => { + expect(chunk).toBe(mockResponse) + done() + }) + + const req = httpMock.expectOne(apiUrl) + expect(req.request.method).toBe('POST') + expect(req.request.body).toEqual({ + document_id: documentId, + q: prompt, + }) + + req.event({ + type: HttpEventType.DownloadProgress, + partialText: mockResponse, + } as any) + }) +}) diff --git a/src-ui/src/app/services/chat.service.ts b/src-ui/src/app/services/chat.service.ts new file mode 100644 index 000000000..9ddfb8330 --- /dev/null +++ b/src-ui/src/app/services/chat.service.ts @@ -0,0 +1,46 @@ +import { + HttpClient, + HttpDownloadProgressEvent, + HttpEventType, +} from '@angular/common/http' +import { inject, Injectable } from '@angular/core' +import { filter, map, Observable } from 'rxjs' +import { environment } from 'src/environments/environment' + +export interface ChatMessage { + role: 'user' | 'assistant' + content: string + isStreaming?: boolean +} + +@Injectable({ + providedIn: 'root', +}) +export class ChatService { + private http: HttpClient = inject(HttpClient) + + streamChat(documentId: number, prompt: string): Observable { + return this.http + .post( + `${environment.apiBaseUrl}documents/chat/`, + { + document_id: documentId, + q: prompt, + }, + { + observe: 'events', + reportProgress: true, + responseType: 'text', + withCredentials: true, + } + ) + .pipe( + map((event) => { + if (event.type === HttpEventType.DownloadProgress) { + return (event as HttpDownloadProgressEvent).partialText! + } + }), + filter((chunk) => !!chunk) + ) + } +} diff --git a/src-ui/src/app/services/rest/share-link-bundle.service.spec.ts b/src-ui/src/app/services/rest/share-link-bundle.service.spec.ts new file mode 100644 index 000000000..6b87ddf04 --- /dev/null +++ b/src-ui/src/app/services/rest/share-link-bundle.service.spec.ts @@ -0,0 +1,60 @@ +import { HttpTestingController } from '@angular/common/http/testing' +import { TestBed } from '@angular/core/testing' +import { Subscription } from 'rxjs' +import { environment } from 'src/environments/environment' +import { commonAbstractPaperlessServiceTests } from './abstract-paperless-service.spec' +import { ShareLinkBundleService } from './share-link-bundle.service' + +const endpoint = 'share_link_bundles' + +commonAbstractPaperlessServiceTests(endpoint, ShareLinkBundleService) + +describe('ShareLinkBundleService', () => { + let httpTestingController: HttpTestingController + let service: ShareLinkBundleService + let subscription: Subscription | undefined + + beforeEach(() => { + httpTestingController = TestBed.inject(HttpTestingController) + service = TestBed.inject(ShareLinkBundleService) + }) + + afterEach(() => { + subscription?.unsubscribe() + httpTestingController.verify() + }) + + it('creates bundled share links', () => { + const payload = { + document_ids: [1, 2], + file_version: 'archive', + expiration_days: 7, + } + subscription = service.createBundle(payload as any).subscribe() + const req = httpTestingController.expectOne( + `${environment.apiBaseUrl}${endpoint}/` + ) + expect(req.request.method).toBe('POST') + expect(req.request.body).toEqual(payload) + req.flush({}) + }) + + it('rebuilds bundles', () => { + subscription = service.rebuildBundle(12).subscribe() + const req = httpTestingController.expectOne( + `${environment.apiBaseUrl}${endpoint}/12/rebuild/` + ) + expect(req.request.method).toBe('POST') + expect(req.request.body).toEqual({}) + req.flush({}) + }) + + it('lists bundles with expected parameters', () => { + subscription = service.listAllBundles().subscribe() + const req = httpTestingController.expectOne( + `${environment.apiBaseUrl}${endpoint}/?page=1&page_size=1000&ordering=-created` + ) + expect(req.request.method).toBe('GET') + req.flush({ results: [] }) + }) +}) diff --git a/src-ui/src/app/services/rest/share-link-bundle.service.ts b/src-ui/src/app/services/rest/share-link-bundle.service.ts new file mode 100644 index 000000000..2aa719974 --- /dev/null +++ b/src-ui/src/app/services/rest/share-link-bundle.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@angular/core' +import { Observable } from 'rxjs' +import { map } from 'rxjs/operators' +import { + ShareLinkBundleCreatePayload, + ShareLinkBundleSummary, +} from 'src/app/data/share-link-bundle' +import { AbstractNameFilterService } from './abstract-name-filter-service' + +@Injectable({ + providedIn: 'root', +}) +export class ShareLinkBundleService extends AbstractNameFilterService { + constructor() { + super() + this.resourceName = 'share_link_bundles' + } + + createBundle( + payload: ShareLinkBundleCreatePayload + ): Observable { + this.clearCache() + return this.http.post( + this.getResourceUrl(), + payload + ) + } + rebuildBundle(bundleId: number): Observable { + this.clearCache() + return this.http.post( + this.getResourceUrl(bundleId, 'rebuild'), + {} + ) + } + + listAllBundles(): Observable { + return this.list(1, 1000, 'created', true).pipe( + map((response) => response.results) + ) + } +} diff --git a/src-ui/src/environments/environment.prod.ts b/src-ui/src/environments/environment.prod.ts index c8bb844e9..9ebf29d16 100644 --- a/src-ui/src/environments/environment.prod.ts +++ b/src-ui/src/environments/environment.prod.ts @@ -6,7 +6,7 @@ export const environment = { apiVersion: '9', // match src/paperless/settings.py appTitle: 'Paperless-ngx', tag: 'prod', - version: '2.20.3', + version: '2.20.5', webSocketHost: window.location.host, webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:', webSocketBaseUrl: base_url.pathname + 'ws/', diff --git a/src-ui/src/main.ts b/src-ui/src/main.ts index 7f1a39fbe..e894bf196 100644 --- a/src-ui/src/main.ts +++ b/src-ui/src/main.ts @@ -1,15 +1,16 @@ import { - APP_INITIALIZER, - enableProdMode, importProvidersFrom, + inject, + provideAppInitializer, provideZoneChangeDetection, } from '@angular/core' import { DragDropModule } from '@angular/cdk/drag-drop' import { DatePipe, registerLocaleData } from '@angular/common' import { - HTTP_INTERCEPTORS, provideHttpClient, + withFetch, + withInterceptors, withInterceptorsFromDi, } from '@angular/common/http' import { FormsModule, ReactiveFormsModule } from '@angular/forms' @@ -49,6 +50,7 @@ import { caretDown, caretUp, chatLeftText, + chatSquareDots, check, check2All, checkAll, @@ -124,6 +126,7 @@ import { sliders2Vertical, sortAlphaDown, sortAlphaUpAlt, + stars, tag, tagFill, tags, @@ -148,15 +151,14 @@ import { AppComponent } from './app/app.component' import { DirtyDocGuard } from './app/guards/dirty-doc.guard' import { DirtySavedViewGuard } from './app/guards/dirty-saved-view.guard' import { PermissionsGuard } from './app/guards/permissions.guard' -import { ApiVersionInterceptor } from './app/interceptors/api-version.interceptor' -import { CsrfInterceptor } from './app/interceptors/csrf.interceptor' +import { withApiVersionInterceptor } from './app/interceptors/api-version.interceptor' +import { withCsrfInterceptor } from './app/interceptors/csrf.interceptor' import { DocumentTitlePipe } from './app/pipes/document-title.pipe' import { FilterPipe } from './app/pipes/filter.pipe' import { UsernamePipe } from './app/pipes/username.pipe' import { SettingsService } from './app/services/settings.service' import { LocalizedDateParserFormatter } from './app/utils/ngb-date-parser-formatter' import { ISODateAdapter } from './app/utils/ngb-iso-date-adapter' -import { environment } from './environments/environment' import localeAf from '@angular/common/locales/af' import localeAr from '@angular/common/locales/ar' @@ -234,11 +236,11 @@ registerLocaleData(localeUk) registerLocaleData(localeZh) registerLocaleData(localeZhHant) -function initializeApp(settings: SettingsService) { - return () => { - return settings.initializeSettings() - } +function initializeApp() { + const settings = inject(SettingsService) + return settings.initializeSettings() } + const icons = { airplane, archive, @@ -266,6 +268,7 @@ const icons = { caretDown, caretUp, chatLeftText, + chatSquareDots, check, check2All, checkAll, @@ -341,6 +344,7 @@ const icons = { sliders2Vertical, sortAlphaDown, sortAlphaUpAlt, + stars, tagFill, tag, tags, @@ -358,10 +362,6 @@ const icons = { xLg, } -if (environment.production) { - enableProdMode() -} - bootstrapApplication(AppComponent, { providers: [ provideZoneChangeDetection(), @@ -378,24 +378,9 @@ bootstrapApplication(AppComponent, { DragDropModule, NgxBootstrapIconsModule.pick(icons) ), - { - provide: APP_INITIALIZER, - useFactory: initializeApp, - deps: [SettingsService], - multi: true, - }, + provideAppInitializer(initializeApp), DatePipe, CookieService, - { - provide: HTTP_INTERCEPTORS, - useClass: CsrfInterceptor, - multi: true, - }, - { - provide: HTTP_INTERCEPTORS, - useClass: ApiVersionInterceptor, - multi: true, - }, FilterPipe, DocumentTitlePipe, { provide: NgbDateAdapter, useClass: ISODateAdapter }, @@ -407,6 +392,10 @@ bootstrapApplication(AppComponent, { CorrespondentNamePipe, DocumentTypeNamePipe, StoragePathNamePipe, - provideHttpClient(withInterceptorsFromDi()), + provideHttpClient( + withInterceptorsFromDi(), + withInterceptors([withCsrfInterceptor, withApiVersionInterceptor]), + withFetch() + ), ], }).catch((err) => console.error(err)) diff --git a/src-ui/src/theme.scss b/src-ui/src/theme.scss index eacc3b4e7..6ff5f4a09 100644 --- a/src-ui/src/theme.scss +++ b/src-ui/src/theme.scss @@ -73,6 +73,7 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml, 0 ): @@ -442,15 +462,24 @@ class BarcodePlugin(ConsumeTaskPlugin): for bc in self.barcodes if bc.is_separator and (not retain or (retain and bc.page > 0)) } # as below, dont include the first page if retain is enabled - if not self.settings.barcode_enable_asn: - return separator_pages # add the page numbers of the ASN barcodes # (except for first page, that might lead to infinite loops). - return { - **separator_pages, - **{bc.page: True for bc in self.barcodes if bc.is_asn and bc.page != 0}, - } + if self.settings.barcode_enable_asn: + separator_pages = { + **separator_pages, + **{bc.page: True for bc in self.barcodes if bc.is_asn and bc.page != 0}, + } + + # add the page numbers of the TAG barcodes if splitting is enabled + # (except for first page, that might lead to infinite loops). + if self.settings.barcode_tag_split and self.settings.barcode_enable_tag: + separator_pages = { + **separator_pages, + **{bc.page: True for bc in self.barcodes if bc.is_tag and bc.page != 0}, + } + + return separator_pages def separate_pages(self, pages_to_split_on: dict[int, bool]) -> list[Path]: """ diff --git a/src/documents/caching.py b/src/documents/caching.py index ed7f6dbc1..f2911e51e 100644 --- a/src/documents/caching.py +++ b/src/documents/caching.py @@ -41,6 +41,7 @@ class SuggestionCacheData: CLASSIFIER_VERSION_KEY: Final[str] = "classifier_version" CLASSIFIER_HASH_KEY: Final[str] = "classifier_hash" CLASSIFIER_MODIFIED_KEY: Final[str] = "classifier_modified" +LLM_CACHE_CLASSIFIER_VERSION: Final[int] = 1000 # Marker distinguishing LLM suggestions CACHE_1_MINUTE: Final[int] = 60 CACHE_5_MINUTES: Final[int] = 5 * CACHE_1_MINUTE @@ -196,6 +197,54 @@ def refresh_suggestions_cache( cache.touch(doc_key, timeout) +def get_llm_suggestion_cache( + document_id: int, + backend: str, +) -> SuggestionCacheData | None: + doc_key = get_suggestion_cache_key(document_id) + data: SuggestionCacheData = cache.get(doc_key) + + if data and data.classifier_hash == backend: + return data + + return None + + +def set_llm_suggestions_cache( + document_id: int, + suggestions: dict, + *, + backend: str, + timeout: int = CACHE_50_MINUTES, +) -> None: + """ + Cache LLM-generated suggestions using a backend-specific identifier (e.g. 'openai:gpt-4'). + """ + doc_key = get_suggestion_cache_key(document_id) + cache.set( + doc_key, + SuggestionCacheData( + classifier_version=LLM_CACHE_CLASSIFIER_VERSION, + classifier_hash=backend, + suggestions=suggestions, + ), + timeout, + ) + + +def invalidate_llm_suggestions_cache( + document_id: int, +) -> None: + """ + Invalidate the LLM suggestions cache for a specific document and backend. + """ + doc_key = get_suggestion_cache_key(document_id) + data: SuggestionCacheData = cache.get(doc_key) + + if data: + cache.delete(doc_key) + + def get_metadata_cache_key(document_id: int) -> str: """ Returns the basic key for a document's metadata diff --git a/src/documents/checks.py b/src/documents/checks.py index 8f8fbf4f9..b6e9e90fc 100644 --- a/src/documents/checks.py +++ b/src/documents/checks.py @@ -1,60 +1,12 @@ -import textwrap - from django.conf import settings from django.core.checks import Error from django.core.checks import Warning from django.core.checks import register -from django.core.exceptions import FieldError -from django.db.utils import OperationalError -from django.db.utils import ProgrammingError from documents.signals import document_consumer_declaration from documents.templating.utils import convert_format_str_to_template_format -@register() -def changed_password_check(app_configs, **kwargs): - from documents.models import Document - from paperless.db import GnuPG - - try: - encrypted_doc = ( - Document.objects.filter( - storage_type=Document.STORAGE_TYPE_GPG, - ) - .only("pk", "storage_type") - .first() - ) - except (OperationalError, ProgrammingError, FieldError): - return [] # No documents table yet - - if encrypted_doc: - if not settings.PASSPHRASE: - return [ - Error( - "The database contains encrypted documents but no password is set.", - ), - ] - - if not GnuPG.decrypted(encrypted_doc.source_file): - return [ - Error( - textwrap.dedent( - """ - The current password doesn't match the password of the - existing documents. - - If you intend to change your password, you must first export - all of the old documents, start fresh with the new password - and then re-import them." - """, - ), - ), - ] - - return [] - - @register() def parser_check(app_configs, **kwargs): parsers = [] diff --git a/src/documents/conditionals.py b/src/documents/conditionals.py index 47d9bfe4b..b93cabf62 100644 --- a/src/documents/conditionals.py +++ b/src/documents/conditionals.py @@ -128,7 +128,7 @@ def thumbnail_last_modified(request, pk: int) -> datetime | None: Cache should be (slightly?) faster than filesystem """ try: - doc = Document.objects.only("storage_type").get(pk=pk) + doc = Document.objects.only("pk").get(pk=pk) if not doc.thumbnail_path.exists(): return None doc_key = get_thumbnail_modified_key(pk) diff --git a/src/documents/consumer.py b/src/documents/consumer.py index 2c1cf025b..1ff60220b 100644 --- a/src/documents/consumer.py +++ b/src/documents/consumer.py @@ -497,7 +497,6 @@ class ConsumerPlugin( create_source_path_directory(document.source_path) self._write( - document.storage_type, self.unmodified_original if self.unmodified_original is not None else self.working_copy, @@ -505,7 +504,6 @@ class ConsumerPlugin( ) self._write( - document.storage_type, thumbnail, document.thumbnail_path, ) @@ -517,7 +515,6 @@ class ConsumerPlugin( ) create_source_path_directory(document.archive_path) self._write( - document.storage_type, archive_path, document.archive_path, ) @@ -637,8 +634,6 @@ class ConsumerPlugin( ) self.log.debug(f"Creation date from st_mtime: {create_date}") - storage_type = Document.STORAGE_TYPE_UNENCRYPTED - if self.metadata.filename: title = Path(self.metadata.filename).stem else: @@ -665,7 +660,6 @@ class ConsumerPlugin( checksum=hashlib.md5(file_for_checksum.read_bytes()).hexdigest(), created=create_date, modified=create_date, - storage_type=storage_type, page_count=page_count, original_filename=self.filename, ) @@ -736,7 +730,7 @@ class ConsumerPlugin( } CustomFieldInstance.objects.create(**args) # adds to document - def _write(self, storage_type, source, target): + def _write(self, source, target): with ( Path(source).open("rb") as read_file, Path(target).open("wb") as write_file, @@ -785,19 +779,45 @@ class ConsumerPreflightPlugin( Q(checksum=checksum) | Q(archive_checksum=checksum), ) if existing_doc.exists(): - msg = ConsumerStatusShortMessage.DOCUMENT_ALREADY_EXISTS - log_msg = f"Not consuming {self.filename}: It is a duplicate of {existing_doc.get().title} (#{existing_doc.get().pk})." + existing_doc = existing_doc.order_by("-created") + duplicates_in_trash = existing_doc.filter(deleted_at__isnull=False) + log_msg = ( + f"Consuming duplicate {self.filename}: " + f"{existing_doc.count()} existing document(s) share the same content." + ) - if existing_doc.first().deleted_at is not None: - msg = ConsumerStatusShortMessage.DOCUMENT_ALREADY_EXISTS_IN_TRASH - log_msg += " Note: existing document is in the trash." + if duplicates_in_trash.exists(): + log_msg += " Note: at least one existing document is in the trash." + + self.log.warning(log_msg) if settings.CONSUMER_DELETE_DUPLICATES: + duplicate = existing_doc.first() + duplicate_label = ( + duplicate.title + or duplicate.original_filename + or (Path(duplicate.filename).name if duplicate.filename else None) + or str(duplicate.pk) + ) + Path(self.input_doc.original_file).unlink() - self._fail( - msg, - log_msg, - ) + + failure_msg = ( + f"Not consuming {self.filename}: " + f"It is a duplicate of {duplicate_label} (#{duplicate.pk})" + ) + status_msg = ConsumerStatusShortMessage.DOCUMENT_ALREADY_EXISTS + + if duplicates_in_trash.exists(): + status_msg = ( + ConsumerStatusShortMessage.DOCUMENT_ALREADY_EXISTS_IN_TRASH + ) + failure_msg += " Note: existing document is in the trash." + + self._fail( + status_msg, + failure_msg, + ) def pre_check_directories(self): """ diff --git a/src/documents/data_models.py b/src/documents/data_models.py index 3acc3f51b..a4b1150dd 100644 --- a/src/documents/data_models.py +++ b/src/documents/data_models.py @@ -118,7 +118,7 @@ class DocumentMetadataOverrides: ).values_list("id", flat=True), ) overrides.custom_fields = { - custom_field.id: custom_field.value + custom_field.field.id: custom_field.value for custom_field in doc.custom_fields.all() } diff --git a/src/documents/file_handling.py b/src/documents/file_handling.py index 48cd57311..39831016d 100644 --- a/src/documents/file_handling.py +++ b/src/documents/file_handling.py @@ -126,7 +126,6 @@ def generate_filename( doc: Document, *, counter=0, - append_gpg=True, archive_filename=False, ) -> Path: base_path: Path | None = None @@ -170,8 +169,4 @@ def generate_filename( final_filename = f"{doc.pk:07}{counter_str}{filetype_str}" full_path = Path(final_filename) - # Add GPG extension if needed - if append_gpg and doc.storage_type == doc.STORAGE_TYPE_GPG: - full_path = full_path.with_suffix(full_path.suffix + ".gpg") - return full_path diff --git a/src/documents/filters.py b/src/documents/filters.py index 54f3c1fa0..9e53d01af 100644 --- a/src/documents/filters.py +++ b/src/documents/filters.py @@ -39,6 +39,7 @@ from documents.models import Document from documents.models import DocumentType from documents.models import PaperlessTask from documents.models import ShareLink +from documents.models import ShareLinkBundle from documents.models import StoragePath from documents.models import Tag @@ -796,6 +797,29 @@ class ShareLinkFilterSet(FilterSet): } +class ShareLinkBundleFilterSet(FilterSet): + documents = Filter(method="filter_documents") + + class Meta: + model = ShareLinkBundle + fields = { + "created": DATETIME_KWARGS, + "expiration": DATETIME_KWARGS, + "status": ["exact"], + } + + def filter_documents(self, queryset, name, value): + ids = [] + if value: + try: + ids = [int(item) for item in value.split(",") if item] + except ValueError: + return queryset.none() + if not ids: + return queryset + return queryset.filter(documents__in=ids).distinct() + + class PaperlessTaskFilterSet(FilterSet): acknowledged = BooleanFilter( label="Acknowledged", diff --git a/src/documents/index.py b/src/documents/index.py index ea26ea926..8afc31fe9 100644 --- a/src/documents/index.py +++ b/src/documents/index.py @@ -602,7 +602,7 @@ def rewrite_natural_date_keywords(query_string: str) -> str: case "this year": start = datetime(local_now.year, 1, 1, 0, 0, 0, tzinfo=tz) - end = datetime.combine(today, time.max, tzinfo=tz) + end = datetime(local_now.year, 12, 31, 23, 59, 59, tzinfo=tz) case "previous week": days_since_monday = local_now.weekday() diff --git a/src/documents/management/commands/decrypt_documents.py b/src/documents/management/commands/decrypt_documents.py deleted file mode 100644 index 793cac4bb..000000000 --- a/src/documents/management/commands/decrypt_documents.py +++ /dev/null @@ -1,93 +0,0 @@ -from pathlib import Path - -from django.conf import settings -from django.core.management.base import BaseCommand -from django.core.management.base import CommandError - -from documents.models import Document -from paperless.db import GnuPG - - -class Command(BaseCommand): - help = ( - "This is how you migrate your stored documents from an encrypted " - "state to an unencrypted one (or vice-versa)" - ) - - def add_arguments(self, parser) -> None: - parser.add_argument( - "--passphrase", - help=( - "If PAPERLESS_PASSPHRASE isn't set already, you need to specify it here" - ), - ) - - def handle(self, *args, **options) -> None: - try: - self.stdout.write( - self.style.WARNING( - "\n\n" - "WARNING: This script is going to work directly on your " - "document originals, so\n" - "WARNING: you probably shouldn't run " - "this unless you've got a recent backup\n" - "WARNING: handy. It " - "*should* work without a hitch, but be safe and backup your\n" - "WARNING: stuff first.\n\n" - "Hit Ctrl+C to exit now, or Enter to " - "continue.\n\n", - ), - ) - _ = input() - except KeyboardInterrupt: - return - - passphrase = options["passphrase"] or settings.PASSPHRASE - if not passphrase: - raise CommandError( - "Passphrase not defined. Please set it with --passphrase or " - "by declaring it in your environment or your config.", - ) - - self.__gpg_to_unencrypted(passphrase) - - def __gpg_to_unencrypted(self, passphrase: str) -> None: - encrypted_files = Document.objects.filter( - storage_type=Document.STORAGE_TYPE_GPG, - ) - - for document in encrypted_files: - self.stdout.write(f"Decrypting {document}") - - old_paths = [document.source_path, document.thumbnail_path] - - with document.source_file as file_handle: - raw_document = GnuPG.decrypted(file_handle, passphrase) - with document.thumbnail_file as file_handle: - raw_thumb = GnuPG.decrypted(file_handle, passphrase) - - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED - - ext: str = Path(document.filename).suffix - - if not ext == ".gpg": - raise CommandError( - f"Abort: encrypted file {document.source_path} does not " - f"end with .gpg", - ) - - document.filename = Path(document.filename).stem - - with document.source_path.open("wb") as f: - f.write(raw_document) - - with document.thumbnail_path.open("wb") as f: - f.write(raw_thumb) - - Document.objects.filter(id=document.id).update( - storage_type=document.storage_type, - filename=document.filename, - ) - - for path in old_paths: - path.unlink() diff --git a/src/documents/management/commands/document_consumer.py b/src/documents/management/commands/document_consumer.py index 97027e02d..5ba8d30cd 100644 --- a/src/documents/management/commands/document_consumer.py +++ b/src/documents/management/commands/document_consumer.py @@ -1,135 +1,343 @@ +""" +Document consumer management command. + +Watches a consumption directory for new documents and queues them for processing. +Uses watchfiles for efficient file system monitoring with support for both +native OS notifications and polling fallback. +""" + +from __future__ import annotations + import logging -import os -from concurrent.futures import ThreadPoolExecutor -from fnmatch import filter +from dataclasses import dataclass from pathlib import Path -from pathlib import PurePath from threading import Event from time import monotonic -from time import sleep +from typing import TYPE_CHECKING from typing import Final from django import db from django.conf import settings from django.core.management.base import BaseCommand from django.core.management.base import CommandError -from watchdog.events import FileSystemEventHandler -from watchdog.observers.polling import PollingObserver +from watchfiles import Change +from watchfiles import DefaultFilter +from watchfiles import watch from documents.data_models import ConsumableDocument from documents.data_models import DocumentMetadataOverrides from documents.data_models import DocumentSource from documents.models import Tag -from documents.parsers import is_file_ext_supported +from documents.parsers import get_supported_file_extensions from documents.tasks import consume_file -try: - from inotifyrecursive import INotify - from inotifyrecursive import flags -except ImportError: # pragma: no cover - INotify = flags = None +if TYPE_CHECKING: + from collections.abc import Iterator + logger = logging.getLogger("paperless.management.consumer") -def _tags_from_path(filepath: Path) -> list[int]: +@dataclass +class TrackedFile: + """Represents a file being tracked for stability.""" + + path: Path + last_event_time: float + last_mtime: float | None = None + last_size: int | None = None + + def update_stats(self) -> bool: + """ + Update file stats. Returns True if file exists and stats were updated. + """ + try: + stat = self.path.stat() + self.last_mtime = stat.st_mtime + self.last_size = stat.st_size + return True + except OSError: + return False + + def is_unchanged(self) -> bool: + """ + Check if file stats match the previously recorded values. + Returns False if file doesn't exist or stats changed. + """ + try: + stat = self.path.stat() + return stat.st_mtime == self.last_mtime and stat.st_size == self.last_size + except OSError: + return False + + +class FileStabilityTracker: """ - Walk up the directory tree from filepath to CONSUMPTION_DIR + Tracks file events and determines when files are stable for consumption. + + A file is considered stable when: + 1. No new events have been received for it within the stability delay + 2. Its size and modification time haven't changed + 3. It still exists as a regular file + + This handles various edge cases: + - Network copies that write in chunks + - Scanners that open/close files multiple times + - Temporary files that get renamed + - Files that are deleted before becoming stable + """ + + def __init__(self, stability_delay: float = 1.0) -> None: + """ + Initialize the tracker. + + Args: + stability_delay: Time in seconds a file must remain unchanged + before being considered stable. + """ + self.stability_delay = stability_delay + self._tracked: dict[Path, TrackedFile] = {} + + def track(self, path: Path, change: Change) -> None: + """ + Register a file event. + + Args: + path: The file path that changed. + change: The type of change (added, modified, deleted). + """ + path = path.resolve() + + match change: + case Change.deleted: + self._tracked.pop(path, None) + logger.debug(f"Stopped tracking deleted file: {path}") + case Change.added | Change.modified: + current_time = monotonic() + if path in self._tracked: + tracked = self._tracked[path] + tracked.last_event_time = current_time + tracked.update_stats() + logger.debug(f"Updated tracking for: {path}") + else: + tracked = TrackedFile(path=path, last_event_time=current_time) + if tracked.update_stats(): + self._tracked[path] = tracked + logger.debug(f"Started tracking: {path}") + else: + logger.debug(f"Could not stat file, not tracking: {path}") + + def get_stable_files(self) -> Iterator[Path]: + """ + Yield files that have been stable for the configured delay. + + Files are removed from tracking once yielded or determined to be invalid. + """ + current_time = monotonic() + to_remove: list[Path] = [] + to_yield: list[Path] = [] + + for path, tracked in self._tracked.items(): + time_since_event = current_time - tracked.last_event_time + + if time_since_event < self.stability_delay: + continue + + # File has waited long enough, verify it's unchanged + if not tracked.is_unchanged(): + # Stats changed or file gone - update and wait again + if tracked.update_stats(): + tracked.last_event_time = current_time + logger.debug(f"File changed during stability check: {path}") + else: + # File no longer exists, remove from tracking + to_remove.append(path) + logger.debug(f"File disappeared during stability check: {path}") + continue + + # File is stable, we can return it + to_yield.append(path) + logger.info(f"File is stable: {path}") + + # Remove files that are no longer valid + for path in to_remove: + self._tracked.pop(path, None) + + # Remove and yield stable files + for path in to_yield: + self._tracked.pop(path, None) + yield path + + def has_pending_files(self) -> bool: + """Check if there are files waiting for stability check.""" + return len(self._tracked) > 0 + + @property + def pending_count(self) -> int: + """Number of files being tracked.""" + return len(self._tracked) + + +class ConsumerFilter(DefaultFilter): + """ + Filter for watchfiles that accepts only supported document types + and ignores system files/directories. + + Extends DefaultFilter leveraging its built-in filtering: + - `ignore_dirs`: Directory names to ignore (and all their contents) + - `ignore_entity_patterns`: Regex patterns matched against filename/dirname only + + We add custom logic for file extension filtering (only accept supported + document types), which the library doesn't provide. + """ + + # Regex patterns for files to always ignore (matched against filename only) + # These are passed to DefaultFilter.ignore_entity_patterns + DEFAULT_IGNORE_PATTERNS: Final[tuple[str, ...]] = ( + r"^\.DS_Store$", + r"^\.DS_STORE$", + r"^\._.*", + r"^desktop\.ini$", + r"^Thumbs\.db$", + ) + + # Directories to always ignore (passed to DefaultFilter.ignore_dirs) + # These are matched by directory name, not full path + DEFAULT_IGNORE_DIRS: Final[tuple[str, ...]] = ( + ".stfolder", # Syncthing + ".stversions", # Syncthing + ".localized", # macOS + "@eaDir", # Synology NAS + ".Spotlight-V100", # macOS + ".Trashes", # macOS + "__MACOSX", # macOS archive artifacts + ) + + def __init__( + self, + *, + supported_extensions: frozenset[str] | None = None, + ignore_patterns: list[str] | None = None, + ignore_dirs: list[str] | None = None, + ) -> None: + """ + Initialize the consumer filter. + + Args: + supported_extensions: Set of file extensions to accept (e.g., {".pdf", ".png"}). + If None, uses get_supported_file_extensions(). + ignore_patterns: Additional regex patterns to ignore (matched against filename). + ignore_dirs: Additional directory names to ignore (merged with defaults). + """ + # Get supported extensions + if supported_extensions is None: + supported_extensions = frozenset(get_supported_file_extensions()) + self._supported_extensions = supported_extensions + + # Combine default and user patterns + all_patterns: list[str] = list(self.DEFAULT_IGNORE_PATTERNS) + if ignore_patterns: + all_patterns.extend(ignore_patterns) + + # Combine default and user ignore_dirs + all_ignore_dirs: list[str] = list(self.DEFAULT_IGNORE_DIRS) + if ignore_dirs: + all_ignore_dirs.extend(ignore_dirs) + + # Let DefaultFilter handle all the pattern and directory filtering + super().__init__( + ignore_dirs=tuple(all_ignore_dirs), + ignore_entity_patterns=tuple(all_patterns), + ignore_paths=(), + ) + + def __call__(self, change: Change, path: str) -> bool: + """ + Filter function for watchfiles. + + Returns True if the path should be watched, False to ignore. + + The parent DefaultFilter handles: + - Hidden files/directories (starting with .) + - Directories in ignore_dirs + - Files/directories matching ignore_entity_patterns + + We additionally filter files by extension. + """ + # Let parent filter handle directory ignoring and pattern matching + if not super().__call__(change, path): + return False + + path_obj = Path(path) + + # For directories, parent filter already handled everything + if path_obj.is_dir(): + return True + + # For files, check extension + return self._has_supported_extension(path_obj) + + def _has_supported_extension(self, path: Path) -> bool: + """Check if the file has a supported extension.""" + suffix = path.suffix.lower() + return suffix in self._supported_extensions + + +def _tags_from_path(filepath: Path, consumption_dir: Path) -> list[int]: + """ + Walk up the directory tree from filepath to consumption_dir and get or create Tag IDs for every directory. - Returns set of Tag models + Returns list of Tag primary keys. """ db.close_old_connections() - tag_ids = set() - path_parts = filepath.relative_to(settings.CONSUMPTION_DIR).parent.parts + tag_ids: set[int] = set() + path_parts = filepath.relative_to(consumption_dir).parent.parts + for part in path_parts: - tag_ids.add( - Tag.objects.get_or_create(name__iexact=part, defaults={"name": part})[0].pk, + tag, _ = Tag.objects.get_or_create( + name__iexact=part, + defaults={"name": part}, ) + tag_ids.add(tag.pk) return list(tag_ids) -def _is_ignored(filepath: Path) -> bool: +def _consume_file( + filepath: Path, + consumption_dir: Path, + *, + subdirs_as_tags: bool, +) -> None: """ - Checks if the given file should be ignored, based on configured - patterns. + Queue a file for consumption. - Returns True if the file is ignored, False otherwise + Args: + filepath: Path to the file to consume. + consumption_dir: Base consumption directory. + subdirs_as_tags: Whether to create tags from subdirectory names. """ - # Trim out the consume directory, leaving only filename and it's - # path relative to the consume directory - filepath_relative = PurePath(filepath).relative_to(settings.CONSUMPTION_DIR) - - # March through the components of the path, including directories and the filename - # looking for anything matching - # foo/bar/baz/file.pdf -> (foo, bar, baz, file.pdf) - parts = [] - for part in filepath_relative.parts: - # If the part is not the name (ie, it's a dir) - # Need to append the trailing slash or fnmatch doesn't match - # fnmatch("dir", "dir/*") == False - # fnmatch("dir/", "dir/*") == True - if part != filepath_relative.name: - part = part + "/" - parts.append(part) - - for pattern in settings.CONSUMER_IGNORE_PATTERNS: - if len(filter(parts, pattern)): - return True - - return False - - -def _consume(filepath: Path) -> None: - # Check permissions early + # Verify file still exists and is accessible try: - filepath.stat() - except (PermissionError, OSError): - logger.warning(f"Not consuming file {filepath}: Permission denied.") + if not filepath.is_file(): + logger.debug(f"Not consuming {filepath}: not a file or doesn't exist") + return + except OSError as e: + logger.warning(f"Not consuming {filepath}: {e}") return - if filepath.is_dir() or _is_ignored(filepath): - return - - if not filepath.is_file(): - logger.debug(f"Not consuming file {filepath}: File has moved.") - return - - if not is_file_ext_supported(filepath.suffix): - logger.warning(f"Not consuming file {filepath}: Unknown file extension.") - return - - # Total wait time: up to 500ms - os_error_retry_count: Final[int] = 50 - os_error_retry_wait: Final[float] = 0.01 - - read_try_count = 0 - file_open_ok = False - os_error_str = None - - while (read_try_count < os_error_retry_count) and not file_open_ok: + # Get tags from path if configured + tag_ids: list[int] | None = None + if subdirs_as_tags: try: - with filepath.open("rb"): - file_open_ok = True - except OSError as e: - read_try_count += 1 - os_error_str = str(e) - sleep(os_error_retry_wait) + tag_ids = _tags_from_path(filepath, consumption_dir) + except Exception: + logger.exception(f"Error creating tags from path for {filepath}") - if read_try_count >= os_error_retry_count: - logger.warning(f"Not consuming file {filepath}: OS reports {os_error_str}") - return - - tag_ids = None + # Queue for consumption try: - if settings.CONSUMER_SUBDIRS_AS_TAGS: - tag_ids = _tags_from_path(filepath) - except Exception: - logger.exception("Error creating tags from path") - - try: - logger.info(f"Adding {filepath} to the task queue.") + logger.info(f"Adding {filepath} to the task queue") consume_file.delay( ConsumableDocument( source=DocumentSource.ConsumeFolder, @@ -138,228 +346,228 @@ def _consume(filepath: Path) -> None: DocumentMetadataOverrides(tag_ids=tag_ids), ) except Exception: - # Catch all so that the consumer won't crash. - # This is also what the test case is listening for to check for - # errors. - logger.exception("Error while consuming document") - - -def _consume_wait_unmodified(file: Path) -> None: - """ - Waits for the given file to appear unmodified based on file size - and modification time. Will wait a configured number of seconds - and retry a configured number of times before either consuming or - giving up - """ - if _is_ignored(file): - return - - logger.debug(f"Waiting for file {file} to remain unmodified") - mtime = -1 - size = -1 - current_try = 0 - while current_try < settings.CONSUMER_POLLING_RETRY_COUNT: - try: - stat_data = file.stat() - new_mtime = stat_data.st_mtime - new_size = stat_data.st_size - except FileNotFoundError: - logger.debug( - f"File {file} moved while waiting for it to remain unmodified.", - ) - return - if new_mtime == mtime and new_size == size: - _consume(file) - return - mtime = new_mtime - size = new_size - sleep(settings.CONSUMER_POLLING_DELAY) - current_try += 1 - - logger.error(f"Timeout while waiting on file {file} to remain unmodified.") - - -class Handler(FileSystemEventHandler): - def __init__(self, pool: ThreadPoolExecutor) -> None: - super().__init__() - self._pool = pool - - def on_created(self, event): - self._pool.submit(_consume_wait_unmodified, Path(event.src_path)) - - def on_moved(self, event): - self._pool.submit(_consume_wait_unmodified, Path(event.dest_path)) + logger.exception(f"Error while queuing document {filepath}") class Command(BaseCommand): """ - On every iteration of an infinite loop, consume what we can from the - consumption directory. + Watch a consumption directory and queue new documents for processing. + + Uses watchfiles for efficient file system monitoring. Supports both + native OS notifications (inotify on Linux, FSEvents on macOS) and + polling for network filesystems. """ - # This is here primarily for the tests and is irrelevant in production. - stop_flag = Event() - # Also only for testing, configures in one place the timeout used before checking - # the stop flag - testing_timeout_s: Final[float] = 0.5 - testing_timeout_ms: Final[float] = testing_timeout_s * 1000.0 + help = "Watch the consumption directory for new documents" - def add_arguments(self, parser): + # For testing - allows tests to stop the consumer + stop_flag: Event = Event() + + # Testing timeout in seconds + testing_timeout_s: Final[float] = 0.5 + + def add_arguments(self, parser) -> None: parser.add_argument( "directory", - default=settings.CONSUMPTION_DIR, + default=None, nargs="?", - help="The consumption directory.", + help="The consumption directory (defaults to CONSUMPTION_DIR setting)", + ) + parser.add_argument( + "--oneshot", + action="store_true", + help="Process existing files and exit without watching", ) - parser.add_argument("--oneshot", action="store_true", help="Run only once.") - - # Only use during unit testing, will configure a timeout - # Leaving it unset or false and the consumer will exit when it - # receives SIGINT parser.add_argument( "--testing", action="store_true", - help="Flag used only for unit testing", + help="Enable testing mode with shorter timeouts", default=False, ) - def handle(self, *args, **options): - directory = options["directory"] - recursive = settings.CONSUMER_RECURSIVE - + def handle(self, *args, **options) -> None: + # Resolve consumption directory + directory = options.get("directory") if not directory: - raise CommandError("CONSUMPTION_DIR does not appear to be set.") + directory = getattr(settings, "CONSUMPTION_DIR", None) + if not directory: + raise CommandError("CONSUMPTION_DIR is not configured") directory = Path(directory).resolve() - if not directory.is_dir(): - raise CommandError(f"Consumption directory {directory} does not exist") + if not directory.exists(): + raise CommandError(f"Consumption directory does not exist: {directory}") - # Consumer will need this + if not directory.is_dir(): + raise CommandError(f"Consumption path is not a directory: {directory}") + + # Ensure scratch directory exists settings.SCRATCH_DIR.mkdir(parents=True, exist_ok=True) - if recursive: - for dirpath, _, filenames in os.walk(directory): - for filename in filenames: - filepath = Path(dirpath) / filename - _consume(filepath) - else: - for filepath in directory.iterdir(): - _consume(filepath) + # Get settings + recursive: bool = settings.CONSUMER_RECURSIVE + subdirs_as_tags: bool = settings.CONSUMER_SUBDIRS_AS_TAGS + polling_interval: float = settings.CONSUMER_POLLING_INTERVAL + stability_delay: float = settings.CONSUMER_STABILITY_DELAY + ignore_patterns: list[str] = settings.CONSUMER_IGNORE_PATTERNS + ignore_dirs: list[str] = settings.CONSUMER_IGNORE_DIRS + is_testing: bool = options.get("testing", False) + is_oneshot: bool = options.get("oneshot", False) - if options["oneshot"]: + # Create filter + consumer_filter = ConsumerFilter( + ignore_patterns=ignore_patterns, + ignore_dirs=ignore_dirs, + ) + + # Process existing files + self._process_existing_files( + directory=directory, + recursive=recursive, + subdirs_as_tags=subdirs_as_tags, + consumer_filter=consumer_filter, + ) + + if is_oneshot: + logger.info("Oneshot mode: processed existing files, exiting") return - if settings.CONSUMER_POLLING == 0 and INotify: - self.handle_inotify(directory, recursive, is_testing=options["testing"]) + # Start watching + self._watch_directory( + directory=directory, + recursive=recursive, + subdirs_as_tags=subdirs_as_tags, + consumer_filter=consumer_filter, + polling_interval=polling_interval, + stability_delay=stability_delay, + is_testing=is_testing, + ) + + logger.debug("Consumer exiting") + + def _process_existing_files( + self, + *, + directory: Path, + recursive: bool, + subdirs_as_tags: bool, + consumer_filter: ConsumerFilter, + ) -> None: + """Process any existing files in the consumption directory.""" + logger.info(f"Processing existing files in {directory}") + + glob_pattern = "**/*" if recursive else "*" + + for filepath in directory.glob(glob_pattern): + # Use filter to check if file should be processed + if not filepath.is_file(): + continue + + if not consumer_filter(Change.added, str(filepath)): + continue + + _consume_file( + filepath=filepath, + consumption_dir=directory, + subdirs_as_tags=subdirs_as_tags, + ) + + def _watch_directory( + self, + *, + directory: Path, + recursive: bool, + subdirs_as_tags: bool, + consumer_filter: ConsumerFilter, + polling_interval: float, + stability_delay: float, + is_testing: bool, + ) -> None: + """Watch directory for changes and process stable files.""" + use_polling = polling_interval > 0 + poll_delay_ms = int(polling_interval * 1000) if use_polling else 0 + + if use_polling: + logger.info( + f"Watching {directory} using polling (interval: {polling_interval}s)", + ) else: - if INotify is None and settings.CONSUMER_POLLING == 0: # pragma: no cover - logger.warning("Using polling as INotify import failed") - self.handle_polling(directory, recursive, is_testing=options["testing"]) + logger.info(f"Watching {directory} using native file system events") - logger.debug("Consumer exiting.") + # Create stability tracker + tracker = FileStabilityTracker(stability_delay=stability_delay) - def handle_polling(self, directory, recursive, *, is_testing: bool): - logger.info(f"Polling directory for changes: {directory}") + # Calculate timeouts + stability_timeout_ms = int(stability_delay * 1000) + testing_timeout_ms = int(self.testing_timeout_s * 1000) - timeout = None + # Calculate appropriate timeout for watch loop + # In polling mode, rust_timeout must be significantly longer than poll_delay_ms + # to ensure poll cycles can complete before timing out if is_testing: - timeout = self.testing_timeout_s - logger.debug(f"Configuring timeout to {timeout}s") + if use_polling: + # For polling: timeout must be at least 3x the poll interval to allow + # multiple poll cycles. This prevents timeouts from interfering with + # the polling mechanism. + min_polling_timeout_ms = poll_delay_ms * 3 + timeout_ms = max(min_polling_timeout_ms, testing_timeout_ms) + else: + # For native watching, use short timeout to check stop flag + timeout_ms = testing_timeout_ms + else: + # Not testing, wait indefinitely for first event + timeout_ms = 0 - polling_interval = settings.CONSUMER_POLLING - if polling_interval == 0: # pragma: no cover - # Only happens if INotify failed to import - logger.warning("Using polling of 10s, consider setting this") - polling_interval = 10 + self.stop_flag.clear() - with ThreadPoolExecutor(max_workers=4) as pool: - observer = PollingObserver(timeout=polling_interval) - observer.schedule(Handler(pool), directory, recursive=recursive) - observer.start() + while not self.stop_flag.is_set(): try: - while observer.is_alive(): - observer.join(timeout) - if self.stop_flag.is_set(): - observer.stop() - except KeyboardInterrupt: - observer.stop() - observer.join() - - def handle_inotify(self, directory, recursive, *, is_testing: bool): - logger.info(f"Using inotify to watch directory for changes: {directory}") - - timeout_ms = None - if is_testing: - timeout_ms = self.testing_timeout_ms - logger.debug(f"Configuring timeout to {timeout_ms}ms") - - inotify = INotify() - inotify_flags = flags.CLOSE_WRITE | flags.MOVED_TO | flags.MODIFY - if recursive: - inotify.add_watch_recursive(directory, inotify_flags) - else: - inotify.add_watch(directory, inotify_flags) - - inotify_debounce_secs: Final[float] = settings.CONSUMER_INOTIFY_DELAY - inotify_debounce_ms: Final[int] = inotify_debounce_secs * 1000 - - finished = False - - notified_files = {} - - try: - while not finished: - try: - for event in inotify.read(timeout=timeout_ms): - path = inotify.get_path(event.wd) if recursive else directory - filepath = Path(path) / event.name - if flags.MODIFY in flags.from_mask(event.mask): - notified_files.pop(filepath, None) - else: - notified_files[filepath] = monotonic() - - # Check the files against the timeout - still_waiting = {} - # last_event_time is time of the last inotify event for this file - for filepath, last_event_time in notified_files.items(): - # Current time - last time over the configured timeout - waited_long_enough = ( - monotonic() - last_event_time - ) > inotify_debounce_secs - - # Also make sure the file exists still, some scanners might write a - # temporary file first - try: - file_still_exists = filepath.exists() and filepath.is_file() - except (PermissionError, OSError): # pragma: no cover - # If we can't check, let it fail in the _consume function - file_still_exists = True + for changes in watch( + directory, + watch_filter=consumer_filter, + rust_timeout=timeout_ms, + yield_on_timeout=True, + force_polling=use_polling, + poll_delay_ms=poll_delay_ms, + recursive=recursive, + stop_event=self.stop_flag, + ): + # Process each change + for change_type, path in changes: + path = Path(path).resolve() + if not path.is_file(): continue + logger.debug(f"Event: {change_type.name} for {path}") + tracker.track(path, change_type) - if waited_long_enough and file_still_exists: - _consume(filepath) - elif file_still_exists: - still_waiting[filepath] = last_event_time + # Check for stable files + for stable_path in tracker.get_stable_files(): + _consume_file( + filepath=stable_path, + consumption_dir=directory, + subdirs_as_tags=subdirs_as_tags, + ) - # These files are still waiting to hit the timeout - notified_files = still_waiting + # Exit watch loop to reconfigure timeout + break - # If files are waiting, need to exit read() to check them - # Otherwise, go back to infinite sleep time, but only if not testing - if len(notified_files) > 0: - timeout_ms = inotify_debounce_ms - elif is_testing: - timeout_ms = self.testing_timeout_ms + # Determine next timeout + if tracker.has_pending_files(): + # Check pending files at stability interval + timeout_ms = stability_timeout_ms + elif is_testing: + # In testing, use appropriate timeout based on watch mode + if use_polling: + # For polling: ensure timeout allows polls to complete + min_polling_timeout_ms = poll_delay_ms * 3 + timeout_ms = max(min_polling_timeout_ms, testing_timeout_ms) else: - timeout_ms = None + # For native watching, use short timeout to check stop flag + timeout_ms = testing_timeout_ms + else: # pragma: nocover + # No pending files, wait indefinitely + timeout_ms = 0 - if self.stop_flag.is_set(): - logger.debug("Finishing because event is set") - finished = True - - except KeyboardInterrupt: - logger.info("Received SIGINT, stopping inotify") - finished = True - finally: - inotify.close() + except KeyboardInterrupt: # pragma: nocover + logger.info("Received interrupt, stopping consumer") + self.stop_flag.set() diff --git a/src/documents/management/commands/document_exporter.py b/src/documents/management/commands/document_exporter.py index 88daeddf5..77b3b6416 100644 --- a/src/documents/management/commands/document_exporter.py +++ b/src/documents/management/commands/document_exporter.py @@ -3,7 +3,6 @@ import json import os import shutil import tempfile -import time from pathlib import Path from typing import TYPE_CHECKING @@ -56,7 +55,6 @@ from documents.settings import EXPORTER_FILE_NAME from documents.settings import EXPORTER_THUMBNAIL_NAME from documents.utils import copy_file_with_basic_stats from paperless import version -from paperless.db import GnuPG from paperless.models import ApplicationConfiguration from paperless_mail.models import MailAccount from paperless_mail.models import MailRule @@ -316,20 +314,17 @@ class Command(CryptMixin, BaseCommand): total=len(document_manifest), disable=self.no_progress_bar, ): - # 3.1. store files unencrypted - document_dict["fields"]["storage_type"] = Document.STORAGE_TYPE_UNENCRYPTED - document = document_map[document_dict["pk"]] - # 3.2. generate a unique filename + # 3.1. generate a unique filename base_name = self.generate_base_name(document) - # 3.3. write filenames into manifest + # 3.2. write filenames into manifest original_target, thumbnail_target, archive_target = ( self.generate_document_targets(document, base_name, document_dict) ) - # 3.4. write files to target folder + # 3.3. write files to target folder if not self.data_only: self.copy_document_files( document, @@ -423,7 +418,6 @@ class Command(CryptMixin, BaseCommand): base_name = generate_filename( document, counter=filename_counter, - append_gpg=False, ) else: base_name = document.get_public_filename(counter=filename_counter) @@ -482,46 +476,24 @@ class Command(CryptMixin, BaseCommand): If the document is encrypted, the files are decrypted before copying them to the target location. """ - if document.storage_type == Document.STORAGE_TYPE_GPG: - t = int(time.mktime(document.created.timetuple())) + self.check_and_copy( + document.source_path, + document.checksum, + original_target, + ) - original_target.parent.mkdir(parents=True, exist_ok=True) - with document.source_file as out_file: - original_target.write_bytes(GnuPG.decrypted(out_file)) - os.utime(original_target, times=(t, t)) + if thumbnail_target: + self.check_and_copy(document.thumbnail_path, None, thumbnail_target) - if thumbnail_target: - thumbnail_target.parent.mkdir(parents=True, exist_ok=True) - with document.thumbnail_file as out_file: - thumbnail_target.write_bytes(GnuPG.decrypted(out_file)) - os.utime(thumbnail_target, times=(t, t)) - - if archive_target: - archive_target.parent.mkdir(parents=True, exist_ok=True) - if TYPE_CHECKING: - assert isinstance(document.archive_path, Path) - with document.archive_path as out_file: - archive_target.write_bytes(GnuPG.decrypted(out_file)) - os.utime(archive_target, times=(t, t)) - else: + if archive_target: + if TYPE_CHECKING: + assert isinstance(document.archive_path, Path) self.check_and_copy( - document.source_path, - document.checksum, - original_target, + document.archive_path, + document.archive_checksum, + archive_target, ) - if thumbnail_target: - self.check_and_copy(document.thumbnail_path, None, thumbnail_target) - - if archive_target: - if TYPE_CHECKING: - assert isinstance(document.archive_path, Path) - self.check_and_copy( - document.archive_path, - document.archive_checksum, - archive_target, - ) - def check_and_write_json( self, content: list[dict] | dict, diff --git a/src/documents/management/commands/document_importer.py b/src/documents/management/commands/document_importer.py index 3e614c6a6..ba3d793b3 100644 --- a/src/documents/management/commands/document_importer.py +++ b/src/documents/management/commands/document_importer.py @@ -383,8 +383,6 @@ class Command(CryptMixin, BaseCommand): else: archive_path = None - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED - with FileLock(settings.MEDIA_LOCK): if Path(document.source_path).is_file(): raise FileExistsError(document.source_path) diff --git a/src/documents/management/commands/document_llmindex.py b/src/documents/management/commands/document_llmindex.py new file mode 100644 index 000000000..d2df02ed9 --- /dev/null +++ b/src/documents/management/commands/document_llmindex.py @@ -0,0 +1,22 @@ +from django.core.management import BaseCommand +from django.db import transaction + +from documents.management.commands.mixins import ProgressBarMixin +from documents.tasks import llmindex_index + + +class Command(ProgressBarMixin, BaseCommand): + help = "Manages the LLM-based vector index for Paperless." + + def add_arguments(self, parser): + parser.add_argument("command", choices=["rebuild", "update"]) + self.add_argument_progress_bar_mixin(parser) + + def handle(self, *args, **options): + self.handle_progress_bar_mixin(**options) + with transaction.atomic(): + llmindex_index( + progress_bar_disable=self.no_progress_bar, + rebuild=options["command"] == "rebuild", + scheduled=False, + ) diff --git a/src/documents/matching.py b/src/documents/matching.py index 198ead64c..9276ad583 100644 --- a/src/documents/matching.py +++ b/src/documents/matching.py @@ -403,6 +403,18 @@ def existing_document_matches_workflow( f"Document tags {list(document.tags.all())} include excluded tags {list(trigger_has_not_tags_qs)}", ) + allowed_correspondent_ids = set( + trigger.filter_has_any_correspondents.values_list("id", flat=True), + ) + if ( + allowed_correspondent_ids + and document.correspondent_id not in allowed_correspondent_ids + ): + return ( + False, + f"Document correspondent {document.correspondent} is not one of {list(trigger.filter_has_any_correspondents.all())}", + ) + # Document correspondent vs trigger has_correspondent if ( trigger.filter_has_correspondent_id is not None @@ -424,6 +436,17 @@ def existing_document_matches_workflow( f"Document correspondent {document.correspondent} is excluded by {list(trigger.filter_has_not_correspondents.all())}", ) + allowed_document_type_ids = set( + trigger.filter_has_any_document_types.values_list("id", flat=True), + ) + if allowed_document_type_ids and ( + document.document_type_id not in allowed_document_type_ids + ): + return ( + False, + f"Document doc type {document.document_type} is not one of {list(trigger.filter_has_any_document_types.all())}", + ) + # Document document_type vs trigger has_document_type if ( trigger.filter_has_document_type_id is not None @@ -445,6 +468,17 @@ def existing_document_matches_workflow( f"Document doc type {document.document_type} is excluded by {list(trigger.filter_has_not_document_types.all())}", ) + allowed_storage_path_ids = set( + trigger.filter_has_any_storage_paths.values_list("id", flat=True), + ) + if allowed_storage_path_ids and ( + document.storage_path_id not in allowed_storage_path_ids + ): + return ( + False, + f"Document storage path {document.storage_path} is not one of {list(trigger.filter_has_any_storage_paths.all())}", + ) + # Document storage_path vs trigger has_storage_path if ( trigger.filter_has_storage_path_id is not None @@ -532,6 +566,10 @@ def prefilter_documents_by_workflowtrigger( # Correspondent, DocumentType, etc. filtering + if trigger.filter_has_any_correspondents.exists(): + documents = documents.filter( + correspondent__in=trigger.filter_has_any_correspondents.all(), + ) if trigger.filter_has_correspondent is not None: documents = documents.filter( correspondent=trigger.filter_has_correspondent, @@ -541,6 +579,10 @@ def prefilter_documents_by_workflowtrigger( correspondent__in=trigger.filter_has_not_correspondents.all(), ) + if trigger.filter_has_any_document_types.exists(): + documents = documents.filter( + document_type__in=trigger.filter_has_any_document_types.all(), + ) if trigger.filter_has_document_type is not None: documents = documents.filter( document_type=trigger.filter_has_document_type, @@ -550,6 +592,10 @@ def prefilter_documents_by_workflowtrigger( document_type__in=trigger.filter_has_not_document_types.all(), ) + if trigger.filter_has_any_storage_paths.exists(): + documents = documents.filter( + storage_path__in=trigger.filter_has_any_storage_paths.all(), + ) if trigger.filter_has_storage_path is not None: documents = documents.filter( storage_path=trigger.filter_has_storage_path, @@ -604,8 +650,11 @@ def document_matches_workflow( "filter_has_tags", "filter_has_all_tags", "filter_has_not_tags", + "filter_has_any_document_types", "filter_has_not_document_types", + "filter_has_any_correspondents", "filter_has_not_correspondents", + "filter_has_any_storage_paths", "filter_has_not_storage_paths", ) ) diff --git a/src/documents/migrations/0001_initial.py b/src/documents/migrations/0001_initial.py index 89c9e29df..d68a5934b 100644 --- a/src/documents/migrations/0001_initial.py +++ b/src/documents/migrations/0001_initial.py @@ -1,5 +1,13 @@ -# Generated by Django 1.9 on 2015-12-20 19:10 +# Generated by Django 5.2.7 on 2026-01-15 22:08 +import datetime + +import django.core.validators +import django.db.models.deletion +import django.db.models.functions.comparison +import django.db.models.functions.text +import django.utils.timezone +import multiselectfield.db.fields from django.conf import settings from django.db import migrations from django.db import models @@ -8,9 +16,554 @@ from django.db import models class Migration(migrations.Migration): initial = True - dependencies = [] + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] operations = [ + migrations.CreateModel( + name="WorkflowActionEmail", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "subject", + models.CharField( + help_text="The subject of the email, can include some placeholders, see documentation.", + max_length=256, + verbose_name="email subject", + ), + ), + ( + "body", + models.TextField( + help_text="The body (message) of the email, can include some placeholders, see documentation.", + verbose_name="email body", + ), + ), + ( + "to", + models.TextField( + help_text="The destination email addresses, comma separated.", + verbose_name="emails to", + ), + ), + ( + "include_document", + models.BooleanField( + default=False, + verbose_name="include document in email", + ), + ), + ], + ), + migrations.CreateModel( + name="WorkflowActionWebhook", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "url", + models.CharField( + help_text="The destination URL for the notification.", + max_length=256, + verbose_name="webhook url", + ), + ), + ( + "use_params", + models.BooleanField(default=True, verbose_name="use parameters"), + ), + ( + "as_json", + models.BooleanField(default=False, verbose_name="send as JSON"), + ), + ( + "params", + models.JSONField( + blank=True, + help_text="The parameters to send with the webhook URL if body not used.", + null=True, + verbose_name="webhook parameters", + ), + ), + ( + "body", + models.TextField( + blank=True, + help_text="The body to send with the webhook URL if parameters not used.", + null=True, + verbose_name="webhook body", + ), + ), + ( + "headers", + models.JSONField( + blank=True, + help_text="The headers to send with the webhook URL.", + null=True, + verbose_name="webhook headers", + ), + ), + ( + "include_document", + models.BooleanField( + default=False, + verbose_name="include document in webhook", + ), + ), + ], + ), + migrations.CreateModel( + name="Correspondent", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=128, verbose_name="name")), + ( + "match", + models.CharField(blank=True, max_length=256, verbose_name="match"), + ), + ( + "matching_algorithm", + models.PositiveIntegerField( + choices=[ + (0, "None"), + (1, "Any word"), + (2, "All words"), + (3, "Exact match"), + (4, "Regular expression"), + (5, "Fuzzy word"), + (6, "Automatic"), + ], + default=1, + verbose_name="matching algorithm", + ), + ), + ( + "is_insensitive", + models.BooleanField(default=True, verbose_name="is insensitive"), + ), + ( + "owner", + models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + verbose_name="owner", + ), + ), + ], + options={ + "verbose_name": "correspondent", + "verbose_name_plural": "correspondents", + "ordering": ("name",), + "abstract": False, + }, + ), + migrations.CreateModel( + name="CustomField", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "created", + models.DateTimeField( + db_index=True, + default=django.utils.timezone.now, + editable=False, + verbose_name="created", + ), + ), + ("name", models.CharField(max_length=128)), + ( + "data_type", + models.CharField( + choices=[ + ("string", "String"), + ("url", "URL"), + ("date", "Date"), + ("boolean", "Boolean"), + ("integer", "Integer"), + ("float", "Float"), + ("monetary", "Monetary"), + ("documentlink", "Document Link"), + ("select", "Select"), + ("longtext", "Long Text"), + ], + editable=False, + max_length=50, + verbose_name="data type", + ), + ), + ( + "extra_data", + models.JSONField( + blank=True, + help_text="Extra data for the custom field, such as select options", + null=True, + verbose_name="extra data", + ), + ), + ], + options={ + "verbose_name": "custom field", + "verbose_name_plural": "custom fields", + "ordering": ("created",), + "constraints": [ + models.UniqueConstraint( + fields=("name",), + name="documents_customfield_unique_name", + ), + ], + }, + ), + migrations.CreateModel( + name="DocumentType", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=128, verbose_name="name")), + ( + "match", + models.CharField(blank=True, max_length=256, verbose_name="match"), + ), + ( + "matching_algorithm", + models.PositiveIntegerField( + choices=[ + (0, "None"), + (1, "Any word"), + (2, "All words"), + (3, "Exact match"), + (4, "Regular expression"), + (5, "Fuzzy word"), + (6, "Automatic"), + ], + default=1, + verbose_name="matching algorithm", + ), + ), + ( + "is_insensitive", + models.BooleanField(default=True, verbose_name="is insensitive"), + ), + ( + "owner", + models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + verbose_name="owner", + ), + ), + ], + options={ + "verbose_name": "document type", + "verbose_name_plural": "document types", + "ordering": ("name",), + "abstract": False, + }, + ), + migrations.CreateModel( + name="StoragePath", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=128, verbose_name="name")), + ( + "match", + models.CharField(blank=True, max_length=256, verbose_name="match"), + ), + ( + "matching_algorithm", + models.PositiveIntegerField( + choices=[ + (0, "None"), + (1, "Any word"), + (2, "All words"), + (3, "Exact match"), + (4, "Regular expression"), + (5, "Fuzzy word"), + (6, "Automatic"), + ], + default=1, + verbose_name="matching algorithm", + ), + ), + ( + "is_insensitive", + models.BooleanField(default=True, verbose_name="is insensitive"), + ), + ("path", models.TextField(verbose_name="path")), + ( + "owner", + models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + verbose_name="owner", + ), + ), + ], + options={ + "verbose_name": "storage path", + "verbose_name_plural": "storage paths", + "ordering": ("name",), + "abstract": False, + }, + ), + migrations.CreateModel( + name="Tag", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "tn_ancestors_pks", + models.TextField( + blank=True, + default="", + editable=False, + verbose_name="Ancestors pks", + ), + ), + ( + "tn_ancestors_count", + models.PositiveIntegerField( + default=0, + editable=False, + verbose_name="Ancestors count", + ), + ), + ( + "tn_children_pks", + models.TextField( + blank=True, + default="", + editable=False, + verbose_name="Children pks", + ), + ), + ( + "tn_children_count", + models.PositiveIntegerField( + default=0, + editable=False, + verbose_name="Children count", + ), + ), + ( + "tn_depth", + models.PositiveIntegerField( + default=0, + editable=False, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(10), + ], + verbose_name="Depth", + ), + ), + ( + "tn_descendants_pks", + models.TextField( + blank=True, + default="", + editable=False, + verbose_name="Descendants pks", + ), + ), + ( + "tn_descendants_count", + models.PositiveIntegerField( + default=0, + editable=False, + verbose_name="Descendants count", + ), + ), + ( + "tn_index", + models.PositiveIntegerField( + default=0, + editable=False, + verbose_name="Index", + ), + ), + ( + "tn_level", + models.PositiveIntegerField( + default=1, + editable=False, + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(10), + ], + verbose_name="Level", + ), + ), + ( + "tn_priority", + models.PositiveIntegerField( + default=0, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(9999999999), + ], + verbose_name="Priority", + ), + ), + ( + "tn_order", + models.PositiveIntegerField( + default=0, + editable=False, + verbose_name="Order", + ), + ), + ( + "tn_siblings_pks", + models.TextField( + blank=True, + default="", + editable=False, + verbose_name="Siblings pks", + ), + ), + ( + "tn_siblings_count", + models.PositiveIntegerField( + default=0, + editable=False, + verbose_name="Siblings count", + ), + ), + ("name", models.CharField(max_length=128, verbose_name="name")), + ( + "match", + models.CharField(blank=True, max_length=256, verbose_name="match"), + ), + ( + "matching_algorithm", + models.PositiveIntegerField( + choices=[ + (0, "None"), + (1, "Any word"), + (2, "All words"), + (3, "Exact match"), + (4, "Regular expression"), + (5, "Fuzzy word"), + (6, "Automatic"), + ], + default=1, + verbose_name="matching algorithm", + ), + ), + ( + "is_insensitive", + models.BooleanField(default=True, verbose_name="is insensitive"), + ), + ( + "color", + models.CharField( + default="#a6cee3", + max_length=7, + verbose_name="color", + ), + ), + ( + "is_inbox_tag", + models.BooleanField( + default=False, + help_text="Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags.", + verbose_name="is inbox tag", + ), + ), + ( + "owner", + models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + verbose_name="owner", + ), + ), + ( + "tn_parent", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="tn_children", + to="documents.tag", + verbose_name="Parent", + ), + ), + ], + options={ + "verbose_name": "tag", + "verbose_name_plural": "tags", + "ordering": ("name",), + "abstract": False, + }, + ), migrations.CreateModel( name="Document", fields=[ @@ -23,18 +576,1397 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), - ("sender", models.CharField(blank=True, db_index=True, max_length=128)), - ("title", models.CharField(blank=True, db_index=True, max_length=128)), + ("deleted_at", models.DateTimeField(blank=True, null=True)), + ("restored_at", models.DateTimeField(blank=True, null=True)), + ("transaction_id", models.UUIDField(blank=True, null=True)), + ( + "title", + models.CharField( + blank=True, + db_index=True, + max_length=128, + verbose_name="title", + ), + ), ( "content", models.TextField( - db_index=( - "mysql" not in settings.DATABASES["default"]["ENGINE"] + blank=True, + help_text="The raw, text-only data of the document. This field is primarily used for searching.", + verbose_name="content", + ), + ), + ( + "mime_type", + models.CharField( + editable=False, + max_length=256, + verbose_name="mime type", + ), + ), + ( + "checksum", + models.CharField( + editable=False, + help_text="The checksum of the original document.", + max_length=32, + unique=True, + verbose_name="checksum", + ), + ), + ( + "archive_checksum", + models.CharField( + blank=True, + editable=False, + help_text="The checksum of the archived document.", + max_length=32, + null=True, + verbose_name="archive checksum", + ), + ), + ( + "page_count", + models.PositiveIntegerField( + help_text="The number of pages of the document.", + null=True, + validators=[django.core.validators.MinValueValidator(1)], + verbose_name="page count", + ), + ), + ( + "created", + models.DateField( + db_index=True, + default=datetime.date.today, + verbose_name="created", + ), + ), + ( + "modified", + models.DateTimeField( + auto_now=True, + db_index=True, + verbose_name="modified", + ), + ), + ( + "storage_type", + models.CharField( + choices=[ + ("unencrypted", "Unencrypted"), + ("gpg", "Encrypted with GNU Privacy Guard"), + ], + default="unencrypted", + editable=False, + max_length=11, + verbose_name="storage type", + ), + ), + ( + "added", + models.DateTimeField( + db_index=True, + default=django.utils.timezone.now, + editable=False, + verbose_name="added", + ), + ), + ( + "filename", + models.FilePathField( + default=None, + editable=False, + help_text="Current filename in storage", + max_length=1024, + null=True, + unique=True, + verbose_name="filename", + ), + ), + ( + "archive_filename", + models.FilePathField( + default=None, + editable=False, + help_text="Current archive filename in storage", + max_length=1024, + null=True, + unique=True, + verbose_name="archive filename", + ), + ), + ( + "original_filename", + models.CharField( + default=None, + editable=False, + help_text="The original name of the file when it was uploaded", + max_length=1024, + null=True, + verbose_name="original filename", + ), + ), + ( + "archive_serial_number", + models.PositiveIntegerField( + blank=True, + db_index=True, + help_text="The position of this document in your physical document archive.", + null=True, + unique=True, + validators=[ + django.core.validators.MaxValueValidator(4294967295), + django.core.validators.MinValueValidator(0), + ], + verbose_name="archive serial number", + ), + ), + ( + "correspondent", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="documents", + to="documents.correspondent", + verbose_name="correspondent", + ), + ), + ( + "owner", + models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + verbose_name="owner", + ), + ), + ( + "document_type", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="documents", + to="documents.documenttype", + verbose_name="document type", + ), + ), + ( + "storage_path", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="documents", + to="documents.storagepath", + verbose_name="storage path", + ), + ), + ( + "tags", + models.ManyToManyField( + blank=True, + related_name="documents", + to="documents.tag", + verbose_name="tags", + ), + ), + ], + options={ + "verbose_name": "document", + "verbose_name_plural": "documents", + "ordering": ("-created",), + }, + ), + migrations.CreateModel( + name="CustomFieldInstance", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("deleted_at", models.DateTimeField(blank=True, null=True)), + ("restored_at", models.DateTimeField(blank=True, null=True)), + ("transaction_id", models.UUIDField(blank=True, null=True)), + ( + "created", + models.DateTimeField( + db_index=True, + default=django.utils.timezone.now, + editable=False, + verbose_name="created", + ), + ), + ("value_text", models.CharField(max_length=128, null=True)), + ("value_bool", models.BooleanField(null=True)), + ("value_url", models.URLField(null=True)), + ("value_date", models.DateField(null=True)), + ("value_int", models.IntegerField(null=True)), + ("value_float", models.FloatField(null=True)), + ("value_monetary", models.CharField(max_length=128, null=True)), + ( + "value_monetary_amount", + models.GeneratedField( + db_persist=True, + expression=models.Case( + models.When( + then=django.db.models.functions.comparison.Cast( + django.db.models.functions.text.Substr( + "value_monetary", + 1, + ), + output_field=models.DecimalField( + decimal_places=2, + max_digits=65, + ), + ), + value_monetary__regex="^\\d+", + ), + default=django.db.models.functions.comparison.Cast( + django.db.models.functions.text.Substr( + "value_monetary", + 4, + ), + output_field=models.DecimalField( + decimal_places=2, + max_digits=65, + ), + ), + output_field=models.DecimalField( + decimal_places=2, + max_digits=65, + ), + ), + output_field=models.DecimalField( + decimal_places=2, + max_digits=65, ), ), ), - ("created", models.DateTimeField(auto_now_add=True)), - ("modified", models.DateTimeField(auto_now=True)), + ("value_document_ids", models.JSONField(null=True)), + ("value_select", models.CharField(max_length=16, null=True)), + ("value_long_text", models.TextField(null=True)), + ( + "field", + models.ForeignKey( + editable=False, + on_delete=django.db.models.deletion.CASCADE, + related_name="fields", + to="documents.customfield", + ), + ), + ( + "document", + models.ForeignKey( + editable=False, + on_delete=django.db.models.deletion.CASCADE, + related_name="custom_fields", + to="documents.document", + ), + ), + ], + options={ + "verbose_name": "custom field instance", + "verbose_name_plural": "custom field instances", + "ordering": ("created",), + }, + ), + migrations.CreateModel( + name="Note", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("deleted_at", models.DateTimeField(blank=True, null=True)), + ("restored_at", models.DateTimeField(blank=True, null=True)), + ("transaction_id", models.UUIDField(blank=True, null=True)), + ( + "note", + models.TextField( + blank=True, + help_text="Note for the document", + verbose_name="content", + ), + ), + ( + "created", + models.DateTimeField( + db_index=True, + default=django.utils.timezone.now, + verbose_name="created", + ), + ), + ( + "document", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="notes", + to="documents.document", + verbose_name="document", + ), + ), + ( + "user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="notes", + to=settings.AUTH_USER_MODEL, + verbose_name="user", + ), + ), + ], + options={ + "verbose_name": "note", + "verbose_name_plural": "notes", + "ordering": ("created",), + }, + ), + migrations.CreateModel( + name="PaperlessTask", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "task_id", + models.CharField( + help_text="Celery ID for the Task that was run", + max_length=255, + unique=True, + verbose_name="Task ID", + ), + ), + ( + "acknowledged", + models.BooleanField( + default=False, + help_text="If the task is acknowledged via the frontend or API", + verbose_name="Acknowledged", + ), + ), + ( + "task_file_name", + models.CharField( + help_text="Name of the file which the Task was run for", + max_length=255, + null=True, + verbose_name="Task Filename", + ), + ), + ( + "task_name", + models.CharField( + choices=[ + ("consume_file", "Consume File"), + ("train_classifier", "Train Classifier"), + ("check_sanity", "Check Sanity"), + ("index_optimize", "Index Optimize"), + ("llmindex_update", "LLM Index Update"), + ], + help_text="Name of the task that was run", + max_length=255, + null=True, + verbose_name="Task Name", + ), + ), + ( + "status", + models.CharField( + choices=[ + ("FAILURE", "FAILURE"), + ("PENDING", "PENDING"), + ("RECEIVED", "RECEIVED"), + ("RETRY", "RETRY"), + ("REVOKED", "REVOKED"), + ("STARTED", "STARTED"), + ("SUCCESS", "SUCCESS"), + ], + default="PENDING", + help_text="Current state of the task being run", + max_length=30, + verbose_name="Task State", + ), + ), + ( + "date_created", + models.DateTimeField( + default=django.utils.timezone.now, + help_text="Datetime field when the task result was created in UTC", + null=True, + verbose_name="Created DateTime", + ), + ), + ( + "date_started", + models.DateTimeField( + default=None, + help_text="Datetime field when the task was started in UTC", + null=True, + verbose_name="Started DateTime", + ), + ), + ( + "date_done", + models.DateTimeField( + default=None, + help_text="Datetime field when the task was completed in UTC", + null=True, + verbose_name="Completed DateTime", + ), + ), + ( + "result", + models.TextField( + default=None, + help_text="The data returned by the task", + null=True, + verbose_name="Result Data", + ), + ), + ( + "type", + models.CharField( + choices=[ + ("auto_task", "Auto Task"), + ("scheduled_task", "Scheduled Task"), + ("manual_task", "Manual Task"), + ], + default="auto_task", + help_text="The type of task that was run", + max_length=30, + verbose_name="Task Type", + ), + ), + ( + "owner", + models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + verbose_name="owner", + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="SavedView", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=128, verbose_name="name")), + ( + "show_on_dashboard", + models.BooleanField(verbose_name="show on dashboard"), + ), + ( + "show_in_sidebar", + models.BooleanField(verbose_name="show in sidebar"), + ), + ( + "sort_field", + models.CharField( + blank=True, + max_length=128, + null=True, + verbose_name="sort field", + ), + ), + ( + "sort_reverse", + models.BooleanField(default=False, verbose_name="sort reverse"), + ), + ( + "page_size", + models.PositiveIntegerField( + blank=True, + null=True, + validators=[django.core.validators.MinValueValidator(1)], + verbose_name="View page size", + ), + ), + ( + "display_mode", + models.CharField( + blank=True, + choices=[ + ("table", "Table"), + ("smallCards", "Small Cards"), + ("largeCards", "Large Cards"), + ], + max_length=128, + null=True, + verbose_name="View display mode", + ), + ), + ( + "display_fields", + models.JSONField( + blank=True, + null=True, + verbose_name="Document display fields", + ), + ), + ( + "owner", + models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + verbose_name="owner", + ), + ), + ], + options={ + "verbose_name": "saved view", + "verbose_name_plural": "saved views", + "ordering": ("name",), + }, + ), + migrations.CreateModel( + name="SavedViewFilterRule", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "rule_type", + models.PositiveIntegerField( + choices=[ + (0, "title contains"), + (1, "content contains"), + (2, "ASN is"), + (3, "correspondent is"), + (4, "document type is"), + (5, "is in inbox"), + (6, "has tag"), + (7, "has any tag"), + (8, "created before"), + (9, "created after"), + (10, "created year is"), + (11, "created month is"), + (12, "created day is"), + (13, "added before"), + (14, "added after"), + (15, "modified before"), + (16, "modified after"), + (17, "does not have tag"), + (18, "does not have ASN"), + (19, "title or content contains"), + (20, "fulltext query"), + (21, "more like this"), + (22, "has tags in"), + (23, "ASN greater than"), + (24, "ASN less than"), + (25, "storage path is"), + (26, "has correspondent in"), + (27, "does not have correspondent in"), + (28, "has document type in"), + (29, "does not have document type in"), + (30, "has storage path in"), + (31, "does not have storage path in"), + (32, "owner is"), + (33, "has owner in"), + (34, "does not have owner"), + (35, "does not have owner in"), + (36, "has custom field value"), + (37, "is shared by me"), + (38, "has custom fields"), + (39, "has custom field in"), + (40, "does not have custom field in"), + (41, "does not have custom field"), + (42, "custom fields query"), + (43, "created to"), + (44, "created from"), + (45, "added to"), + (46, "added from"), + (47, "mime type is"), + ], + verbose_name="rule type", + ), + ), + ( + "value", + models.CharField( + blank=True, + max_length=255, + null=True, + verbose_name="value", + ), + ), + ( + "saved_view", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="filter_rules", + to="documents.savedview", + verbose_name="saved view", + ), + ), + ], + options={ + "verbose_name": "filter rule", + "verbose_name_plural": "filter rules", + }, + ), + migrations.CreateModel( + name="ShareLink", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("deleted_at", models.DateTimeField(blank=True, null=True)), + ("restored_at", models.DateTimeField(blank=True, null=True)), + ("transaction_id", models.UUIDField(blank=True, null=True)), + ( + "created", + models.DateTimeField( + blank=True, + db_index=True, + default=django.utils.timezone.now, + editable=False, + verbose_name="created", + ), + ), + ( + "expiration", + models.DateTimeField( + blank=True, + db_index=True, + null=True, + verbose_name="expiration", + ), + ), + ( + "slug", + models.SlugField( + blank=True, + editable=False, + unique=True, + verbose_name="slug", + ), + ), + ( + "file_version", + models.CharField( + choices=[("archive", "Archive"), ("original", "Original")], + default="archive", + max_length=50, + ), + ), + ( + "document", + models.ForeignKey( + blank=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="share_links", + to="documents.document", + verbose_name="document", + ), + ), + ( + "owner", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="share_links", + to=settings.AUTH_USER_MODEL, + verbose_name="owner", + ), + ), + ], + options={ + "verbose_name": "share link", + "verbose_name_plural": "share links", + "ordering": ("created",), + }, + ), + migrations.CreateModel( + name="UiSettings", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("settings", models.JSONField(null=True)), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="ui_settings", + to=settings.AUTH_USER_MODEL, + ), + ), ], ), + migrations.CreateModel( + name="WorkflowAction", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "type", + models.PositiveIntegerField( + choices=[ + (1, "Assignment"), + (2, "Removal"), + (3, "Email"), + (4, "Webhook"), + ], + default=1, + verbose_name="Workflow Action Type", + ), + ), + ( + "assign_title", + models.TextField( + blank=True, + help_text="Assign a document title, must be a Jinja2 template, see documentation.", + null=True, + verbose_name="assign title", + ), + ), + ( + "assign_custom_fields_values", + models.JSONField( + blank=True, + default=dict, + help_text="Optional values to assign to the custom fields.", + null=True, + verbose_name="custom field values", + ), + ), + ( + "remove_all_tags", + models.BooleanField(default=False, verbose_name="remove all tags"), + ), + ( + "remove_all_document_types", + models.BooleanField( + default=False, + verbose_name="remove all document types", + ), + ), + ( + "remove_all_correspondents", + models.BooleanField( + default=False, + verbose_name="remove all correspondents", + ), + ), + ( + "remove_all_storage_paths", + models.BooleanField( + default=False, + verbose_name="remove all storage paths", + ), + ), + ( + "remove_all_owners", + models.BooleanField( + default=False, + verbose_name="remove all owners", + ), + ), + ( + "remove_all_permissions", + models.BooleanField( + default=False, + verbose_name="remove all permissions", + ), + ), + ( + "remove_all_custom_fields", + models.BooleanField( + default=False, + verbose_name="remove all custom fields", + ), + ), + ( + "assign_change_groups", + models.ManyToManyField( + blank=True, + related_name="+", + to="auth.group", + verbose_name="grant change permissions to these groups", + ), + ), + ( + "assign_change_users", + models.ManyToManyField( + blank=True, + related_name="+", + to=settings.AUTH_USER_MODEL, + verbose_name="grant change permissions to these users", + ), + ), + ( + "assign_correspondent", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="documents.correspondent", + verbose_name="assign this correspondent", + ), + ), + ( + "assign_custom_fields", + models.ManyToManyField( + blank=True, + related_name="+", + to="documents.customfield", + verbose_name="assign these custom fields", + ), + ), + ( + "assign_document_type", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="documents.documenttype", + verbose_name="assign this document type", + ), + ), + ( + "assign_owner", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to=settings.AUTH_USER_MODEL, + verbose_name="assign this owner", + ), + ), + ( + "assign_storage_path", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="documents.storagepath", + verbose_name="assign this storage path", + ), + ), + ( + "assign_tags", + models.ManyToManyField( + blank=True, + related_name="+", + to="documents.tag", + verbose_name="assign this tag", + ), + ), + ( + "assign_view_groups", + models.ManyToManyField( + blank=True, + related_name="+", + to="auth.group", + verbose_name="grant view permissions to these groups", + ), + ), + ( + "assign_view_users", + models.ManyToManyField( + blank=True, + related_name="+", + to=settings.AUTH_USER_MODEL, + verbose_name="grant view permissions to these users", + ), + ), + ( + "remove_change_groups", + models.ManyToManyField( + blank=True, + related_name="+", + to="auth.group", + verbose_name="remove change permissions for these groups", + ), + ), + ( + "remove_change_users", + models.ManyToManyField( + blank=True, + related_name="+", + to=settings.AUTH_USER_MODEL, + verbose_name="remove change permissions for these users", + ), + ), + ( + "remove_correspondents", + models.ManyToManyField( + blank=True, + related_name="+", + to="documents.correspondent", + verbose_name="remove these correspondent(s)", + ), + ), + ( + "remove_custom_fields", + models.ManyToManyField( + blank=True, + related_name="+", + to="documents.customfield", + verbose_name="remove these custom fields", + ), + ), + ( + "remove_document_types", + models.ManyToManyField( + blank=True, + related_name="+", + to="documents.documenttype", + verbose_name="remove these document type(s)", + ), + ), + ( + "remove_owners", + models.ManyToManyField( + blank=True, + related_name="+", + to=settings.AUTH_USER_MODEL, + verbose_name="remove these owner(s)", + ), + ), + ( + "remove_storage_paths", + models.ManyToManyField( + blank=True, + related_name="+", + to="documents.storagepath", + verbose_name="remove these storage path(s)", + ), + ), + ( + "remove_tags", + models.ManyToManyField( + blank=True, + related_name="+", + to="documents.tag", + verbose_name="remove these tag(s)", + ), + ), + ( + "remove_view_groups", + models.ManyToManyField( + blank=True, + related_name="+", + to="auth.group", + verbose_name="remove view permissions for these groups", + ), + ), + ( + "remove_view_users", + models.ManyToManyField( + blank=True, + related_name="+", + to=settings.AUTH_USER_MODEL, + verbose_name="remove view permissions for these users", + ), + ), + ( + "email", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="action", + to="documents.workflowactionemail", + verbose_name="email", + ), + ), + ( + "webhook", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="action", + to="documents.workflowactionwebhook", + verbose_name="webhook", + ), + ), + ], + options={ + "verbose_name": "workflow action", + "verbose_name_plural": "workflow actions", + }, + ), + migrations.CreateModel( + name="Workflow", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "name", + models.CharField(max_length=256, unique=True, verbose_name="name"), + ), + ("order", models.IntegerField(default=0, verbose_name="order")), + ("enabled", models.BooleanField(default=True, verbose_name="enabled")), + ( + "actions", + models.ManyToManyField( + related_name="workflows", + to="documents.workflowaction", + verbose_name="actions", + ), + ), + ], + ), + migrations.CreateModel( + name="WorkflowRun", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("deleted_at", models.DateTimeField(blank=True, null=True)), + ("restored_at", models.DateTimeField(blank=True, null=True)), + ("transaction_id", models.UUIDField(blank=True, null=True)), + ( + "type", + models.PositiveIntegerField( + choices=[ + (1, "Consumption Started"), + (2, "Document Added"), + (3, "Document Updated"), + (4, "Scheduled"), + ], + null=True, + verbose_name="workflow trigger type", + ), + ), + ( + "run_at", + models.DateTimeField( + db_index=True, + default=django.utils.timezone.now, + verbose_name="date run", + ), + ), + ( + "document", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="workflow_runs", + to="documents.document", + verbose_name="document", + ), + ), + ( + "workflow", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="runs", + to="documents.workflow", + verbose_name="workflow", + ), + ), + ], + options={ + "verbose_name": "workflow run", + "verbose_name_plural": "workflow runs", + }, + ), + migrations.CreateModel( + name="WorkflowTrigger", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "type", + models.PositiveIntegerField( + choices=[ + (1, "Consumption Started"), + (2, "Document Added"), + (3, "Document Updated"), + (4, "Scheduled"), + ], + default=1, + verbose_name="Workflow Trigger Type", + ), + ), + ( + "sources", + multiselectfield.db.fields.MultiSelectField( + choices=[ + (1, "Consume Folder"), + (2, "Api Upload"), + (3, "Mail Fetch"), + (4, "Web UI"), + ], + default="1,2,3,4", + max_length=7, + ), + ), + ( + "filter_path", + models.CharField( + blank=True, + help_text="Only consume documents with a path that matches this if specified. Wildcards specified as * are allowed. Case insensitive.", + max_length=256, + null=True, + verbose_name="filter path", + ), + ), + ( + "filter_filename", + models.CharField( + blank=True, + help_text="Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.", + max_length=256, + null=True, + verbose_name="filter filename", + ), + ), + ( + "match", + models.CharField(blank=True, max_length=256, verbose_name="match"), + ), + ( + "matching_algorithm", + models.PositiveIntegerField( + choices=[ + (0, "None"), + (1, "Any word"), + (2, "All words"), + (3, "Exact match"), + (4, "Regular expression"), + (5, "Fuzzy word"), + ], + default=0, + verbose_name="matching algorithm", + ), + ), + ( + "is_insensitive", + models.BooleanField(default=True, verbose_name="is insensitive"), + ), + ( + "filter_custom_field_query", + models.TextField( + blank=True, + help_text="JSON-encoded custom field query expression.", + null=True, + verbose_name="filter custom field query", + ), + ), + ( + "schedule_offset_days", + models.IntegerField( + default=0, + help_text="The number of days to offset the schedule trigger by.", + verbose_name="schedule offset days", + ), + ), + ( + "schedule_is_recurring", + models.BooleanField( + default=False, + help_text="If the schedule should be recurring.", + verbose_name="schedule is recurring", + ), + ), + ( + "schedule_recurring_interval_days", + models.PositiveIntegerField( + default=1, + help_text="The number of days between recurring schedule triggers.", + validators=[django.core.validators.MinValueValidator(1)], + verbose_name="schedule recurring delay in days", + ), + ), + ( + "schedule_date_field", + models.CharField( + choices=[ + ("added", "Added"), + ("created", "Created"), + ("modified", "Modified"), + ("custom_field", "Custom Field"), + ], + default="added", + help_text="The field to check for a schedule trigger.", + max_length=20, + verbose_name="schedule date field", + ), + ), + ( + "filter_has_all_tags", + models.ManyToManyField( + blank=True, + related_name="workflowtriggers_has_all", + to="documents.tag", + verbose_name="has all of these tag(s)", + ), + ), + ( + "filter_has_correspondent", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="documents.correspondent", + verbose_name="has this correspondent", + ), + ), + ( + "filter_has_document_type", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="documents.documenttype", + verbose_name="has this document type", + ), + ), + ( + "filter_has_not_correspondents", + models.ManyToManyField( + blank=True, + related_name="workflowtriggers_has_not_correspondent", + to="documents.correspondent", + verbose_name="does not have these correspondent(s)", + ), + ), + ( + "filter_has_not_document_types", + models.ManyToManyField( + blank=True, + related_name="workflowtriggers_has_not_document_type", + to="documents.documenttype", + verbose_name="does not have these document type(s)", + ), + ), + ( + "filter_has_not_storage_paths", + models.ManyToManyField( + blank=True, + related_name="workflowtriggers_has_not_storage_path", + to="documents.storagepath", + verbose_name="does not have these storage path(s)", + ), + ), + ( + "filter_has_not_tags", + models.ManyToManyField( + blank=True, + related_name="workflowtriggers_has_not", + to="documents.tag", + verbose_name="does not have these tag(s)", + ), + ), + ( + "filter_has_storage_path", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="documents.storagepath", + verbose_name="has this storage path", + ), + ), + ( + "filter_has_tags", + models.ManyToManyField( + blank=True, + to="documents.tag", + verbose_name="has these tag(s)", + ), + ), + ], + options={ + "verbose_name": "workflow trigger", + "verbose_name_plural": "workflow triggers", + }, + ), ] diff --git a/src/documents/migrations/0002_auto_20151226_1316.py b/src/documents/migrations/0002_auto_20151226_1316.py deleted file mode 100644 index ffd240902..000000000 --- a/src/documents/migrations/0002_auto_20151226_1316.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 1.9 on 2015-12-26 13:16 - -import django.utils.timezone -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "0001_initial"), - ] - - operations = [ - migrations.AlterModelOptions( - name="document", - options={"ordering": ("sender", "title")}, - ), - migrations.AlterField( - model_name="document", - name="created", - field=models.DateTimeField( - default=django.utils.timezone.now, - editable=False, - ), - ), - ] diff --git a/src/documents/migrations/1033_alter_documenttype_options_alter_tag_options_and_more.py b/src/documents/migrations/0002_initial.py similarity index 61% rename from src/documents/migrations/1033_alter_documenttype_options_alter_tag_options_and_more.py rename to src/documents/migrations/0002_initial.py index 1368ac641..ba0e93da8 100644 --- a/src/documents/migrations/1033_alter_documenttype_options_alter_tag_options_and_more.py +++ b/src/documents/migrations/0002_initial.py @@ -1,50 +1,49 @@ -# Generated by Django 4.1.5 on 2023-03-04 22:33 +# Generated by Django 5.2.9 on 2026-01-20 18:46 +import django.db.models.deletion from django.db import migrations from django.db import models class Migration(migrations.Migration): + initial = True + dependencies = [ - ("documents", "1032_alter_correspondent_matching_algorithm_and_more"), + ("documents", "0001_initial"), + ("paperless_mail", "0001_initial"), ] operations = [ - migrations.AlterModelOptions( - name="documenttype", - options={ - "ordering": ("name",), - "verbose_name": "document type", - "verbose_name_plural": "document types", - }, + migrations.AddField( + model_name="workflowtrigger", + name="filter_mailrule", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="paperless_mail.mailrule", + verbose_name="filter documents from this mail rule", + ), ), - migrations.AlterModelOptions( - name="tag", - options={ - "ordering": ("name",), - "verbose_name": "tag", - "verbose_name_plural": "tags", - }, + migrations.AddField( + model_name="workflowtrigger", + name="schedule_date_custom_field", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="documents.customfield", + verbose_name="schedule date custom field", + ), ), - migrations.AlterField( - model_name="correspondent", - name="name", - field=models.CharField(max_length=128, verbose_name="name"), - ), - migrations.AlterField( - model_name="documenttype", - name="name", - field=models.CharField(max_length=128, verbose_name="name"), - ), - migrations.AlterField( - model_name="storagepath", - name="name", - field=models.CharField(max_length=128, verbose_name="name"), - ), - migrations.AlterField( - model_name="tag", - name="name", - field=models.CharField(max_length=128, verbose_name="name"), + migrations.AddField( + model_name="workflow", + name="triggers", + field=models.ManyToManyField( + related_name="workflows", + to="documents.workflowtrigger", + verbose_name="triggers", + ), ), migrations.AddConstraint( model_name="correspondent", @@ -61,6 +60,13 @@ class Migration(migrations.Migration): name="documents_correspondent_name_uniq", ), ), + migrations.AddConstraint( + model_name="customfieldinstance", + constraint=models.UniqueConstraint( + fields=("document", "field"), + name="documents_customfieldinstance_unique_document_field", + ), + ), migrations.AddConstraint( model_name="documenttype", constraint=models.UniqueConstraint( diff --git a/src/documents/migrations/0003_sender.py b/src/documents/migrations/0003_sender.py deleted file mode 100644 index dd194afdb..000000000 --- a/src/documents/migrations/0003_sender.py +++ /dev/null @@ -1,70 +0,0 @@ -# Generated by Django 1.9 on 2016-01-11 12:21 - -import django.db.models.deletion -from django.db import migrations -from django.db import models -from django.template.defaultfilters import slugify - -DOCUMENT_SENDER_MAP = {} - - -def move_sender_strings_to_sender_model(apps, schema_editor): - sender_model = apps.get_model("documents", "Sender") - document_model = apps.get_model("documents", "Document") - - # Create the sender and log the relationship with the document - for document in document_model.objects.all(): - if document.sender: - ( - DOCUMENT_SENDER_MAP[document.pk], - _, - ) = sender_model.objects.get_or_create( - name=document.sender, - defaults={"slug": slugify(document.sender)}, - ) - - -def realign_senders(apps, schema_editor): - document_model = apps.get_model("documents", "Document") - for pk, sender in DOCUMENT_SENDER_MAP.items(): - document_model.objects.filter(pk=pk).update(sender=sender) - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "0002_auto_20151226_1316"), - ] - - operations = [ - migrations.CreateModel( - name="Sender", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("name", models.CharField(max_length=128, unique=True)), - ("slug", models.SlugField()), - ], - ), - migrations.RunPython(move_sender_strings_to_sender_model), - migrations.RemoveField( - model_name="document", - name="sender", - ), - migrations.AddField( - model_name="document", - name="sender", - field=models.ForeignKey( - blank=True, - on_delete=django.db.models.deletion.CASCADE, - to="documents.Sender", - ), - ), - migrations.RunPython(realign_senders), - ] diff --git a/src/paperless_mail/migrations/0004_mailrule_order.py b/src/documents/migrations/0003_workflowaction_order.py similarity index 51% rename from src/paperless_mail/migrations/0004_mailrule_order.py rename to src/documents/migrations/0003_workflowaction_order.py index 4ffc0a6e5..82bc49ba7 100644 --- a/src/paperless_mail/migrations/0004_mailrule_order.py +++ b/src/documents/migrations/0003_workflowaction_order.py @@ -1,4 +1,4 @@ -# Generated by Django 3.1.3 on 2020-11-21 21:51 +# Generated by Django 5.2.9 on 2026-01-20 20:06 from django.db import migrations from django.db import models @@ -6,13 +6,13 @@ from django.db import models class Migration(migrations.Migration): dependencies = [ - ("paperless_mail", "0003_auto_20201118_1940"), + ("documents", "0002_initial"), ] operations = [ migrations.AddField( - model_name="mailrule", + model_name="workflowaction", name="order", - field=models.IntegerField(default=0), + field=models.PositiveIntegerField(default=0, verbose_name="order"), ), ] diff --git a/src/documents/migrations/0004_auto_20160114_1844.py b/src/documents/migrations/0004_auto_20160114_1844.py deleted file mode 100644 index 97bda420e..000000000 --- a/src/documents/migrations/0004_auto_20160114_1844.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 1.9 on 2016-01-14 18:44 - -import django.db.models.deletion -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "0003_sender"), - ] - - operations = [ - migrations.AlterField( - model_name="document", - name="sender", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="documents", - to="documents.Sender", - ), - ), - ] diff --git a/src/documents/migrations/0004_auto_20160114_1844_squashed_0011_auto_20160303_1929.py b/src/documents/migrations/0004_auto_20160114_1844_squashed_0011_auto_20160303_1929.py deleted file mode 100644 index 8d86cbbc1..000000000 --- a/src/documents/migrations/0004_auto_20160114_1844_squashed_0011_auto_20160303_1929.py +++ /dev/null @@ -1,178 +0,0 @@ -# Generated by Django 4.2.13 on 2024-06-28 17:52 - -import django.db.models.deletion -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - replaces = [ - ("documents", "0004_auto_20160114_1844"), - ("documents", "0005_auto_20160123_0313"), - ("documents", "0006_auto_20160123_0430"), - ("documents", "0007_auto_20160126_2114"), - ("documents", "0008_document_file_type"), - ("documents", "0009_auto_20160214_0040"), - ("documents", "0010_log"), - ("documents", "0011_auto_20160303_1929"), - ] - - dependencies = [ - ("documents", "0003_sender"), - ] - - operations = [ - migrations.AlterField( - model_name="document", - name="sender", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="documents", - to="documents.sender", - ), - ), - migrations.AlterModelOptions( - name="sender", - options={"ordering": ("name",)}, - ), - migrations.CreateModel( - name="Tag", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("name", models.CharField(max_length=128, unique=True)), - ("slug", models.SlugField(blank=True)), - ( - "colour", - models.PositiveIntegerField( - choices=[ - (1, "#a6cee3"), - (2, "#1f78b4"), - (3, "#b2df8a"), - (4, "#33a02c"), - (5, "#fb9a99"), - (6, "#e31a1c"), - (7, "#fdbf6f"), - (8, "#ff7f00"), - (9, "#cab2d6"), - (10, "#6a3d9a"), - (11, "#b15928"), - (12, "#000000"), - (13, "#cccccc"), - ], - default=1, - ), - ), - ("match", models.CharField(blank=True, max_length=256)), - ( - "matching_algorithm", - models.PositiveIntegerField( - choices=[ - (1, "Any"), - (2, "All"), - (3, "Literal"), - (4, "Regular Expression"), - ], - default=1, - help_text='Which algorithm you want to use when matching text to the OCR\'d PDF. Here, "any" looks for any occurrence of any word provided in the PDF, while "all" requires that every word provided appear in the PDF, albeit not in the order provided. A "literal" match means that the text you enter must appear in the PDF exactly as you\'ve entered it, and "regular expression" uses a regex to match the PDF. If you don\'t know what a regex is, you probably don\'t want this option.', - ), - ), - ], - options={ - "abstract": False, - }, - ), - migrations.AlterField( - model_name="sender", - name="slug", - field=models.SlugField(blank=True), - ), - migrations.AddField( - model_name="document", - name="file_type", - field=models.CharField( - choices=[ - ("pdf", "PDF"), - ("png", "PNG"), - ("jpg", "JPG"), - ("gif", "GIF"), - ("tiff", "TIFF"), - ], - default="pdf", - editable=False, - max_length=4, - ), - preserve_default=False, - ), - migrations.AddField( - model_name="document", - name="tags", - field=models.ManyToManyField( - blank=True, - related_name="documents", - to="documents.tag", - ), - ), - migrations.CreateModel( - name="Log", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("group", models.UUIDField(blank=True)), - ("message", models.TextField()), - ( - "level", - models.PositiveIntegerField( - choices=[ - (10, "Debugging"), - (20, "Informational"), - (30, "Warning"), - (40, "Error"), - (50, "Critical"), - ], - default=20, - ), - ), - ( - "component", - models.PositiveIntegerField( - choices=[(1, "Consumer"), (2, "Mail Fetcher")], - ), - ), - ("created", models.DateTimeField(auto_now_add=True)), - ("modified", models.DateTimeField(auto_now=True)), - ], - options={ - "ordering": ("-modified",), - }, - ), - migrations.RenameModel( - old_name="Sender", - new_name="Correspondent", - ), - migrations.AlterModelOptions( - name="document", - options={"ordering": ("correspondent", "title")}, - ), - migrations.RenameField( - model_name="document", - old_name="sender", - new_name="correspondent", - ), - ] diff --git a/src/paperless_mail/migrations/0011_remove_mailrule_assign_tag.py b/src/documents/migrations/0004_remove_document_storage_type.py similarity index 50% rename from src/paperless_mail/migrations/0011_remove_mailrule_assign_tag.py rename to src/documents/migrations/0004_remove_document_storage_type.py index 16cec8710..e138d5d78 100644 --- a/src/paperless_mail/migrations/0011_remove_mailrule_assign_tag.py +++ b/src/documents/migrations/0004_remove_document_storage_type.py @@ -1,16 +1,16 @@ -# Generated by Django 3.2.12 on 2022-03-11 15:18 +# Generated by Django 5.2.9 on 2026-01-24 23:05 from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ("paperless_mail", "0010_auto_20220311_1602"), + ("documents", "0003_workflowaction_order"), ] operations = [ migrations.RemoveField( - model_name="mailrule", - name="assign_tag", + model_name="document", + name="storage_type", ), ] diff --git a/src/documents/migrations/0005_auto_20160123_0313.py b/src/documents/migrations/0005_auto_20160123_0313.py deleted file mode 100644 index b0ccc5825..000000000 --- a/src/documents/migrations/0005_auto_20160123_0313.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 1.9 on 2016-01-23 03:13 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "0004_auto_20160114_1844"), - ] - - operations = [ - migrations.AlterModelOptions( - name="sender", - options={"ordering": ("name",)}, - ), - ] diff --git a/src/documents/migrations/0005_workflowtrigger_filter_has_any_correspondents_and_more.py b/src/documents/migrations/0005_workflowtrigger_filter_has_any_correspondents_and_more.py new file mode 100644 index 000000000..db5ef5754 --- /dev/null +++ b/src/documents/migrations/0005_workflowtrigger_filter_has_any_correspondents_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 5.2.7 on 2025-12-17 22:25 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("documents", "0004_remove_document_storage_type"), + ] + + operations = [ + migrations.AddField( + model_name="workflowtrigger", + name="filter_has_any_correspondents", + field=models.ManyToManyField( + blank=True, + related_name="workflowtriggers_has_any_correspondent", + to="documents.correspondent", + verbose_name="has one of these correspondents", + ), + ), + migrations.AddField( + model_name="workflowtrigger", + name="filter_has_any_document_types", + field=models.ManyToManyField( + blank=True, + related_name="workflowtriggers_has_any_document_type", + to="documents.documenttype", + verbose_name="has one of these document types", + ), + ), + migrations.AddField( + model_name="workflowtrigger", + name="filter_has_any_storage_paths", + field=models.ManyToManyField( + blank=True, + related_name="workflowtriggers_has_any_storage_path", + to="documents.storagepath", + verbose_name="has one of these storage paths", + ), + ), + ] diff --git a/src/documents/migrations/0006_alter_document_checksum_unique.py b/src/documents/migrations/0006_alter_document_checksum_unique.py new file mode 100644 index 000000000..f86799494 --- /dev/null +++ b/src/documents/migrations/0006_alter_document_checksum_unique.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.7 on 2026-01-14 17:45 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("documents", "0005_workflowtrigger_filter_has_any_correspondents_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="document", + name="checksum", + field=models.CharField( + editable=False, + max_length=32, + verbose_name="checksum", + help_text="The checksum of the original document.", + ), + ), + ] diff --git a/src/documents/migrations/0006_auto_20160123_0430.py b/src/documents/migrations/0006_auto_20160123_0430.py deleted file mode 100644 index 315b9646f..000000000 --- a/src/documents/migrations/0006_auto_20160123_0430.py +++ /dev/null @@ -1,64 +0,0 @@ -# Generated by Django 1.9 on 2016-01-23 04:30 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "0005_auto_20160123_0313"), - ] - - operations = [ - migrations.CreateModel( - name="Tag", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("name", models.CharField(max_length=128, unique=True)), - ("slug", models.SlugField(blank=True)), - ( - "colour", - models.PositiveIntegerField( - choices=[ - (1, "#a6cee3"), - (2, "#1f78b4"), - (3, "#b2df8a"), - (4, "#33a02c"), - (5, "#fb9a99"), - (6, "#e31a1c"), - (7, "#fdbf6f"), - (8, "#ff7f00"), - (9, "#cab2d6"), - (10, "#6a3d9a"), - (11, "#ffff99"), - (12, "#b15928"), - (13, "#000000"), - (14, "#cccccc"), - ], - default=1, - ), - ), - ], - options={ - "abstract": False, - }, - ), - migrations.AlterField( - model_name="sender", - name="slug", - field=models.SlugField(blank=True), - ), - migrations.AddField( - model_name="document", - name="tags", - field=models.ManyToManyField(related_name="documents", to="documents.Tag"), - ), - ] diff --git a/src/documents/migrations/0007_auto_20160126_2114.py b/src/documents/migrations/0007_auto_20160126_2114.py deleted file mode 100644 index 04ccc0589..000000000 --- a/src/documents/migrations/0007_auto_20160126_2114.py +++ /dev/null @@ -1,55 +0,0 @@ -# Generated by Django 1.9 on 2016-01-26 21:14 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "0006_auto_20160123_0430"), - ] - - operations = [ - migrations.AddField( - model_name="tag", - name="match", - field=models.CharField(blank=True, max_length=256), - ), - migrations.AddField( - model_name="tag", - name="matching_algorithm", - field=models.PositiveIntegerField( - blank=True, - choices=[ - (1, "Any"), - (2, "All"), - (3, "Literal"), - (4, "Regular Expression"), - ], - help_text='Which algorithm you want to use when matching text to the OCR\'d PDF. Here, "any" looks for any occurrence of any word provided in the PDF, while "all" requires that every word provided appear in the PDF, albeit not in the order provided. A "literal" match means that the text you enter must appear in the PDF exactly as you\'ve entered it, and "regular expression" uses a regex to match the PDF. If you don\'t know what a regex is, you probably don\'t want this option.', - null=True, - ), - ), - migrations.AlterField( - model_name="tag", - name="colour", - field=models.PositiveIntegerField( - choices=[ - (1, "#a6cee3"), - (2, "#1f78b4"), - (3, "#b2df8a"), - (4, "#33a02c"), - (5, "#fb9a99"), - (6, "#e31a1c"), - (7, "#fdbf6f"), - (8, "#ff7f00"), - (9, "#cab2d6"), - (10, "#6a3d9a"), - (11, "#b15928"), - (12, "#000000"), - (13, "#cccccc"), - ], - default=1, - ), - ), - ] diff --git a/src/documents/migrations/0007_document_content_length.py b/src/documents/migrations/0007_document_content_length.py new file mode 100644 index 000000000..c294afca5 --- /dev/null +++ b/src/documents/migrations/0007_document_content_length.py @@ -0,0 +1,25 @@ +# Generated by Django 5.2.6 on 2026-01-24 07:33 + +import django.db.models.functions.text +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("documents", "0006_alter_document_checksum_unique"), + ] + + operations = [ + migrations.AddField( + model_name="document", + name="content_length", + field=models.GeneratedField( + db_persist=True, + expression=django.db.models.functions.text.Length("content"), + null=False, + help_text="Length of the content field in characters. Automatically maintained by the database for faster statistics computation.", + output_field=models.PositiveIntegerField(default=0), + ), + ), + ] diff --git a/src/documents/migrations/0008_document_file_type.py b/src/documents/migrations/0008_document_file_type.py deleted file mode 100644 index 7787f8622..000000000 --- a/src/documents/migrations/0008_document_file_type.py +++ /dev/null @@ -1,39 +0,0 @@ -# Generated by Django 1.9 on 2016-01-29 22:58 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "0007_auto_20160126_2114"), - ] - - operations = [ - migrations.AddField( - model_name="document", - name="file_type", - field=models.CharField( - choices=[ - ("pdf", "PDF"), - ("png", "PNG"), - ("jpg", "JPG"), - ("gif", "GIF"), - ("tiff", "TIFF"), - ], - default="pdf", - editable=False, - max_length=4, - ), - preserve_default=False, - ), - migrations.AlterField( - model_name="document", - name="tags", - field=models.ManyToManyField( - blank=True, - related_name="documents", - to="documents.Tag", - ), - ), - ] diff --git a/src/documents/migrations/0008_sharelinkbundle.py b/src/documents/migrations/0008_sharelinkbundle.py new file mode 100644 index 000000000..35ef64c75 --- /dev/null +++ b/src/documents/migrations/0008_sharelinkbundle.py @@ -0,0 +1,177 @@ +# Generated by Django 5.2.9 on 2026-01-27 01:09 + +import django.db.models.deletion +import django.db.models.functions.text +import django.utils.timezone +from django.conf import settings +from django.contrib.auth.management import create_permissions +from django.contrib.auth.models import Group +from django.contrib.auth.models import Permission +from django.contrib.auth.models import User +from django.db import migrations +from django.db import models + + +def grant_share_link_bundle_permissions(apps, schema_editor): + # Ensure newly introduced permissions are created for all apps + for app_config in apps.get_app_configs(): + app_config.models_module = True + create_permissions(app_config, apps=apps, verbosity=0) + app_config.models_module = None + + add_document_perm = Permission.objects.filter(codename="add_document").first() + share_bundle_permissions = Permission.objects.filter( + codename__contains="sharelinkbundle", + ) + + users = User.objects.filter(user_permissions=add_document_perm).distinct() + for user in users: + user.user_permissions.add(*share_bundle_permissions) + + groups = Group.objects.filter(permissions=add_document_perm).distinct() + for group in groups: + group.permissions.add(*share_bundle_permissions) + + +def revoke_share_link_bundle_permissions(apps, schema_editor): + share_bundle_permissions = Permission.objects.filter( + codename__contains="sharelinkbundle", + ) + for user in User.objects.all(): + user.user_permissions.remove(*share_bundle_permissions) + for group in Group.objects.all(): + group.permissions.remove(*share_bundle_permissions) + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("documents", "0007_document_content_length"), + ] + + operations = [ + migrations.CreateModel( + name="ShareLinkBundle", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "created", + models.DateTimeField( + blank=True, + db_index=True, + default=django.utils.timezone.now, + editable=False, + verbose_name="created", + ), + ), + ( + "expiration", + models.DateTimeField( + blank=True, + db_index=True, + null=True, + verbose_name="expiration", + ), + ), + ( + "slug", + models.SlugField( + blank=True, + editable=False, + unique=True, + verbose_name="slug", + ), + ), + ( + "file_version", + models.CharField( + choices=[("archive", "Archive"), ("original", "Original")], + default="archive", + max_length=50, + ), + ), + ( + "status", + models.CharField( + choices=[ + ("pending", "Pending"), + ("processing", "Processing"), + ("ready", "Ready"), + ("failed", "Failed"), + ], + default="pending", + max_length=50, + ), + ), + ( + "size_bytes", + models.PositiveIntegerField( + blank=True, + null=True, + verbose_name="size (bytes)", + ), + ), + ( + "last_error", + models.JSONField( + blank=True, + null=True, + default=None, + verbose_name="last error", + ), + ), + ( + "file_path", + models.CharField( + blank=True, + max_length=512, + verbose_name="file path", + ), + ), + ( + "built_at", + models.DateTimeField( + blank=True, + null=True, + verbose_name="built at", + ), + ), + ( + "documents", + models.ManyToManyField( + related_name="share_link_bundles", + to="documents.document", + verbose_name="documents", + ), + ), + ( + "owner", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="share_link_bundles", + to=settings.AUTH_USER_MODEL, + verbose_name="owner", + ), + ), + ], + options={ + "ordering": ("-created",), + "verbose_name": "share link bundle", + "verbose_name_plural": "share link bundles", + }, + ), + migrations.RunPython( + grant_share_link_bundle_permissions, + reverse_code=revoke_share_link_bundle_permissions, + ), + ] diff --git a/src/documents/migrations/0009_auto_20160214_0040.py b/src/documents/migrations/0009_auto_20160214_0040.py deleted file mode 100644 index 5c95e1035..000000000 --- a/src/documents/migrations/0009_auto_20160214_0040.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 1.9 on 2016-02-14 00:40 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "0008_document_file_type"), - ] - - operations = [ - migrations.AlterField( - model_name="tag", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (1, "Any"), - (2, "All"), - (3, "Literal"), - (4, "Regular Expression"), - ], - default=1, - help_text='Which algorithm you want to use when matching text to the OCR\'d PDF. Here, "any" looks for any occurrence of any word provided in the PDF, while "all" requires that every word provided appear in the PDF, albeit not in the order provided. A "literal" match means that the text you enter must appear in the PDF exactly as you\'ve entered it, and "regular expression" uses a regex to match the PDF. If you don\'t know what a regex is, you probably don\'t want this option.', - ), - ), - ] diff --git a/src/documents/migrations/0010_log.py b/src/documents/migrations/0010_log.py deleted file mode 100644 index 8f015cab0..000000000 --- a/src/documents/migrations/0010_log.py +++ /dev/null @@ -1,53 +0,0 @@ -# Generated by Django 1.9 on 2016-02-27 17:54 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "0009_auto_20160214_0040"), - ] - - operations = [ - migrations.CreateModel( - name="Log", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("group", models.UUIDField(blank=True)), - ("message", models.TextField()), - ( - "level", - models.PositiveIntegerField( - choices=[ - (10, "Debugging"), - (20, "Informational"), - (30, "Warning"), - (40, "Error"), - (50, "Critical"), - ], - default=20, - ), - ), - ( - "component", - models.PositiveIntegerField( - choices=[(1, "Consumer"), (2, "Mail Fetcher")], - ), - ), - ("created", models.DateTimeField(auto_now_add=True)), - ("modified", models.DateTimeField(auto_now=True)), - ], - options={ - "ordering": ("-modified",), - }, - ), - ] diff --git a/src/documents/migrations/0011_auto_20160303_1929.py b/src/documents/migrations/0011_auto_20160303_1929.py deleted file mode 100644 index 4c4fcd3ad..000000000 --- a/src/documents/migrations/0011_auto_20160303_1929.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 1.9.2 on 2016-03-03 19:29 - -from django.db import migrations - - -class Migration(migrations.Migration): - atomic = False - dependencies = [ - ("documents", "0010_log"), - ] - - operations = [ - migrations.RenameModel( - old_name="Sender", - new_name="Correspondent", - ), - migrations.AlterModelOptions( - name="document", - options={"ordering": ("correspondent", "title")}, - ), - migrations.RenameField( - model_name="document", - old_name="sender", - new_name="correspondent", - ), - ] diff --git a/src/documents/migrations/0012_auto_20160305_0040.py b/src/documents/migrations/0012_auto_20160305_0040.py deleted file mode 100644 index 097661137..000000000 --- a/src/documents/migrations/0012_auto_20160305_0040.py +++ /dev/null @@ -1,128 +0,0 @@ -# Generated by Django 1.9.2 on 2016-03-05 00:40 - -import os -import re -import shutil -import subprocess -import tempfile -from pathlib import Path - -import gnupg -from django.conf import settings -from django.db import migrations -from django.utils.termcolors import colorize as colourise # Spelling hurts me - - -class GnuPG: - """ - A handy singleton to use when handling encrypted files. - """ - - gpg = gnupg.GPG(gnupghome=settings.GNUPG_HOME) - - @classmethod - def decrypted(cls, file_handle): - return cls.gpg.decrypt_file(file_handle, passphrase=settings.PASSPHRASE).data - - @classmethod - def encrypted(cls, file_handle): - return cls.gpg.encrypt_file( - file_handle, - recipients=None, - passphrase=settings.PASSPHRASE, - symmetric=True, - ).data - - -def move_documents_and_create_thumbnails(apps, schema_editor): - (Path(settings.MEDIA_ROOT) / "documents" / "originals").mkdir( - parents=True, - exist_ok=True, - ) - (Path(settings.MEDIA_ROOT) / "documents" / "thumbnails").mkdir( - parents=True, - exist_ok=True, - ) - - documents: list[str] = os.listdir(Path(settings.MEDIA_ROOT) / "documents") # noqa: PTH208 - - if set(documents) == {"originals", "thumbnails"}: - return - - print( - colourise( - "\n\n" - " This is a one-time only migration to generate thumbnails for all of your\n" - " documents so that future UIs will have something to work with. If you have\n" - " a lot of documents though, this may take a while, so a coffee break may be\n" - " in order." - "\n", - opts=("bold",), - ), - ) - - Path(settings.SCRATCH_DIR).mkdir(parents=True, exist_ok=True) - - for f in sorted(documents): - if not f.endswith("gpg"): - continue - - print( - " {} {} {}".format( - colourise("*", fg="green"), - colourise("Generating a thumbnail for", fg="white"), - colourise(f, fg="cyan"), - ), - ) - - thumb_temp: str = tempfile.mkdtemp(prefix="paperless", dir=settings.SCRATCH_DIR) - orig_temp: str = tempfile.mkdtemp(prefix="paperless", dir=settings.SCRATCH_DIR) - - orig_source: Path = Path(settings.MEDIA_ROOT) / "documents" / f - orig_target: Path = Path(orig_temp) / f.replace(".gpg", "") - - with orig_source.open("rb") as encrypted, orig_target.open("wb") as unencrypted: - unencrypted.write(GnuPG.decrypted(encrypted)) - - subprocess.Popen( - ( - settings.CONVERT_BINARY, - "-scale", - "500x5000", - "-alpha", - "remove", - orig_target, - Path(thumb_temp) / "convert-%04d.png", - ), - ).wait() - - thumb_source: Path = Path(thumb_temp) / "convert-0000.png" - thumb_target: Path = ( - Path(settings.MEDIA_ROOT) - / "documents" - / "thumbnails" - / re.sub(r"(\d+)\.\w+(\.gpg)", "\\1.png\\2", f) - ) - with ( - thumb_source.open("rb") as unencrypted, - thumb_target.open("wb") as encrypted, - ): - encrypted.write(GnuPG.encrypted(unencrypted)) - - shutil.rmtree(thumb_temp) - shutil.rmtree(orig_temp) - - shutil.move( - Path(settings.MEDIA_ROOT) / "documents" / f, - Path(settings.MEDIA_ROOT) / "documents" / "originals" / f, - ) - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "0011_auto_20160303_1929"), - ] - - operations = [ - migrations.RunPython(move_documents_and_create_thumbnails), - ] diff --git a/src/documents/migrations/0013_auto_20160325_2111.py b/src/documents/migrations/0013_auto_20160325_2111.py deleted file mode 100644 index 1d3f8b07d..000000000 --- a/src/documents/migrations/0013_auto_20160325_2111.py +++ /dev/null @@ -1,42 +0,0 @@ -# Generated by Django 1.9.4 on 2016-03-25 21:11 - -import django.utils.timezone -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "0012_auto_20160305_0040"), - ] - - operations = [ - migrations.AddField( - model_name="correspondent", - name="match", - field=models.CharField(blank=True, max_length=256), - ), - migrations.AddField( - model_name="correspondent", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (1, "Any"), - (2, "All"), - (3, "Literal"), - (4, "Regular Expression"), - ], - default=1, - help_text='Which algorithm you want to use when matching text to the OCR\'d PDF. Here, "any" looks for any occurrence of any word provided in the PDF, while "all" requires that every word provided appear in the PDF, albeit not in the order provided. A "literal" match means that the text you enter must appear in the PDF exactly as you\'ve entered it, and "regular expression" uses a regex to match the PDF. If you don\'t know what a regex is, you probably don\'t want this option.', - ), - ), - migrations.AlterField( - model_name="document", - name="created", - field=models.DateTimeField(default=django.utils.timezone.now), - ), - migrations.RemoveField( - model_name="log", - name="component", - ), - ] diff --git a/src/documents/migrations/0014_document_checksum.py b/src/documents/migrations/0014_document_checksum.py deleted file mode 100644 index de256ced7..000000000 --- a/src/documents/migrations/0014_document_checksum.py +++ /dev/null @@ -1,182 +0,0 @@ -# Generated by Django 1.9.4 on 2016-03-28 19:09 - -import hashlib -from pathlib import Path - -import django.utils.timezone -import gnupg -from django.conf import settings -from django.db import migrations -from django.db import models -from django.template.defaultfilters import slugify -from django.utils.termcolors import colorize as colourise # Spelling hurts me - - -class GnuPG: - """ - A handy singleton to use when handling encrypted files. - """ - - gpg = gnupg.GPG(gnupghome=settings.GNUPG_HOME) - - @classmethod - def decrypted(cls, file_handle): - return cls.gpg.decrypt_file(file_handle, passphrase=settings.PASSPHRASE).data - - @classmethod - def encrypted(cls, file_handle): - return cls.gpg.encrypt_file( - file_handle, - recipients=None, - passphrase=settings.PASSPHRASE, - symmetric=True, - ).data - - -class Document: - """ - Django's migrations restrict access to model methods, so this is a snapshot - of the methods that existed at the time this migration was written, since - we need to make use of a lot of these shortcuts here. - """ - - def __init__(self, doc): - self.pk = doc.pk - self.correspondent = doc.correspondent - self.title = doc.title - self.file_type = doc.file_type - self.tags = doc.tags - self.created = doc.created - - def __str__(self): - created = self.created.strftime("%Y%m%d%H%M%S") - if self.correspondent and self.title: - return f"{created}: {self.correspondent} - {self.title}" - if self.correspondent or self.title: - return f"{created}: {self.correspondent or self.title}" - return str(created) - - @property - def source_path(self): - return ( - Path(settings.MEDIA_ROOT) - / "documents" - / "originals" - / f"{self.pk:07}.{self.file_type}.gpg" - ) - - @property - def source_file(self): - return self.source_path.open("rb") - - @property - def file_name(self): - return slugify(str(self)) + "." + self.file_type - - -def set_checksums(apps, schema_editor): - document_model = apps.get_model("documents", "Document") - - if not document_model.objects.all().exists(): - return - - print( - colourise( - "\n\n" - " This is a one-time only migration to generate checksums for all\n" - " of your existing documents. If you have a lot of documents\n" - " though, this may take a while, so a coffee break may be in\n" - " order." - "\n", - opts=("bold",), - ), - ) - - sums = {} - for d in document_model.objects.all(): - document = Document(d) - - print( - " {} {} {}".format( - colourise("*", fg="green"), - colourise("Generating a checksum for", fg="white"), - colourise(document.file_name, fg="cyan"), - ), - ) - - with document.source_file as encrypted: - checksum = hashlib.md5(GnuPG.decrypted(encrypted)).hexdigest() - - if checksum in sums: - error = "\n{line}{p1}\n\n{doc1}\n{doc2}\n\n{p2}\n\n{code}\n\n{p3}{line}".format( - p1=colourise( - "It appears that you have two identical documents in your collection and \nPaperless no longer supports this (see issue #97). The documents in question\nare:", - fg="yellow", - ), - p2=colourise( - "To fix this problem, you'll have to remove one of them from the database, a task\nmost easily done by running the following command in the same\ndirectory as manage.py:", - fg="yellow", - ), - p3=colourise( - "When that's finished, re-run the migrate, and provided that there aren't any\nother duplicates, you should be good to go.", - fg="yellow", - ), - doc1=colourise( - f" * {sums[checksum][1]} (id: {sums[checksum][0]})", - fg="red", - ), - doc2=colourise( - f" * {document.file_name} (id: {document.pk})", - fg="red", - ), - code=colourise( - f" $ echo 'DELETE FROM documents_document WHERE id = {document.pk};' | ./manage.py dbshell", - fg="green", - ), - line=colourise("\n{}\n".format("=" * 80), fg="white", opts=("bold",)), - ) - raise RuntimeError(error) - sums[checksum] = (document.pk, document.file_name) - - document_model.objects.filter(pk=document.pk).update(checksum=checksum) - - -def do_nothing(apps, schema_editor): - pass - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "0013_auto_20160325_2111"), - ] - - operations = [ - migrations.AddField( - model_name="document", - name="checksum", - field=models.CharField( - default="-", - db_index=True, - editable=False, - max_length=32, - help_text="The checksum of the original document (before it " - "was encrypted). We use this to prevent duplicate " - "document imports.", - ), - preserve_default=False, - ), - migrations.RunPython(set_checksums, do_nothing), - migrations.AlterField( - model_name="document", - name="created", - field=models.DateTimeField( - db_index=True, - default=django.utils.timezone.now, - ), - ), - migrations.AlterField( - model_name="document", - name="modified", - field=models.DateTimeField(auto_now=True, db_index=True), - ), - ] diff --git a/src/documents/migrations/0015_add_insensitive_to_match.py b/src/documents/migrations/0015_add_insensitive_to_match.py deleted file mode 100644 index 0761cc929..000000000 --- a/src/documents/migrations/0015_add_insensitive_to_match.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 1.10.2 on 2016-10-05 21:38 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "0014_document_checksum"), - ] - - operations = [ - migrations.AlterField( - model_name="document", - name="checksum", - field=models.CharField( - editable=False, - help_text="The checksum of the original document (before it was encrypted). We use this to prevent duplicate document imports.", - max_length=32, - unique=True, - ), - ), - migrations.AddField( - model_name="correspondent", - name="is_insensitive", - field=models.BooleanField(default=True), - ), - migrations.AddField( - model_name="tag", - name="is_insensitive", - field=models.BooleanField(default=True), - ), - ] diff --git a/src/documents/migrations/0015_add_insensitive_to_match_squashed_0018_auto_20170715_1712.py b/src/documents/migrations/0015_add_insensitive_to_match_squashed_0018_auto_20170715_1712.py deleted file mode 100644 index bfa897601..000000000 --- a/src/documents/migrations/0015_add_insensitive_to_match_squashed_0018_auto_20170715_1712.py +++ /dev/null @@ -1,92 +0,0 @@ -# Generated by Django 4.2.13 on 2024-06-28 17:57 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - replaces = [ - ("documents", "0015_add_insensitive_to_match"), - ("documents", "0016_auto_20170325_1558"), - ("documents", "0017_auto_20170512_0507"), - ("documents", "0018_auto_20170715_1712"), - ] - - dependencies = [ - ("documents", "0014_document_checksum"), - ] - - operations = [ - migrations.AlterField( - model_name="document", - name="checksum", - field=models.CharField( - editable=False, - help_text="The checksum of the original document (before it was encrypted). We use this to prevent duplicate document imports.", - max_length=32, - unique=True, - ), - ), - migrations.AddField( - model_name="correspondent", - name="is_insensitive", - field=models.BooleanField(default=True), - ), - migrations.AddField( - model_name="tag", - name="is_insensitive", - field=models.BooleanField(default=True), - ), - migrations.AlterField( - model_name="document", - name="content", - field=models.TextField( - blank=True, - db_index=("mysql" not in settings.DATABASES["default"]["ENGINE"]), - help_text="The raw, text-only data of the document. This field is primarily used for searching.", - ), - ), - migrations.AlterField( - model_name="correspondent", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (1, "Any"), - (2, "All"), - (3, "Literal"), - (4, "Regular Expression"), - (5, "Fuzzy Match"), - ], - default=1, - help_text='Which algorithm you want to use when matching text to the OCR\'d PDF. Here, "any" looks for any occurrence of any word provided in the PDF, while "all" requires that every word provided appear in the PDF, albeit not in the order provided. A "literal" match means that the text you enter must appear in the PDF exactly as you\'ve entered it, and "regular expression" uses a regex to match the PDF. (If you don\'t know what a regex is, you probably don\'t want this option.) Finally, a "fuzzy match" looks for words or phrases that are mostly—but not exactly—the same, which can be useful for matching against documents containing imperfections that foil accurate OCR.', - ), - ), - migrations.AlterField( - model_name="tag", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (1, "Any"), - (2, "All"), - (3, "Literal"), - (4, "Regular Expression"), - (5, "Fuzzy Match"), - ], - default=1, - help_text='Which algorithm you want to use when matching text to the OCR\'d PDF. Here, "any" looks for any occurrence of any word provided in the PDF, while "all" requires that every word provided appear in the PDF, albeit not in the order provided. A "literal" match means that the text you enter must appear in the PDF exactly as you\'ve entered it, and "regular expression" uses a regex to match the PDF. (If you don\'t know what a regex is, you probably don\'t want this option.) Finally, a "fuzzy match" looks for words or phrases that are mostly—but not exactly—the same, which can be useful for matching against documents containing imperfections that foil accurate OCR.', - ), - ), - migrations.AlterField( - model_name="document", - name="correspondent", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="documents", - to="documents.correspondent", - ), - ), - ] diff --git a/src/documents/migrations/0016_auto_20170325_1558.py b/src/documents/migrations/0016_auto_20170325_1558.py deleted file mode 100644 index 743097d0c..000000000 --- a/src/documents/migrations/0016_auto_20170325_1558.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 1.10.5 on 2017-03-25 15:58 - -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "0015_add_insensitive_to_match"), - ] - - operations = [ - migrations.AlterField( - model_name="document", - name="content", - field=models.TextField( - blank=True, - db_index=("mysql" not in settings.DATABASES["default"]["ENGINE"]), - help_text="The raw, text-only data of the document. This field is primarily used for searching.", - ), - ), - ] diff --git a/src/documents/migrations/0017_auto_20170512_0507.py b/src/documents/migrations/0017_auto_20170512_0507.py deleted file mode 100644 index b9477a06c..000000000 --- a/src/documents/migrations/0017_auto_20170512_0507.py +++ /dev/null @@ -1,43 +0,0 @@ -# Generated by Django 1.10.5 on 2017-05-12 05:07 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "0016_auto_20170325_1558"), - ] - - operations = [ - migrations.AlterField( - model_name="correspondent", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (1, "Any"), - (2, "All"), - (3, "Literal"), - (4, "Regular Expression"), - (5, "Fuzzy Match"), - ], - default=1, - help_text='Which algorithm you want to use when matching text to the OCR\'d PDF. Here, "any" looks for any occurrence of any word provided in the PDF, while "all" requires that every word provided appear in the PDF, albeit not in the order provided. A "literal" match means that the text you enter must appear in the PDF exactly as you\'ve entered it, and "regular expression" uses a regex to match the PDF. (If you don\'t know what a regex is, you probably don\'t want this option.) Finally, a "fuzzy match" looks for words or phrases that are mostly—but not exactly—the same, which can be useful for matching against documents containing imperfections that foil accurate OCR.', - ), - ), - migrations.AlterField( - model_name="tag", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (1, "Any"), - (2, "All"), - (3, "Literal"), - (4, "Regular Expression"), - (5, "Fuzzy Match"), - ], - default=1, - help_text='Which algorithm you want to use when matching text to the OCR\'d PDF. Here, "any" looks for any occurrence of any word provided in the PDF, while "all" requires that every word provided appear in the PDF, albeit not in the order provided. A "literal" match means that the text you enter must appear in the PDF exactly as you\'ve entered it, and "regular expression" uses a regex to match the PDF. (If you don\'t know what a regex is, you probably don\'t want this option.) Finally, a "fuzzy match" looks for words or phrases that are mostly—but not exactly—the same, which can be useful for matching against documents containing imperfections that foil accurate OCR.', - ), - ), - ] diff --git a/src/documents/migrations/0018_auto_20170715_1712.py b/src/documents/migrations/0018_auto_20170715_1712.py deleted file mode 100644 index 838d79ddc..000000000 --- a/src/documents/migrations/0018_auto_20170715_1712.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 1.10.5 on 2017-07-15 17:12 - -import django.db.models.deletion -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "0017_auto_20170512_0507"), - ] - - operations = [ - migrations.AlterField( - model_name="document", - name="correspondent", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="documents", - to="documents.Correspondent", - ), - ), - ] diff --git a/src/documents/migrations/0019_add_consumer_user.py b/src/documents/migrations/0019_add_consumer_user.py deleted file mode 100644 index b38d88538..000000000 --- a/src/documents/migrations/0019_add_consumer_user.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 1.10.5 on 2017-07-15 17:12 - -from django.contrib.auth.models import User -from django.db import migrations - - -def forwards_func(apps, schema_editor): - User.objects.create(username="consumer") - - -def reverse_func(apps, schema_editor): - User.objects.get(username="consumer").delete() - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "0018_auto_20170715_1712"), - ] - - operations = [ - migrations.RunPython(forwards_func, reverse_func), - ] diff --git a/src/documents/migrations/0020_document_added.py b/src/documents/migrations/0020_document_added.py deleted file mode 100644 index 34042eedf..000000000 --- a/src/documents/migrations/0020_document_added.py +++ /dev/null @@ -1,29 +0,0 @@ -import django.utils.timezone -from django.db import migrations -from django.db import models - - -def set_added_time_to_created_time(apps, schema_editor): - Document = apps.get_model("documents", "Document") - for doc in Document.objects.all(): - doc.added = doc.created - doc.save() - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "0019_add_consumer_user"), - ] - - operations = [ - migrations.AddField( - model_name="document", - name="added", - field=models.DateTimeField( - db_index=True, - default=django.utils.timezone.now, - editable=False, - ), - ), - migrations.RunPython(set_added_time_to_created_time), - ] diff --git a/src/documents/migrations/0021_document_storage_type.py b/src/documents/migrations/0021_document_storage_type.py deleted file mode 100644 index b35fe75ed..000000000 --- a/src/documents/migrations/0021_document_storage_type.py +++ /dev/null @@ -1,41 +0,0 @@ -# Generated by Django 1.11.10 on 2018-02-04 13:07 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "0020_document_added"), - ] - - operations = [ - # Add the field with the default GPG-encrypted value - migrations.AddField( - model_name="document", - name="storage_type", - field=models.CharField( - choices=[ - ("unencrypted", "Unencrypted"), - ("gpg", "Encrypted with GNU Privacy Guard"), - ], - default="gpg", - editable=False, - max_length=11, - ), - ), - # Now that the field is added, change the default to unencrypted - migrations.AlterField( - model_name="document", - name="storage_type", - field=models.CharField( - choices=[ - ("unencrypted", "Unencrypted"), - ("gpg", "Encrypted with GNU Privacy Guard"), - ], - default="unencrypted", - editable=False, - max_length=11, - ), - ), - ] diff --git a/src/documents/migrations/0022_auto_20181007_1420.py b/src/documents/migrations/0022_auto_20181007_1420.py deleted file mode 100644 index 02dfa6d2b..000000000 --- a/src/documents/migrations/0022_auto_20181007_1420.py +++ /dev/null @@ -1,61 +0,0 @@ -# Generated by Django 2.0.8 on 2018-10-07 14:20 - -from django.db import migrations -from django.db import models -from django.utils.text import slugify - - -def re_slug_all_the_things(apps, schema_editor): - """ - Rewrite all slug values to make sure they're actually slugs before we brand - them as uneditable. - """ - - Tag = apps.get_model("documents", "Tag") - Correspondent = apps.get_model("documents", "Correspondent") - - for klass in (Tag, Correspondent): - for instance in klass.objects.all(): - klass.objects.filter(pk=instance.pk).update(slug=slugify(instance.slug)) - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "0021_document_storage_type"), - ] - - operations = [ - migrations.AlterModelOptions( - name="tag", - options={"ordering": ("name",)}, - ), - migrations.AlterField( - model_name="correspondent", - name="slug", - field=models.SlugField(blank=True, editable=False), - ), - migrations.AlterField( - model_name="document", - name="file_type", - field=models.CharField( - choices=[ - ("pdf", "PDF"), - ("png", "PNG"), - ("jpg", "JPG"), - ("gif", "GIF"), - ("tiff", "TIFF"), - ("txt", "TXT"), - ("csv", "CSV"), - ("md", "MD"), - ], - editable=False, - max_length=4, - ), - ), - migrations.AlterField( - model_name="tag", - name="slug", - field=models.SlugField(blank=True, editable=False), - ), - migrations.RunPython(re_slug_all_the_things, migrations.RunPython.noop), - ] diff --git a/src/documents/migrations/0023_document_current_filename.py b/src/documents/migrations/0023_document_current_filename.py deleted file mode 100644 index 5f52e1c89..000000000 --- a/src/documents/migrations/0023_document_current_filename.py +++ /dev/null @@ -1,39 +0,0 @@ -# Generated by Django 2.0.10 on 2019-04-26 18:57 - -from django.db import migrations -from django.db import models - - -def set_filename(apps, schema_editor): - Document = apps.get_model("documents", "Document") - for doc in Document.objects.all(): - file_name = f"{doc.pk:07}.{doc.file_type}" - if doc.storage_type == "gpg": - file_name += ".gpg" - - # Set filename - doc.filename = file_name - - # Save document - doc.save() - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "0022_auto_20181007_1420"), - ] - - operations = [ - migrations.AddField( - model_name="document", - name="filename", - field=models.FilePathField( - default=None, - null=True, - editable=False, - help_text="Current filename in storage", - max_length=256, - ), - ), - migrations.RunPython(set_filename), - ] diff --git a/src/documents/migrations/1000_update_paperless_all.py b/src/documents/migrations/1000_update_paperless_all.py deleted file mode 100644 index ae0d217f6..000000000 --- a/src/documents/migrations/1000_update_paperless_all.py +++ /dev/null @@ -1,147 +0,0 @@ -# Generated by Django 3.1.3 on 2020-11-07 12:35 -import uuid - -import django.db.models.deletion -from django.db import migrations -from django.db import models - - -def logs_set_default_group(apps, schema_editor): - Log = apps.get_model("documents", "Log") - for log in Log.objects.all(): - if log.group is None: - log.group = uuid.uuid4() - log.save() - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "0023_document_current_filename"), - ] - - operations = [ - migrations.AddField( - model_name="document", - name="archive_serial_number", - field=models.IntegerField( - blank=True, - db_index=True, - help_text="The position of this document in your physical document archive.", - null=True, - unique=True, - ), - ), - migrations.AddField( - model_name="tag", - name="is_inbox_tag", - field=models.BooleanField( - default=False, - help_text="Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags.", - ), - ), - migrations.CreateModel( - name="DocumentType", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("name", models.CharField(max_length=128, unique=True)), - ("slug", models.SlugField(blank=True, editable=False)), - ("match", models.CharField(blank=True, max_length=256)), - ( - "matching_algorithm", - models.PositiveIntegerField( - choices=[ - (1, "Any"), - (2, "All"), - (3, "Literal"), - (4, "Regular Expression"), - (5, "Fuzzy Match"), - (6, "Automatic Classification"), - ], - default=1, - help_text='Which algorithm you want to use when matching text to the OCR\'d PDF. Here, "any" looks for any occurrence of any word provided in the PDF, while "all" requires that every word provided appear in the PDF, albeit not in the order provided. A "literal" match means that the text you enter must appear in the PDF exactly as you\'ve entered it, and "regular expression" uses a regex to match the PDF. (If you don\'t know what a regex is, you probably don\'t want this option.) Finally, a "fuzzy match" looks for words or phrases that are mostly—but not exactly—the same, which can be useful for matching against documents containing imperfections that foil accurate OCR.', - ), - ), - ("is_insensitive", models.BooleanField(default=True)), - ], - options={ - "abstract": False, - "ordering": ("name",), - }, - ), - migrations.AddField( - model_name="document", - name="document_type", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="documents", - to="documents.documenttype", - ), - ), - migrations.AlterField( - model_name="correspondent", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (1, "Any"), - (2, "All"), - (3, "Literal"), - (4, "Regular Expression"), - (5, "Fuzzy Match"), - (6, "Automatic Classification"), - ], - default=1, - help_text='Which algorithm you want to use when matching text to the OCR\'d PDF. Here, "any" looks for any occurrence of any word provided in the PDF, while "all" requires that every word provided appear in the PDF, albeit not in the order provided. A "literal" match means that the text you enter must appear in the PDF exactly as you\'ve entered it, and "regular expression" uses a regex to match the PDF. (If you don\'t know what a regex is, you probably don\'t want this option.) Finally, a "fuzzy match" looks for words or phrases that are mostly—but not exactly—the same, which can be useful for matching against documents containing imperfections that foil accurate OCR.', - ), - ), - migrations.AlterField( - model_name="tag", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (1, "Any"), - (2, "All"), - (3, "Literal"), - (4, "Regular Expression"), - (5, "Fuzzy Match"), - (6, "Automatic Classification"), - ], - default=1, - help_text='Which algorithm you want to use when matching text to the OCR\'d PDF. Here, "any" looks for any occurrence of any word provided in the PDF, while "all" requires that every word provided appear in the PDF, albeit not in the order provided. A "literal" match means that the text you enter must appear in the PDF exactly as you\'ve entered it, and "regular expression" uses a regex to match the PDF. (If you don\'t know what a regex is, you probably don\'t want this option.) Finally, a "fuzzy match" looks for words or phrases that are mostly—but not exactly—the same, which can be useful for matching against documents containing imperfections that foil accurate OCR.', - ), - ), - migrations.AlterField( - model_name="document", - name="content", - field=models.TextField( - blank=True, - help_text="The raw, text-only data of the document. This field is primarily used for searching.", - ), - ), - migrations.AlterModelOptions( - name="log", - options={"ordering": ("-created",)}, - ), - migrations.RemoveField( - model_name="log", - name="modified", - ), - migrations.AlterField( - model_name="log", - name="group", - field=models.UUIDField(blank=True, null=True), - ), - migrations.RunPython( - code=django.db.migrations.operations.special.RunPython.noop, - reverse_code=logs_set_default_group, - ), - ] diff --git a/src/documents/migrations/1001_auto_20201109_1636.py b/src/documents/migrations/1001_auto_20201109_1636.py deleted file mode 100644 index 7477a118c..000000000 --- a/src/documents/migrations/1001_auto_20201109_1636.py +++ /dev/null @@ -1,13 +0,0 @@ -# Generated by Django 3.1.3 on 2020-11-09 16:36 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1000_update_paperless_all"), - ] - - operations = [ - migrations.RunPython(migrations.RunPython.noop, migrations.RunPython.noop), - ] diff --git a/src/documents/migrations/1002_auto_20201111_1105.py b/src/documents/migrations/1002_auto_20201111_1105.py deleted file mode 100644 index 1835c4ca9..000000000 --- a/src/documents/migrations/1002_auto_20201111_1105.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 3.1.3 on 2020-11-11 11:05 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1001_auto_20201109_1636"), - ] - - operations = [ - migrations.AlterField( - model_name="document", - name="filename", - field=models.FilePathField( - default=None, - editable=False, - help_text="Current filename in storage", - max_length=1024, - null=True, - ), - ), - ] diff --git a/src/documents/migrations/1003_mime_types.py b/src/documents/migrations/1003_mime_types.py deleted file mode 100644 index 4c7ddb492..000000000 --- a/src/documents/migrations/1003_mime_types.py +++ /dev/null @@ -1,92 +0,0 @@ -# Generated by Django 3.1.3 on 2020-11-20 11:21 -from pathlib import Path - -import magic -from django.conf import settings -from django.db import migrations -from django.db import models - -from paperless.db import GnuPG - -STORAGE_TYPE_UNENCRYPTED = "unencrypted" -STORAGE_TYPE_GPG = "gpg" - - -def source_path(self) -> Path: - if self.filename: - fname: str = str(self.filename) - else: - fname = f"{self.pk:07}.{self.file_type}" - if self.storage_type == STORAGE_TYPE_GPG: - fname += ".gpg" - - return Path(settings.ORIGINALS_DIR) / fname - - -def add_mime_types(apps, schema_editor): - Document = apps.get_model("documents", "Document") - documents = Document.objects.all() - - for d in documents: - with Path(source_path(d)).open("rb") as f: - if d.storage_type == STORAGE_TYPE_GPG: - data = GnuPG.decrypted(f) - else: - data = f.read(1024) - - d.mime_type = magic.from_buffer(data, mime=True) - d.save() - - -def add_file_extensions(apps, schema_editor): - Document = apps.get_model("documents", "Document") - documents = Document.objects.all() - - for d in documents: - d.file_type = Path(d.filename).suffix.lstrip(".") - d.save() - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1002_auto_20201111_1105"), - ] - - operations = [ - migrations.AddField( - model_name="document", - name="mime_type", - field=models.CharField(default="-", editable=False, max_length=256), - preserve_default=False, - ), - migrations.RunPython(add_mime_types, migrations.RunPython.noop), - # This operation is here so that we can revert the entire migration: - # By allowing this field to be blank and null, we can revert the - # remove operation further down and the database won't complain about - # NOT NULL violations. - migrations.AlterField( - model_name="document", - name="file_type", - field=models.CharField( - choices=[ - ("pdf", "PDF"), - ("png", "PNG"), - ("jpg", "JPG"), - ("gif", "GIF"), - ("tiff", "TIFF"), - ("txt", "TXT"), - ("csv", "CSV"), - ("md", "MD"), - ], - editable=False, - max_length=4, - null=True, - blank=True, - ), - ), - migrations.RunPython(migrations.RunPython.noop, add_file_extensions), - migrations.RemoveField( - model_name="document", - name="file_type", - ), - ] diff --git a/src/documents/migrations/1004_sanity_check_schedule.py b/src/documents/migrations/1004_sanity_check_schedule.py deleted file mode 100644 index 018cf2492..000000000 --- a/src/documents/migrations/1004_sanity_check_schedule.py +++ /dev/null @@ -1,12 +0,0 @@ -# Generated by Django 3.1.3 on 2020-11-25 14:53 - -from django.db import migrations -from django.db.migrations import RunPython - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1003_mime_types"), - ] - - operations = [RunPython(migrations.RunPython.noop, migrations.RunPython.noop)] diff --git a/src/documents/migrations/1005_checksums.py b/src/documents/migrations/1005_checksums.py deleted file mode 100644 index 4637e06ce..000000000 --- a/src/documents/migrations/1005_checksums.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 3.1.3 on 2020-11-29 00:48 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1004_sanity_check_schedule"), - ] - - operations = [ - migrations.AddField( - model_name="document", - name="archive_checksum", - field=models.CharField( - blank=True, - editable=False, - help_text="The checksum of the archived document.", - max_length=32, - null=True, - ), - ), - migrations.AlterField( - model_name="document", - name="checksum", - field=models.CharField( - editable=False, - help_text="The checksum of the original document.", - max_length=32, - unique=True, - ), - ), - ] diff --git a/src/documents/migrations/1006_auto_20201208_2209.py b/src/documents/migrations/1006_auto_20201208_2209.py deleted file mode 100644 index 425f0a768..000000000 --- a/src/documents/migrations/1006_auto_20201208_2209.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 3.1.4 on 2020-12-08 22:09 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1005_checksums"), - ] - - operations = [ - migrations.RemoveField( - model_name="correspondent", - name="slug", - ), - migrations.RemoveField( - model_name="documenttype", - name="slug", - ), - migrations.RemoveField( - model_name="tag", - name="slug", - ), - ] diff --git a/src/documents/migrations/1006_auto_20201208_2209_squashed_1011_auto_20210101_2340.py b/src/documents/migrations/1006_auto_20201208_2209_squashed_1011_auto_20210101_2340.py deleted file mode 100644 index aa8cb6deb..000000000 --- a/src/documents/migrations/1006_auto_20201208_2209_squashed_1011_auto_20210101_2340.py +++ /dev/null @@ -1,485 +0,0 @@ -# Generated by Django 4.2.13 on 2024-06-28 18:01 - -import django.db.models.deletion -import django.utils.timezone -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - replaces = [ - ("documents", "1006_auto_20201208_2209"), - ("documents", "1007_savedview_savedviewfilterrule"), - ("documents", "1008_auto_20201216_1736"), - ("documents", "1009_auto_20201216_2005"), - ("documents", "1010_auto_20210101_2159"), - ("documents", "1011_auto_20210101_2340"), - ] - - dependencies = [ - ("documents", "1005_checksums"), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.RemoveField( - model_name="correspondent", - name="slug", - ), - migrations.RemoveField( - model_name="documenttype", - name="slug", - ), - migrations.RemoveField( - model_name="tag", - name="slug", - ), - migrations.CreateModel( - name="SavedView", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("name", models.CharField(max_length=128, verbose_name="name")), - ( - "show_on_dashboard", - models.BooleanField(verbose_name="show on dashboard"), - ), - ( - "show_in_sidebar", - models.BooleanField(verbose_name="show in sidebar"), - ), - ( - "sort_field", - models.CharField(max_length=128, verbose_name="sort field"), - ), - ( - "sort_reverse", - models.BooleanField(default=False, verbose_name="sort reverse"), - ), - ( - "user", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL, - verbose_name="user", - ), - ), - ], - options={ - "ordering": ("name",), - "verbose_name": "saved view", - "verbose_name_plural": "saved views", - }, - ), - migrations.AlterModelOptions( - name="correspondent", - options={ - "ordering": ("name",), - "verbose_name": "correspondent", - "verbose_name_plural": "correspondents", - }, - ), - migrations.AlterModelOptions( - name="document", - options={ - "ordering": ("-created",), - "verbose_name": "document", - "verbose_name_plural": "documents", - }, - ), - migrations.AlterModelOptions( - name="documenttype", - options={ - "verbose_name": "document type", - "verbose_name_plural": "document types", - }, - ), - migrations.AlterModelOptions( - name="log", - options={ - "ordering": ("-created",), - "verbose_name": "log", - "verbose_name_plural": "logs", - }, - ), - migrations.AlterModelOptions( - name="tag", - options={"verbose_name": "tag", "verbose_name_plural": "tags"}, - ), - migrations.AlterField( - model_name="correspondent", - name="is_insensitive", - field=models.BooleanField(default=True, verbose_name="is insensitive"), - ), - migrations.AlterField( - model_name="correspondent", - name="match", - field=models.CharField(blank=True, max_length=256, verbose_name="match"), - ), - migrations.AlterField( - model_name="correspondent", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - (6, "Automatic"), - ], - default=1, - verbose_name="matching algorithm", - ), - ), - migrations.AlterField( - model_name="correspondent", - name="name", - field=models.CharField(max_length=128, unique=True, verbose_name="name"), - ), - migrations.AlterField( - model_name="document", - name="added", - field=models.DateTimeField( - db_index=True, - default=django.utils.timezone.now, - editable=False, - verbose_name="added", - ), - ), - migrations.AlterField( - model_name="document", - name="archive_checksum", - field=models.CharField( - blank=True, - editable=False, - help_text="The checksum of the archived document.", - max_length=32, - null=True, - verbose_name="archive checksum", - ), - ), - migrations.AlterField( - model_name="document", - name="archive_serial_number", - field=models.IntegerField( - blank=True, - db_index=True, - help_text="The position of this document in your physical document archive.", - null=True, - unique=True, - verbose_name="archive serial number", - ), - ), - migrations.AlterField( - model_name="document", - name="checksum", - field=models.CharField( - editable=False, - help_text="The checksum of the original document.", - max_length=32, - unique=True, - verbose_name="checksum", - ), - ), - migrations.AlterField( - model_name="document", - name="content", - field=models.TextField( - blank=True, - help_text="The raw, text-only data of the document. This field is primarily used for searching.", - verbose_name="content", - ), - ), - migrations.AlterField( - model_name="document", - name="correspondent", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="documents", - to="documents.correspondent", - verbose_name="correspondent", - ), - ), - migrations.AlterField( - model_name="document", - name="created", - field=models.DateTimeField( - db_index=True, - default=django.utils.timezone.now, - verbose_name="created", - ), - ), - migrations.AlterField( - model_name="document", - name="document_type", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="documents", - to="documents.documenttype", - verbose_name="document type", - ), - ), - migrations.AlterField( - model_name="document", - name="filename", - field=models.FilePathField( - default=None, - editable=False, - help_text="Current filename in storage", - max_length=1024, - null=True, - verbose_name="filename", - ), - ), - migrations.AlterField( - model_name="document", - name="mime_type", - field=models.CharField( - editable=False, - max_length=256, - verbose_name="mime type", - ), - ), - migrations.AlterField( - model_name="document", - name="modified", - field=models.DateTimeField( - auto_now=True, - db_index=True, - verbose_name="modified", - ), - ), - migrations.AlterField( - model_name="document", - name="storage_type", - field=models.CharField( - choices=[ - ("unencrypted", "Unencrypted"), - ("gpg", "Encrypted with GNU Privacy Guard"), - ], - default="unencrypted", - editable=False, - max_length=11, - verbose_name="storage type", - ), - ), - migrations.AlterField( - model_name="document", - name="tags", - field=models.ManyToManyField( - blank=True, - related_name="documents", - to="documents.tag", - verbose_name="tags", - ), - ), - migrations.AlterField( - model_name="document", - name="title", - field=models.CharField( - blank=True, - db_index=True, - max_length=128, - verbose_name="title", - ), - ), - migrations.AlterField( - model_name="documenttype", - name="is_insensitive", - field=models.BooleanField(default=True, verbose_name="is insensitive"), - ), - migrations.AlterField( - model_name="documenttype", - name="match", - field=models.CharField(blank=True, max_length=256, verbose_name="match"), - ), - migrations.AlterField( - model_name="documenttype", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - (6, "Automatic"), - ], - default=1, - verbose_name="matching algorithm", - ), - ), - migrations.AlterField( - model_name="documenttype", - name="name", - field=models.CharField(max_length=128, unique=True, verbose_name="name"), - ), - migrations.AlterField( - model_name="log", - name="created", - field=models.DateTimeField(auto_now_add=True, verbose_name="created"), - ), - migrations.AlterField( - model_name="log", - name="group", - field=models.UUIDField(blank=True, null=True, verbose_name="group"), - ), - migrations.AlterField( - model_name="log", - name="level", - field=models.PositiveIntegerField( - choices=[ - (10, "debug"), - (20, "information"), - (30, "warning"), - (40, "error"), - (50, "critical"), - ], - default=20, - verbose_name="level", - ), - ), - migrations.AlterField( - model_name="log", - name="message", - field=models.TextField(verbose_name="message"), - ), - migrations.CreateModel( - name="SavedViewFilterRule", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "rule_type", - models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - ], - verbose_name="rule type", - ), - ), - ( - "value", - models.CharField( - blank=True, - max_length=128, - null=True, - verbose_name="value", - ), - ), - ( - "saved_view", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="filter_rules", - to="documents.savedview", - verbose_name="saved view", - ), - ), - ], - options={ - "verbose_name": "filter rule", - "verbose_name_plural": "filter rules", - }, - ), - migrations.AlterField( - model_name="tag", - name="colour", - field=models.PositiveIntegerField( - choices=[ - (1, "#a6cee3"), - (2, "#1f78b4"), - (3, "#b2df8a"), - (4, "#33a02c"), - (5, "#fb9a99"), - (6, "#e31a1c"), - (7, "#fdbf6f"), - (8, "#ff7f00"), - (9, "#cab2d6"), - (10, "#6a3d9a"), - (11, "#b15928"), - (12, "#000000"), - (13, "#cccccc"), - ], - default=1, - verbose_name="color", - ), - ), - migrations.AlterField( - model_name="tag", - name="is_inbox_tag", - field=models.BooleanField( - default=False, - help_text="Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags.", - verbose_name="is inbox tag", - ), - ), - migrations.AlterField( - model_name="tag", - name="is_insensitive", - field=models.BooleanField(default=True, verbose_name="is insensitive"), - ), - migrations.AlterField( - model_name="tag", - name="match", - field=models.CharField(blank=True, max_length=256, verbose_name="match"), - ), - migrations.AlterField( - model_name="tag", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - (6, "Automatic"), - ], - default=1, - verbose_name="matching algorithm", - ), - ), - migrations.AlterField( - model_name="tag", - name="name", - field=models.CharField(max_length=128, unique=True, verbose_name="name"), - ), - ] diff --git a/src/documents/migrations/1007_savedview_savedviewfilterrule.py b/src/documents/migrations/1007_savedview_savedviewfilterrule.py deleted file mode 100644 index 64564c6af..000000000 --- a/src/documents/migrations/1007_savedview_savedviewfilterrule.py +++ /dev/null @@ -1,90 +0,0 @@ -# Generated by Django 3.1.4 on 2020-12-12 14:41 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("documents", "1006_auto_20201208_2209"), - ] - - operations = [ - migrations.CreateModel( - name="SavedView", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("name", models.CharField(max_length=128)), - ("show_on_dashboard", models.BooleanField()), - ("show_in_sidebar", models.BooleanField()), - ("sort_field", models.CharField(max_length=128)), - ("sort_reverse", models.BooleanField(default=False)), - ( - "user", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL, - ), - ), - ], - ), - migrations.CreateModel( - name="SavedViewFilterRule", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "rule_type", - models.PositiveIntegerField( - choices=[ - (0, "Title contains"), - (1, "Content contains"), - (2, "ASN is"), - (3, "Correspondent is"), - (4, "Document type is"), - (5, "Is in inbox"), - (6, "Has tag"), - (7, "Has any tag"), - (8, "Created before"), - (9, "Created after"), - (10, "Created year is"), - (11, "Created month is"), - (12, "Created day is"), - (13, "Added before"), - (14, "Added after"), - (15, "Modified before"), - (16, "Modified after"), - (17, "Does not have tag"), - ], - ), - ), - ("value", models.CharField(max_length=128)), - ( - "saved_view", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="filter_rules", - to="documents.savedview", - ), - ), - ], - ), - ] diff --git a/src/documents/migrations/1008_auto_20201216_1736.py b/src/documents/migrations/1008_auto_20201216_1736.py deleted file mode 100644 index 76f0343b1..000000000 --- a/src/documents/migrations/1008_auto_20201216_1736.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.1.4 on 2020-12-16 17:36 - -import django.db.models.functions.text -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1007_savedview_savedviewfilterrule"), - ] - - operations = [ - migrations.AlterModelOptions( - name="correspondent", - options={"ordering": (django.db.models.functions.text.Lower("name"),)}, - ), - migrations.AlterModelOptions( - name="document", - options={"ordering": ("-created",)}, - ), - migrations.AlterModelOptions( - name="documenttype", - options={"ordering": (django.db.models.functions.text.Lower("name"),)}, - ), - migrations.AlterModelOptions( - name="savedview", - options={"ordering": (django.db.models.functions.text.Lower("name"),)}, - ), - migrations.AlterModelOptions( - name="tag", - options={"ordering": (django.db.models.functions.text.Lower("name"),)}, - ), - ] diff --git a/src/documents/migrations/1009_auto_20201216_2005.py b/src/documents/migrations/1009_auto_20201216_2005.py deleted file mode 100644 index 37bae8881..000000000 --- a/src/documents/migrations/1009_auto_20201216_2005.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 3.1.4 on 2020-12-16 20:05 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1008_auto_20201216_1736"), - ] - - operations = [ - migrations.AlterModelOptions( - name="correspondent", - options={"ordering": ("name",)}, - ), - migrations.AlterModelOptions( - name="documenttype", - options={"ordering": ("name",)}, - ), - migrations.AlterModelOptions( - name="savedview", - options={"ordering": ("name",)}, - ), - migrations.AlterModelOptions( - name="tag", - options={"ordering": ("name",)}, - ), - ] diff --git a/src/documents/migrations/1010_auto_20210101_2159.py b/src/documents/migrations/1010_auto_20210101_2159.py deleted file mode 100644 index 0c6a42d30..000000000 --- a/src/documents/migrations/1010_auto_20210101_2159.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.1.4 on 2021-01-01 21:59 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1009_auto_20201216_2005"), - ] - - operations = [ - migrations.AlterField( - model_name="savedviewfilterrule", - name="value", - field=models.CharField(blank=True, max_length=128, null=True), - ), - ] diff --git a/src/documents/migrations/1011_auto_20210101_2340.py b/src/documents/migrations/1011_auto_20210101_2340.py deleted file mode 100644 index dea107715..000000000 --- a/src/documents/migrations/1011_auto_20210101_2340.py +++ /dev/null @@ -1,454 +0,0 @@ -# Generated by Django 3.1.4 on 2021-01-01 23:40 - -import django.db.models.deletion -import django.utils.timezone -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("documents", "1010_auto_20210101_2159"), - ] - - operations = [ - migrations.AlterModelOptions( - name="correspondent", - options={ - "ordering": ("name",), - "verbose_name": "correspondent", - "verbose_name_plural": "correspondents", - }, - ), - migrations.AlterModelOptions( - name="document", - options={ - "ordering": ("-created",), - "verbose_name": "document", - "verbose_name_plural": "documents", - }, - ), - migrations.AlterModelOptions( - name="documenttype", - options={ - "verbose_name": "document type", - "verbose_name_plural": "document types", - }, - ), - migrations.AlterModelOptions( - name="log", - options={ - "ordering": ("-created",), - "verbose_name": "log", - "verbose_name_plural": "logs", - }, - ), - migrations.AlterModelOptions( - name="savedview", - options={ - "ordering": ("name",), - "verbose_name": "saved view", - "verbose_name_plural": "saved views", - }, - ), - migrations.AlterModelOptions( - name="savedviewfilterrule", - options={ - "verbose_name": "filter rule", - "verbose_name_plural": "filter rules", - }, - ), - migrations.AlterModelOptions( - name="tag", - options={"verbose_name": "tag", "verbose_name_plural": "tags"}, - ), - migrations.AlterField( - model_name="correspondent", - name="is_insensitive", - field=models.BooleanField(default=True, verbose_name="is insensitive"), - ), - migrations.AlterField( - model_name="correspondent", - name="match", - field=models.CharField(blank=True, max_length=256, verbose_name="match"), - ), - migrations.AlterField( - model_name="correspondent", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - (6, "Automatic"), - ], - default=1, - verbose_name="matching algorithm", - ), - ), - migrations.AlterField( - model_name="correspondent", - name="name", - field=models.CharField(max_length=128, unique=True, verbose_name="name"), - ), - migrations.AlterField( - model_name="document", - name="added", - field=models.DateTimeField( - db_index=True, - default=django.utils.timezone.now, - editable=False, - verbose_name="added", - ), - ), - migrations.AlterField( - model_name="document", - name="archive_checksum", - field=models.CharField( - blank=True, - editable=False, - help_text="The checksum of the archived document.", - max_length=32, - null=True, - verbose_name="archive checksum", - ), - ), - migrations.AlterField( - model_name="document", - name="archive_serial_number", - field=models.IntegerField( - blank=True, - db_index=True, - help_text="The position of this document in your physical document archive.", - null=True, - unique=True, - verbose_name="archive serial number", - ), - ), - migrations.AlterField( - model_name="document", - name="checksum", - field=models.CharField( - editable=False, - help_text="The checksum of the original document.", - max_length=32, - unique=True, - verbose_name="checksum", - ), - ), - migrations.AlterField( - model_name="document", - name="content", - field=models.TextField( - blank=True, - help_text="The raw, text-only data of the document. This field is primarily used for searching.", - verbose_name="content", - ), - ), - migrations.AlterField( - model_name="document", - name="correspondent", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="documents", - to="documents.correspondent", - verbose_name="correspondent", - ), - ), - migrations.AlterField( - model_name="document", - name="created", - field=models.DateTimeField( - db_index=True, - default=django.utils.timezone.now, - verbose_name="created", - ), - ), - migrations.AlterField( - model_name="document", - name="document_type", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="documents", - to="documents.documenttype", - verbose_name="document type", - ), - ), - migrations.AlterField( - model_name="document", - name="filename", - field=models.FilePathField( - default=None, - editable=False, - help_text="Current filename in storage", - max_length=1024, - null=True, - verbose_name="filename", - ), - ), - migrations.AlterField( - model_name="document", - name="mime_type", - field=models.CharField( - editable=False, - max_length=256, - verbose_name="mime type", - ), - ), - migrations.AlterField( - model_name="document", - name="modified", - field=models.DateTimeField( - auto_now=True, - db_index=True, - verbose_name="modified", - ), - ), - migrations.AlterField( - model_name="document", - name="storage_type", - field=models.CharField( - choices=[ - ("unencrypted", "Unencrypted"), - ("gpg", "Encrypted with GNU Privacy Guard"), - ], - default="unencrypted", - editable=False, - max_length=11, - verbose_name="storage type", - ), - ), - migrations.AlterField( - model_name="document", - name="tags", - field=models.ManyToManyField( - blank=True, - related_name="documents", - to="documents.Tag", - verbose_name="tags", - ), - ), - migrations.AlterField( - model_name="document", - name="title", - field=models.CharField( - blank=True, - db_index=True, - max_length=128, - verbose_name="title", - ), - ), - migrations.AlterField( - model_name="documenttype", - name="is_insensitive", - field=models.BooleanField(default=True, verbose_name="is insensitive"), - ), - migrations.AlterField( - model_name="documenttype", - name="match", - field=models.CharField(blank=True, max_length=256, verbose_name="match"), - ), - migrations.AlterField( - model_name="documenttype", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - (6, "Automatic"), - ], - default=1, - verbose_name="matching algorithm", - ), - ), - migrations.AlterField( - model_name="documenttype", - name="name", - field=models.CharField(max_length=128, unique=True, verbose_name="name"), - ), - migrations.AlterField( - model_name="log", - name="created", - field=models.DateTimeField(auto_now_add=True, verbose_name="created"), - ), - migrations.AlterField( - model_name="log", - name="group", - field=models.UUIDField(blank=True, null=True, verbose_name="group"), - ), - migrations.AlterField( - model_name="log", - name="level", - field=models.PositiveIntegerField( - choices=[ - (10, "debug"), - (20, "information"), - (30, "warning"), - (40, "error"), - (50, "critical"), - ], - default=20, - verbose_name="level", - ), - ), - migrations.AlterField( - model_name="log", - name="message", - field=models.TextField(verbose_name="message"), - ), - migrations.AlterField( - model_name="savedview", - name="name", - field=models.CharField(max_length=128, verbose_name="name"), - ), - migrations.AlterField( - model_name="savedview", - name="show_in_sidebar", - field=models.BooleanField(verbose_name="show in sidebar"), - ), - migrations.AlterField( - model_name="savedview", - name="show_on_dashboard", - field=models.BooleanField(verbose_name="show on dashboard"), - ), - migrations.AlterField( - model_name="savedview", - name="sort_field", - field=models.CharField(max_length=128, verbose_name="sort field"), - ), - migrations.AlterField( - model_name="savedview", - name="sort_reverse", - field=models.BooleanField(default=False, verbose_name="sort reverse"), - ), - migrations.AlterField( - model_name="savedview", - name="user", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL, - verbose_name="user", - ), - ), - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - ], - verbose_name="rule type", - ), - ), - migrations.AlterField( - model_name="savedviewfilterrule", - name="saved_view", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="filter_rules", - to="documents.savedview", - verbose_name="saved view", - ), - ), - migrations.AlterField( - model_name="savedviewfilterrule", - name="value", - field=models.CharField( - blank=True, - max_length=128, - null=True, - verbose_name="value", - ), - ), - migrations.AlterField( - model_name="tag", - name="colour", - field=models.PositiveIntegerField( - choices=[ - (1, "#a6cee3"), - (2, "#1f78b4"), - (3, "#b2df8a"), - (4, "#33a02c"), - (5, "#fb9a99"), - (6, "#e31a1c"), - (7, "#fdbf6f"), - (8, "#ff7f00"), - (9, "#cab2d6"), - (10, "#6a3d9a"), - (11, "#b15928"), - (12, "#000000"), - (13, "#cccccc"), - ], - default=1, - verbose_name="color", - ), - ), - migrations.AlterField( - model_name="tag", - name="is_inbox_tag", - field=models.BooleanField( - default=False, - help_text="Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags.", - verbose_name="is inbox tag", - ), - ), - migrations.AlterField( - model_name="tag", - name="is_insensitive", - field=models.BooleanField(default=True, verbose_name="is insensitive"), - ), - migrations.AlterField( - model_name="tag", - name="match", - field=models.CharField(blank=True, max_length=256, verbose_name="match"), - ), - migrations.AlterField( - model_name="tag", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - (6, "Automatic"), - ], - default=1, - verbose_name="matching algorithm", - ), - ), - migrations.AlterField( - model_name="tag", - name="name", - field=models.CharField(max_length=128, unique=True, verbose_name="name"), - ), - ] diff --git a/src/documents/migrations/1012_fix_archive_files.py b/src/documents/migrations/1012_fix_archive_files.py deleted file mode 100644 index a97fa7a80..000000000 --- a/src/documents/migrations/1012_fix_archive_files.py +++ /dev/null @@ -1,367 +0,0 @@ -# Generated by Django 3.1.6 on 2021-02-07 22:26 -import datetime -import hashlib -import logging -import os -import shutil -from collections import defaultdict -from pathlib import Path -from time import sleep - -import pathvalidate -from django.conf import settings -from django.db import migrations -from django.db import models -from django.template.defaultfilters import slugify - -logger = logging.getLogger("paperless.migrations") - - -############################################################################### -# This is code copied straight paperless before the change. -############################################################################### -class defaultdictNoStr(defaultdict): - def __str__(self): # pragma: no cover - raise ValueError("Don't use {tags} directly.") - - -def many_to_dictionary(field): # pragma: no cover - # Converts ManyToManyField to dictionary by assuming, that field - # entries contain an _ or - which will be used as a delimiter - mydictionary = dict() - - for index, t in enumerate(field.all()): - # Populate tag names by index - mydictionary[index] = slugify(t.name) - - # Find delimiter - delimiter = t.name.find("_") - - if delimiter == -1: - delimiter = t.name.find("-") - - if delimiter == -1: - continue - - key = t.name[:delimiter] - value = t.name[delimiter + 1 :] - - mydictionary[slugify(key)] = slugify(value) - - return mydictionary - - -def archive_name_from_filename(filename: Path) -> Path: - return Path(filename.stem + ".pdf") - - -def archive_path_old(doc) -> Path: - if doc.filename: - fname = archive_name_from_filename(Path(doc.filename)) - else: - fname = Path(f"{doc.pk:07}.pdf") - - return settings.ARCHIVE_DIR / fname - - -STORAGE_TYPE_GPG = "gpg" - - -def archive_path_new(doc) -> Path | None: - if doc.archive_filename is not None: - return settings.ARCHIVE_DIR / doc.archive_filename - else: - return None - - -def source_path(doc) -> Path: - if doc.filename: - fname = doc.filename - else: - fname = f"{doc.pk:07}{doc.file_type}" - if doc.storage_type == STORAGE_TYPE_GPG: - fname = Path(str(fname) + ".gpg") # pragma: no cover - - return settings.ORIGINALS_DIR / fname - - -def generate_unique_filename(doc, *, archive_filename=False): - if archive_filename: - old_filename = doc.archive_filename - root = settings.ARCHIVE_DIR - else: - old_filename = doc.filename - root = settings.ORIGINALS_DIR - - counter = 0 - - while True: - new_filename = generate_filename( - doc, - counter=counter, - archive_filename=archive_filename, - ) - if new_filename == old_filename: - # still the same as before. - return new_filename - - if (root / new_filename).exists(): - counter += 1 - else: - return new_filename - - -def generate_filename(doc, *, counter=0, append_gpg=True, archive_filename=False): - path = "" - - try: - if settings.FILENAME_FORMAT is not None: - tags = defaultdictNoStr(lambda: slugify(None), many_to_dictionary(doc.tags)) - - tag_list = pathvalidate.sanitize_filename( - ",".join(sorted([tag.name for tag in doc.tags.all()])), - replacement_text="-", - ) - - if doc.correspondent: - correspondent = pathvalidate.sanitize_filename( - doc.correspondent.name, - replacement_text="-", - ) - else: - correspondent = "none" - - if doc.document_type: - document_type = pathvalidate.sanitize_filename( - doc.document_type.name, - replacement_text="-", - ) - else: - document_type = "none" - - path = settings.FILENAME_FORMAT.format( - title=pathvalidate.sanitize_filename(doc.title, replacement_text="-"), - correspondent=correspondent, - document_type=document_type, - created=datetime.date.isoformat(doc.created), - created_year=doc.created.year if doc.created else "none", - created_month=f"{doc.created.month:02}" if doc.created else "none", - created_day=f"{doc.created.day:02}" if doc.created else "none", - added=datetime.date.isoformat(doc.added), - added_year=doc.added.year if doc.added else "none", - added_month=f"{doc.added.month:02}" if doc.added else "none", - added_day=f"{doc.added.day:02}" if doc.added else "none", - tags=tags, - tag_list=tag_list, - ).strip() - - path = path.strip(os.sep) - - except (ValueError, KeyError, IndexError): - logger.warning( - f"Invalid PAPERLESS_FILENAME_FORMAT: " - f"{settings.FILENAME_FORMAT}, falling back to default", - ) - - counter_str = f"_{counter:02}" if counter else "" - - filetype_str = ".pdf" if archive_filename else doc.file_type - - if len(path) > 0: - filename = f"{path}{counter_str}{filetype_str}" - else: - filename = f"{doc.pk:07}{counter_str}{filetype_str}" - - # Append .gpg for encrypted files - if append_gpg and doc.storage_type == STORAGE_TYPE_GPG: - filename += ".gpg" - - return filename - - -############################################################################### -# This code performs bidirection archive file transformation. -############################################################################### - - -def parse_wrapper(parser, path, mime_type, file_name): - # this is here so that I can mock this out for testing. - parser.parse(path, mime_type, file_name) - - -def create_archive_version(doc, retry_count=3): - from documents.parsers import DocumentParser - from documents.parsers import ParseError - from documents.parsers import get_parser_class_for_mime_type - - logger.info(f"Regenerating archive document for document ID:{doc.id}") - parser_class = get_parser_class_for_mime_type(doc.mime_type) - for try_num in range(retry_count): - parser: DocumentParser = parser_class(None, None) - try: - parse_wrapper( - parser, - source_path(doc), - doc.mime_type, - Path(doc.filename).name, - ) - doc.content = parser.get_text() - - if parser.get_archive_path() and Path(parser.get_archive_path()).is_file(): - doc.archive_filename = generate_unique_filename( - doc, - archive_filename=True, - ) - with Path(parser.get_archive_path()).open("rb") as f: - doc.archive_checksum = hashlib.md5(f.read()).hexdigest() - archive_path_new(doc).parent.mkdir(parents=True, exist_ok=True) - shutil.copy2(parser.get_archive_path(), archive_path_new(doc)) - else: - doc.archive_checksum = None - logger.error( - f"Parser did not return an archive document for document " - f"ID:{doc.id}. Removing archive document.", - ) - doc.save() - return - except ParseError: - if try_num + 1 == retry_count: - logger.exception( - f"Unable to regenerate archive document for ID:{doc.id}. You " - f"need to invoke the document_archiver management command " - f"manually for that document.", - ) - doc.archive_checksum = None - doc.save() - return - else: - # This is mostly here for the tika parser in docker - # environments. The servers for parsing need to come up first, - # and the docker setup doesn't ensure that tika is running - # before attempting migrations. - logger.error("Parse error, will try again in 5 seconds...") - sleep(5) - finally: - parser.cleanup() - - -def move_old_to_new_locations(apps, schema_editor): - Document = apps.get_model("documents", "Document") - - affected_document_ids = set() - - old_archive_path_to_id = {} - - # check for documents that have incorrect archive versions - for doc in Document.objects.filter(archive_checksum__isnull=False): - old_path = archive_path_old(doc) - - if old_path in old_archive_path_to_id: - affected_document_ids.add(doc.id) - affected_document_ids.add(old_archive_path_to_id[old_path]) - else: - old_archive_path_to_id[old_path] = doc.id - - # check that archive files of all unaffected documents are in place - for doc in Document.objects.filter(archive_checksum__isnull=False): - old_path = archive_path_old(doc) - if doc.id not in affected_document_ids and not old_path.is_file(): - raise ValueError( - f"Archived document ID:{doc.id} does not exist at: {old_path}", - ) - - # check that we can regenerate affected archive versions - for doc_id in affected_document_ids: - from documents.parsers import get_parser_class_for_mime_type - - doc = Document.objects.get(id=doc_id) - parser_class = get_parser_class_for_mime_type(doc.mime_type) - if not parser_class: - raise ValueError( - f"Document ID:{doc.id} has an invalid archived document, " - f"but no parsers are available. Cannot migrate.", - ) - - for doc in Document.objects.filter(archive_checksum__isnull=False): - if doc.id in affected_document_ids: - old_path = archive_path_old(doc) - # remove affected archive versions - if old_path.is_file(): - logger.debug(f"Removing {old_path}") - old_path.unlink() - else: - # Set archive path for unaffected files - doc.archive_filename = archive_name_from_filename(Path(doc.filename)) - Document.objects.filter(id=doc.id).update( - archive_filename=doc.archive_filename, - ) - - # regenerate archive documents - for doc_id in affected_document_ids: - doc = Document.objects.get(id=doc_id) - create_archive_version(doc) - - -def move_new_to_old_locations(apps, schema_editor): - Document = apps.get_model("documents", "Document") - - old_archive_paths = set() - - for doc in Document.objects.filter(archive_checksum__isnull=False): - new_archive_path = archive_path_new(doc) - old_archive_path = archive_path_old(doc) - if old_archive_path in old_archive_paths: - raise ValueError( - f"Cannot migrate: Archive file name {old_archive_path} of " - f"document {doc.filename} would clash with another archive " - f"filename.", - ) - old_archive_paths.add(old_archive_path) - if new_archive_path != old_archive_path and old_archive_path.is_file(): - raise ValueError( - f"Cannot migrate: Cannot move {new_archive_path} to " - f"{old_archive_path}: file already exists.", - ) - - for doc in Document.objects.filter(archive_checksum__isnull=False): - new_archive_path = archive_path_new(doc) - old_archive_path = archive_path_old(doc) - if new_archive_path != old_archive_path: - logger.debug(f"Moving {new_archive_path} to {old_archive_path}") - shutil.move(new_archive_path, old_archive_path) - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1011_auto_20210101_2340"), - ] - - operations = [ - migrations.AddField( - model_name="document", - name="archive_filename", - field=models.FilePathField( - default=None, - editable=False, - help_text="Current archive filename in storage", - max_length=1024, - null=True, - unique=True, - verbose_name="archive filename", - ), - ), - migrations.AlterField( - model_name="document", - name="filename", - field=models.FilePathField( - default=None, - editable=False, - help_text="Current filename in storage", - max_length=1024, - null=True, - unique=True, - verbose_name="filename", - ), - ), - migrations.RunPython(move_old_to_new_locations, move_new_to_old_locations), - ] diff --git a/src/documents/migrations/1013_migrate_tag_colour.py b/src/documents/migrations/1013_migrate_tag_colour.py deleted file mode 100644 index 6cae10898..000000000 --- a/src/documents/migrations/1013_migrate_tag_colour.py +++ /dev/null @@ -1,74 +0,0 @@ -# Generated by Django 3.1.4 on 2020-12-02 21:43 - -from django.db import migrations -from django.db import models - -COLOURS_OLD = { - 1: "#a6cee3", - 2: "#1f78b4", - 3: "#b2df8a", - 4: "#33a02c", - 5: "#fb9a99", - 6: "#e31a1c", - 7: "#fdbf6f", - 8: "#ff7f00", - 9: "#cab2d6", - 10: "#6a3d9a", - 11: "#b15928", - 12: "#000000", - 13: "#cccccc", -} - - -def forward(apps, schema_editor): - Tag = apps.get_model("documents", "Tag") - - for tag in Tag.objects.all(): - colour_old_id = tag.colour_old - rgb = COLOURS_OLD[colour_old_id] - tag.color = rgb - tag.save() - - -def reverse(apps, schema_editor): - Tag = apps.get_model("documents", "Tag") - - def _get_colour_id(rdb): - for idx, rdbx in COLOURS_OLD.items(): - if rdbx == rdb: - return idx - # Return colour 1 if we can't match anything - return 1 - - for tag in Tag.objects.all(): - colour_id = _get_colour_id(tag.color) - tag.colour_old = colour_id - tag.save() - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1012_fix_archive_files"), - ] - - operations = [ - migrations.RenameField( - model_name="tag", - old_name="colour", - new_name="colour_old", - ), - migrations.AddField( - model_name="tag", - name="color", - field=models.CharField( - default="#a6cee3", - max_length=7, - verbose_name="color", - ), - ), - migrations.RunPython(forward, reverse), - migrations.RemoveField( - model_name="tag", - name="colour_old", - ), - ] diff --git a/src/documents/migrations/1014_auto_20210228_1614.py b/src/documents/migrations/1014_auto_20210228_1614.py deleted file mode 100644 index 5785bcb53..000000000 --- a/src/documents/migrations/1014_auto_20210228_1614.py +++ /dev/null @@ -1,42 +0,0 @@ -# Generated by Django 3.1.7 on 2021-02-28 15:14 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1013_migrate_tag_colour"), - ] - - operations = [ - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - (18, "does not have ASN"), - (19, "title or content contains"), - ], - verbose_name="rule type", - ), - ), - ] diff --git a/src/documents/migrations/1015_remove_null_characters.py b/src/documents/migrations/1015_remove_null_characters.py deleted file mode 100644 index 9872b3a75..000000000 --- a/src/documents/migrations/1015_remove_null_characters.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 3.1.7 on 2021-04-04 18:28 -import logging - -from django.db import migrations - -logger = logging.getLogger("paperless.migrations") - - -def remove_null_characters(apps, schema_editor): - Document = apps.get_model("documents", "Document") - - for doc in Document.objects.all(): - content: str = doc.content - if "\0" in content: - logger.info(f"Removing null characters from document {doc}...") - doc.content = content.replace("\0", " ") - doc.save() - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1014_auto_20210228_1614"), - ] - - operations = [ - migrations.RunPython(remove_null_characters, migrations.RunPython.noop), - ] diff --git a/src/documents/migrations/1016_auto_20210317_1351.py b/src/documents/migrations/1016_auto_20210317_1351.py deleted file mode 100644 index 67147fd4c..000000000 --- a/src/documents/migrations/1016_auto_20210317_1351.py +++ /dev/null @@ -1,54 +0,0 @@ -# Generated by Django 3.1.7 on 2021-03-17 12:51 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1015_remove_null_characters"), - ] - - operations = [ - migrations.AlterField( - model_name="savedview", - name="sort_field", - field=models.CharField( - blank=True, - max_length=128, - null=True, - verbose_name="sort field", - ), - ), - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - (18, "does not have ASN"), - (19, "title or content contains"), - (20, "fulltext query"), - (21, "more like this"), - ], - verbose_name="rule type", - ), - ), - ] diff --git a/src/documents/migrations/1016_auto_20210317_1351_squashed_1020_merge_20220518_1839.py b/src/documents/migrations/1016_auto_20210317_1351_squashed_1020_merge_20220518_1839.py deleted file mode 100644 index a7f92e931..000000000 --- a/src/documents/migrations/1016_auto_20210317_1351_squashed_1020_merge_20220518_1839.py +++ /dev/null @@ -1,190 +0,0 @@ -# Generated by Django 4.2.13 on 2024-06-28 18:09 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - replaces = [ - ("documents", "1016_auto_20210317_1351"), - ("documents", "1017_alter_savedviewfilterrule_rule_type"), - ("documents", "1018_alter_savedviewfilterrule_value"), - ("documents", "1019_uisettings"), - ("documents", "1019_storagepath_document_storage_path"), - ("documents", "1020_merge_20220518_1839"), - ] - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("documents", "1015_remove_null_characters"), - ] - - operations = [ - migrations.AlterField( - model_name="savedview", - name="sort_field", - field=models.CharField( - blank=True, - max_length=128, - null=True, - verbose_name="sort field", - ), - ), - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - (18, "does not have ASN"), - (19, "title or content contains"), - (20, "fulltext query"), - (21, "more like this"), - ], - verbose_name="rule type", - ), - ), - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - (18, "does not have ASN"), - (19, "title or content contains"), - (20, "fulltext query"), - (21, "more like this"), - (22, "has tags in"), - ], - verbose_name="rule type", - ), - ), - migrations.AlterField( - model_name="savedviewfilterrule", - name="value", - field=models.CharField( - blank=True, - max_length=255, - null=True, - verbose_name="value", - ), - ), - migrations.CreateModel( - name="UiSettings", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("settings", models.JSONField(null=True)), - ( - "user", - models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - related_name="ui_settings", - to=settings.AUTH_USER_MODEL, - ), - ), - ], - ), - migrations.CreateModel( - name="StoragePath", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "name", - models.CharField(max_length=128, unique=True, verbose_name="name"), - ), - ( - "match", - models.CharField(blank=True, max_length=256, verbose_name="match"), - ), - ( - "matching_algorithm", - models.PositiveIntegerField( - choices=[ - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - (6, "Automatic"), - ], - default=1, - verbose_name="matching algorithm", - ), - ), - ( - "is_insensitive", - models.BooleanField(default=True, verbose_name="is insensitive"), - ), - ("path", models.CharField(max_length=512, verbose_name="path")), - ], - options={ - "verbose_name": "storage path", - "verbose_name_plural": "storage paths", - "ordering": ("name",), - }, - ), - migrations.AddField( - model_name="document", - name="storage_path", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="documents", - to="documents.storagepath", - verbose_name="storage path", - ), - ), - ] diff --git a/src/documents/migrations/1017_alter_savedviewfilterrule_rule_type.py b/src/documents/migrations/1017_alter_savedviewfilterrule_rule_type.py deleted file mode 100644 index ab18f1bc1..000000000 --- a/src/documents/migrations/1017_alter_savedviewfilterrule_rule_type.py +++ /dev/null @@ -1,45 +0,0 @@ -# Generated by Django 3.2.12 on 2022-03-17 11:59 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1016_auto_20210317_1351"), - ] - - operations = [ - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - (18, "does not have ASN"), - (19, "title or content contains"), - (20, "fulltext query"), - (21, "more like this"), - (22, "has tags in"), - ], - verbose_name="rule type", - ), - ), - ] diff --git a/src/documents/migrations/1018_alter_savedviewfilterrule_value.py b/src/documents/migrations/1018_alter_savedviewfilterrule_value.py deleted file mode 100644 index 95ef4861d..000000000 --- a/src/documents/migrations/1018_alter_savedviewfilterrule_value.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 4.0.3 on 2022-04-01 22:50 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1017_alter_savedviewfilterrule_rule_type"), - ] - - operations = [ - migrations.AlterField( - model_name="savedviewfilterrule", - name="value", - field=models.CharField( - blank=True, - max_length=255, - null=True, - verbose_name="value", - ), - ), - ] diff --git a/src/documents/migrations/1019_storagepath_document_storage_path.py b/src/documents/migrations/1019_storagepath_document_storage_path.py deleted file mode 100644 index b09941bf5..000000000 --- a/src/documents/migrations/1019_storagepath_document_storage_path.py +++ /dev/null @@ -1,73 +0,0 @@ -# Generated by Django 4.0.4 on 2022-05-02 15:56 - -import django.db.models.deletion -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1018_alter_savedviewfilterrule_value"), - ] - - operations = [ - migrations.CreateModel( - name="StoragePath", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "name", - models.CharField(max_length=128, unique=True, verbose_name="name"), - ), - ( - "match", - models.CharField(blank=True, max_length=256, verbose_name="match"), - ), - ( - "matching_algorithm", - models.PositiveIntegerField( - choices=[ - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - (6, "Automatic"), - ], - default=1, - verbose_name="matching algorithm", - ), - ), - ( - "is_insensitive", - models.BooleanField(default=True, verbose_name="is insensitive"), - ), - ("path", models.CharField(max_length=512, verbose_name="path")), - ], - options={ - "verbose_name": "storage path", - "verbose_name_plural": "storage paths", - "ordering": ("name",), - }, - ), - migrations.AddField( - model_name="document", - name="storage_path", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="documents", - to="documents.storagepath", - verbose_name="storage path", - ), - ), - ] diff --git a/src/documents/migrations/1019_uisettings.py b/src/documents/migrations/1019_uisettings.py deleted file mode 100644 index e84138077..000000000 --- a/src/documents/migrations/1019_uisettings.py +++ /dev/null @@ -1,39 +0,0 @@ -# Generated by Django 4.0.4 on 2022-05-07 05:10 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("documents", "1018_alter_savedviewfilterrule_value"), - ] - - operations = [ - migrations.CreateModel( - name="UiSettings", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("settings", models.JSONField(null=True)), - ( - "user", - models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - related_name="ui_settings", - to=settings.AUTH_USER_MODEL, - ), - ), - ], - ), - ] diff --git a/src/documents/migrations/1020_merge_20220518_1839.py b/src/documents/migrations/1020_merge_20220518_1839.py deleted file mode 100644 index a766aaa20..000000000 --- a/src/documents/migrations/1020_merge_20220518_1839.py +++ /dev/null @@ -1,12 +0,0 @@ -# Generated by Django 4.0.4 on 2022-05-18 18:39 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1019_storagepath_document_storage_path"), - ("documents", "1019_uisettings"), - ] - - operations = [] diff --git a/src/documents/migrations/1021_webp_thumbnail_conversion.py b/src/documents/migrations/1021_webp_thumbnail_conversion.py deleted file mode 100644 index 50b12b156..000000000 --- a/src/documents/migrations/1021_webp_thumbnail_conversion.py +++ /dev/null @@ -1,104 +0,0 @@ -# Generated by Django 4.0.5 on 2022-06-11 15:40 -import logging -import multiprocessing.pool -import shutil -import tempfile -import time -from pathlib import Path - -from django.conf import settings -from django.db import migrations - -from documents.parsers import run_convert - -logger = logging.getLogger("paperless.migrations") - - -def _do_convert(work_package): - existing_thumbnail, converted_thumbnail = work_package - try: - logger.info(f"Converting thumbnail: {existing_thumbnail}") - - # Run actual conversion - run_convert( - density=300, - scale="500x5000>", - alpha="remove", - strip=True, - trim=False, - auto_orient=True, - input_file=f"{existing_thumbnail}[0]", - output_file=str(converted_thumbnail), - ) - - # Copy newly created thumbnail to thumbnail directory - shutil.copy(converted_thumbnail, existing_thumbnail.parent) - - # Remove the PNG version - existing_thumbnail.unlink() - - logger.info( - "Conversion to WebP completed, " - f"replaced {existing_thumbnail.name} with {converted_thumbnail.name}", - ) - - except Exception as e: - logger.error(f"Error converting thumbnail (existing file unchanged): {e}") - - -def _convert_thumbnails_to_webp(apps, schema_editor): - start = time.time() - - with tempfile.TemporaryDirectory() as tempdir: - work_packages = [] - - for file in Path(settings.THUMBNAIL_DIR).glob("*.png"): - existing_thumbnail = file.resolve() - - # Change the existing filename suffix from png to webp - converted_thumbnail_name = existing_thumbnail.with_suffix( - ".webp", - ).name - - # Create the expected output filename in the tempdir - converted_thumbnail = ( - Path(tempdir) / Path(converted_thumbnail_name) - ).resolve() - - # Package up the necessary info - work_packages.append( - (existing_thumbnail, converted_thumbnail), - ) - - if work_packages: - logger.info( - "\n\n" - " This is a one-time only migration to convert thumbnails for all of your\n" - " documents into WebP format. If you have a lot of documents though, \n" - " this may take a while, so a coffee break may be in order." - "\n", - ) - - with multiprocessing.pool.Pool( - processes=min(multiprocessing.cpu_count(), 4), - maxtasksperchild=4, - ) as pool: - pool.map(_do_convert, work_packages) - - end = time.time() - duration = end - start - - logger.info(f"Conversion completed in {duration:.3f}s") - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1020_merge_20220518_1839"), - ] - - operations = [ - migrations.RunPython( - code=_convert_thumbnails_to_webp, - reverse_code=migrations.RunPython.noop, - ), - ] diff --git a/src/documents/migrations/1022_paperlesstask.py b/src/documents/migrations/1022_paperlesstask.py deleted file mode 100644 index c7b3f7744..000000000 --- a/src/documents/migrations/1022_paperlesstask.py +++ /dev/null @@ -1,52 +0,0 @@ -# Generated by Django 4.0.4 on 2022-05-23 07:14 - -import django.db.models.deletion -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1021_webp_thumbnail_conversion"), - ] - - operations = [ - migrations.CreateModel( - name="PaperlessTask", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("task_id", models.CharField(max_length=128)), - ("name", models.CharField(max_length=256, null=True)), - ( - "created", - models.DateTimeField(auto_now=True, verbose_name="created"), - ), - ( - "started", - models.DateTimeField(null=True, verbose_name="started"), - ), - ("acknowledged", models.BooleanField(default=False)), - ( - "attempted_task", - models.OneToOneField( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="attempted_task", - # This is a dummy field, 1026 will fix up the column - # This manual change is required, as django doesn't django doesn't really support - # removing an app which has migration deps like this - to="documents.document", - ), - ), - ], - ), - ] diff --git a/src/documents/migrations/1022_paperlesstask_squashed_1036_alter_savedviewfilterrule_rule_type.py b/src/documents/migrations/1022_paperlesstask_squashed_1036_alter_savedviewfilterrule_rule_type.py deleted file mode 100644 index dc73a51a2..000000000 --- a/src/documents/migrations/1022_paperlesstask_squashed_1036_alter_savedviewfilterrule_rule_type.py +++ /dev/null @@ -1,668 +0,0 @@ -# Generated by Django 4.2.13 on 2024-06-28 18:10 - -import django.core.validators -import django.db.models.deletion -import django.utils.timezone -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - replaces = [ - ("documents", "1022_paperlesstask"), - ("documents", "1023_add_comments"), - ("documents", "1024_document_original_filename"), - ("documents", "1025_alter_savedviewfilterrule_rule_type"), - ("documents", "1026_transition_to_celery"), - ("documents", "1027_remove_paperlesstask_attempted_task_and_more"), - ("documents", "1028_remove_paperlesstask_task_args_and_more"), - ("documents", "1029_alter_document_archive_serial_number"), - ("documents", "1030_alter_paperlesstask_task_file_name"), - ("documents", "1031_remove_savedview_user_correspondent_owner_and_more"), - ("documents", "1032_alter_correspondent_matching_algorithm_and_more"), - ("documents", "1033_alter_documenttype_options_alter_tag_options_and_more"), - ("documents", "1034_alter_savedviewfilterrule_rule_type"), - ("documents", "1035_rename_comment_note"), - ("documents", "1036_alter_savedviewfilterrule_rule_type"), - ] - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("django_celery_results", "0011_taskresult_periodic_task_name"), - ("documents", "1021_webp_thumbnail_conversion"), - ] - - operations = [ - migrations.CreateModel( - name="Comment", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "comment", - models.TextField( - blank=True, - help_text="Comment for the document", - verbose_name="content", - ), - ), - ( - "created", - models.DateTimeField( - db_index=True, - default=django.utils.timezone.now, - verbose_name="created", - ), - ), - ( - "document", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="documents", - to="documents.document", - verbose_name="document", - ), - ), - ( - "user", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="users", - to=settings.AUTH_USER_MODEL, - verbose_name="user", - ), - ), - ], - options={ - "verbose_name": "comment", - "verbose_name_plural": "comments", - "ordering": ("created",), - }, - ), - migrations.AddField( - model_name="document", - name="original_filename", - field=models.CharField( - default=None, - editable=False, - help_text="The original name of the file when it was uploaded", - max_length=1024, - null=True, - verbose_name="original filename", - ), - ), - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - (18, "does not have ASN"), - (19, "title or content contains"), - (20, "fulltext query"), - (21, "more like this"), - (22, "has tags in"), - (23, "ASN greater than"), - (24, "ASN less than"), - (25, "storage path is"), - ], - verbose_name="rule type", - ), - ), - migrations.CreateModel( - name="PaperlessTask", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("task_id", models.CharField(max_length=128)), - ("acknowledged", models.BooleanField(default=False)), - ( - "attempted_task", - models.OneToOneField( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="attempted_task", - to="django_celery_results.taskresult", - ), - ), - ], - ), - migrations.RunSQL( - sql="DROP TABLE IF EXISTS django_q_ormq", - reverse_sql="", - ), - migrations.RunSQL( - sql="DROP TABLE IF EXISTS django_q_schedule", - reverse_sql="", - ), - migrations.RunSQL( - sql="DROP TABLE IF EXISTS django_q_task", - reverse_sql="", - ), - migrations.RemoveField( - model_name="paperlesstask", - name="attempted_task", - ), - migrations.AddField( - model_name="paperlesstask", - name="date_created", - field=models.DateTimeField( - default=django.utils.timezone.now, - help_text="Datetime field when the task result was created in UTC", - null=True, - verbose_name="Created DateTime", - ), - ), - migrations.AddField( - model_name="paperlesstask", - name="date_done", - field=models.DateTimeField( - default=None, - help_text="Datetime field when the task was completed in UTC", - null=True, - verbose_name="Completed DateTime", - ), - ), - migrations.AddField( - model_name="paperlesstask", - name="date_started", - field=models.DateTimeField( - default=None, - help_text="Datetime field when the task was started in UTC", - null=True, - verbose_name="Started DateTime", - ), - ), - migrations.AddField( - model_name="paperlesstask", - name="result", - field=models.TextField( - default=None, - help_text="The data returned by the task", - null=True, - verbose_name="Result Data", - ), - ), - migrations.AddField( - model_name="paperlesstask", - name="status", - field=models.CharField( - choices=[ - ("FAILURE", "FAILURE"), - ("PENDING", "PENDING"), - ("RECEIVED", "RECEIVED"), - ("RETRY", "RETRY"), - ("REVOKED", "REVOKED"), - ("STARTED", "STARTED"), - ("SUCCESS", "SUCCESS"), - ], - default="PENDING", - help_text="Current state of the task being run", - max_length=30, - verbose_name="Task State", - ), - ), - migrations.AddField( - model_name="paperlesstask", - name="task_name", - field=models.CharField( - help_text="Name of the Task which was run", - max_length=255, - null=True, - verbose_name="Task Name", - ), - ), - migrations.AlterField( - model_name="paperlesstask", - name="acknowledged", - field=models.BooleanField( - default=False, - help_text="If the task is acknowledged via the frontend or API", - verbose_name="Acknowledged", - ), - ), - migrations.AlterField( - model_name="paperlesstask", - name="task_id", - field=models.CharField( - help_text="Celery ID for the Task that was run", - max_length=255, - unique=True, - verbose_name="Task ID", - ), - ), - migrations.AlterField( - model_name="document", - name="archive_serial_number", - field=models.PositiveIntegerField( - blank=True, - db_index=True, - help_text="The position of this document in your physical document archive.", - null=True, - unique=True, - validators=[ - django.core.validators.MaxValueValidator(4294967295), - django.core.validators.MinValueValidator(0), - ], - verbose_name="archive serial number", - ), - ), - migrations.AddField( - model_name="paperlesstask", - name="task_file_name", - field=models.CharField( - help_text="Name of the file which the Task was run for", - max_length=255, - null=True, - verbose_name="Task Filename", - ), - ), - migrations.RenameField( - model_name="savedview", - old_name="user", - new_name="owner", - ), - migrations.AlterField( - model_name="savedview", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AddField( - model_name="correspondent", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AddField( - model_name="document", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AddField( - model_name="documenttype", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AddField( - model_name="storagepath", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AddField( - model_name="tag", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AlterField( - model_name="correspondent", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (0, "None"), - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - (6, "Automatic"), - ], - default=1, - verbose_name="matching algorithm", - ), - ), - migrations.AlterField( - model_name="documenttype", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (0, "None"), - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - (6, "Automatic"), - ], - default=1, - verbose_name="matching algorithm", - ), - ), - migrations.AlterField( - model_name="storagepath", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (0, "None"), - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - (6, "Automatic"), - ], - default=1, - verbose_name="matching algorithm", - ), - ), - migrations.AlterField( - model_name="tag", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (0, "None"), - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - (6, "Automatic"), - ], - default=1, - verbose_name="matching algorithm", - ), - ), - migrations.AlterModelOptions( - name="documenttype", - options={ - "ordering": ("name",), - "verbose_name": "document type", - "verbose_name_plural": "document types", - }, - ), - migrations.AlterModelOptions( - name="tag", - options={ - "ordering": ("name",), - "verbose_name": "tag", - "verbose_name_plural": "tags", - }, - ), - migrations.AlterField( - model_name="correspondent", - name="name", - field=models.CharField(max_length=128, verbose_name="name"), - ), - migrations.AlterField( - model_name="documenttype", - name="name", - field=models.CharField(max_length=128, verbose_name="name"), - ), - migrations.AlterField( - model_name="storagepath", - name="name", - field=models.CharField(max_length=128, verbose_name="name"), - ), - migrations.AlterField( - model_name="tag", - name="name", - field=models.CharField(max_length=128, verbose_name="name"), - ), - migrations.AddConstraint( - model_name="correspondent", - constraint=models.UniqueConstraint( - fields=("name", "owner"), - name="documents_correspondent_unique_name_owner", - ), - ), - migrations.AddConstraint( - model_name="correspondent", - constraint=models.UniqueConstraint( - condition=models.Q(("owner__isnull", True)), - fields=("name",), - name="documents_correspondent_name_uniq", - ), - ), - migrations.AddConstraint( - model_name="documenttype", - constraint=models.UniqueConstraint( - fields=("name", "owner"), - name="documents_documenttype_unique_name_owner", - ), - ), - migrations.AddConstraint( - model_name="documenttype", - constraint=models.UniqueConstraint( - condition=models.Q(("owner__isnull", True)), - fields=("name",), - name="documents_documenttype_name_uniq", - ), - ), - migrations.AddConstraint( - model_name="storagepath", - constraint=models.UniqueConstraint( - fields=("name", "owner"), - name="documents_storagepath_unique_name_owner", - ), - ), - migrations.AddConstraint( - model_name="storagepath", - constraint=models.UniqueConstraint( - condition=models.Q(("owner__isnull", True)), - fields=("name",), - name="documents_storagepath_name_uniq", - ), - ), - migrations.AddConstraint( - model_name="tag", - constraint=models.UniqueConstraint( - fields=("name", "owner"), - name="documents_tag_unique_name_owner", - ), - ), - migrations.AddConstraint( - model_name="tag", - constraint=models.UniqueConstraint( - condition=models.Q(("owner__isnull", True)), - fields=("name",), - name="documents_tag_name_uniq", - ), - ), - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - (18, "does not have ASN"), - (19, "title or content contains"), - (20, "fulltext query"), - (21, "more like this"), - (22, "has tags in"), - (23, "ASN greater than"), - (24, "ASN less than"), - (25, "storage path is"), - (26, "has correspondent in"), - (27, "does not have correspondent in"), - (28, "has document type in"), - (29, "does not have document type in"), - (30, "has storage path in"), - (31, "does not have storage path in"), - ], - verbose_name="rule type", - ), - ), - migrations.RenameModel( - old_name="Comment", - new_name="Note", - ), - migrations.RenameField( - model_name="note", - old_name="comment", - new_name="note", - ), - migrations.AlterModelOptions( - name="note", - options={ - "ordering": ("created",), - "verbose_name": "note", - "verbose_name_plural": "notes", - }, - ), - migrations.AlterField( - model_name="note", - name="document", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="notes", - to="documents.document", - verbose_name="document", - ), - ), - migrations.AlterField( - model_name="note", - name="note", - field=models.TextField( - blank=True, - help_text="Note for the document", - verbose_name="content", - ), - ), - migrations.AlterField( - model_name="note", - name="user", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="notes", - to=settings.AUTH_USER_MODEL, - verbose_name="user", - ), - ), - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - (18, "does not have ASN"), - (19, "title or content contains"), - (20, "fulltext query"), - (21, "more like this"), - (22, "has tags in"), - (23, "ASN greater than"), - (24, "ASN less than"), - (25, "storage path is"), - (26, "has correspondent in"), - (27, "does not have correspondent in"), - (28, "has document type in"), - (29, "does not have document type in"), - (30, "has storage path in"), - (31, "does not have storage path in"), - (32, "owner is"), - (33, "has owner in"), - (34, "does not have owner"), - (35, "does not have owner in"), - ], - verbose_name="rule type", - ), - ), - ] diff --git a/src/documents/migrations/1023_add_comments.py b/src/documents/migrations/1023_add_comments.py deleted file mode 100644 index 0b26739bc..000000000 --- a/src/documents/migrations/1023_add_comments.py +++ /dev/null @@ -1,70 +0,0 @@ -import django.utils.timezone -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1022_paperlesstask"), - ] - - operations = [ - migrations.CreateModel( - name="Comment", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "comment", - models.TextField( - blank=True, - help_text="Comment for the document", - verbose_name="content", - ), - ), - ( - "created", - models.DateTimeField( - db_index=True, - default=django.utils.timezone.now, - verbose_name="created", - ), - ), - ( - "document", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="documents", - to="documents.document", - verbose_name="document", - ), - ), - ( - "user", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="users", - to=settings.AUTH_USER_MODEL, - verbose_name="user", - ), - ), - ], - options={ - "verbose_name": "comment", - "verbose_name_plural": "comments", - "ordering": ("created",), - }, - ), - ] diff --git a/src/documents/migrations/1024_document_original_filename.py b/src/documents/migrations/1024_document_original_filename.py deleted file mode 100644 index 05be7269e..000000000 --- a/src/documents/migrations/1024_document_original_filename.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 4.0.6 on 2022-07-25 06:34 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1023_add_comments"), - ] - - operations = [ - migrations.AddField( - model_name="document", - name="original_filename", - field=models.CharField( - default=None, - editable=False, - help_text="The original name of the file when it was uploaded", - max_length=1024, - null=True, - verbose_name="original filename", - ), - ), - ] diff --git a/src/documents/migrations/1025_alter_savedviewfilterrule_rule_type.py b/src/documents/migrations/1025_alter_savedviewfilterrule_rule_type.py deleted file mode 100644 index a2deb9579..000000000 --- a/src/documents/migrations/1025_alter_savedviewfilterrule_rule_type.py +++ /dev/null @@ -1,48 +0,0 @@ -# Generated by Django 4.0.5 on 2022-08-26 16:49 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1024_document_original_filename"), - ] - - operations = [ - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - (18, "does not have ASN"), - (19, "title or content contains"), - (20, "fulltext query"), - (21, "more like this"), - (22, "has tags in"), - (23, "ASN greater than"), - (24, "ASN less than"), - (25, "storage path is"), - ], - verbose_name="rule type", - ), - ), - ] diff --git a/src/documents/migrations/1026_transition_to_celery.py b/src/documents/migrations/1026_transition_to_celery.py deleted file mode 100644 index 227188d22..000000000 --- a/src/documents/migrations/1026_transition_to_celery.py +++ /dev/null @@ -1,60 +0,0 @@ -# Generated by Django 4.1.1 on 2022-09-27 19:31 - -import django.db.models.deletion -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("django_celery_results", "0011_taskresult_periodic_task_name"), - ("documents", "1025_alter_savedviewfilterrule_rule_type"), - ] - - operations = [ - migrations.RemoveField( - model_name="paperlesstask", - name="created", - ), - migrations.RemoveField( - model_name="paperlesstask", - name="name", - ), - migrations.RemoveField( - model_name="paperlesstask", - name="started", - ), - # Remove the field from the model - migrations.RemoveField( - model_name="paperlesstask", - name="attempted_task", - ), - # Add the field back, pointing to the correct model - # This resolves a problem where the temporary change in 1022 - # results in a type mismatch - migrations.AddField( - model_name="paperlesstask", - name="attempted_task", - field=models.OneToOneField( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="attempted_task", - to="django_celery_results.taskresult", - ), - ), - # Drop the django-q tables entirely - # Must be done last or there could be references here - migrations.RunSQL( - "DROP TABLE IF EXISTS django_q_ormq", - reverse_sql=migrations.RunSQL.noop, - ), - migrations.RunSQL( - "DROP TABLE IF EXISTS django_q_schedule", - reverse_sql=migrations.RunSQL.noop, - ), - migrations.RunSQL( - "DROP TABLE IF EXISTS django_q_task", - reverse_sql=migrations.RunSQL.noop, - ), - ] diff --git a/src/documents/migrations/1027_remove_paperlesstask_attempted_task_and_more.py b/src/documents/migrations/1027_remove_paperlesstask_attempted_task_and_more.py deleted file mode 100644 index c169c3096..000000000 --- a/src/documents/migrations/1027_remove_paperlesstask_attempted_task_and_more.py +++ /dev/null @@ -1,134 +0,0 @@ -# Generated by Django 4.1.2 on 2022-10-17 16:31 - -import django.utils.timezone -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1026_transition_to_celery"), - ] - - operations = [ - migrations.RemoveField( - model_name="paperlesstask", - name="attempted_task", - ), - migrations.AddField( - model_name="paperlesstask", - name="date_created", - field=models.DateTimeField( - default=django.utils.timezone.now, - help_text="Datetime field when the task result was created in UTC", - null=True, - verbose_name="Created DateTime", - ), - ), - migrations.AddField( - model_name="paperlesstask", - name="date_done", - field=models.DateTimeField( - default=None, - help_text="Datetime field when the task was completed in UTC", - null=True, - verbose_name="Completed DateTime", - ), - ), - migrations.AddField( - model_name="paperlesstask", - name="date_started", - field=models.DateTimeField( - default=None, - help_text="Datetime field when the task was started in UTC", - null=True, - verbose_name="Started DateTime", - ), - ), - migrations.AddField( - model_name="paperlesstask", - name="result", - field=models.TextField( - default=None, - help_text="The data returned by the task", - null=True, - verbose_name="Result Data", - ), - ), - migrations.AddField( - model_name="paperlesstask", - name="status", - field=models.CharField( - choices=[ - ("FAILURE", "FAILURE"), - ("PENDING", "PENDING"), - ("RECEIVED", "RECEIVED"), - ("RETRY", "RETRY"), - ("REVOKED", "REVOKED"), - ("STARTED", "STARTED"), - ("SUCCESS", "SUCCESS"), - ], - default="PENDING", - help_text="Current state of the task being run", - max_length=30, - verbose_name="Task State", - ), - ), - migrations.AddField( - model_name="paperlesstask", - name="task_args", - field=models.JSONField( - help_text="JSON representation of the positional arguments used with the task", - null=True, - verbose_name="Task Positional Arguments", - ), - ), - migrations.AddField( - model_name="paperlesstask", - name="task_file_name", - field=models.CharField( - help_text="Name of the file which the Task was run for", - max_length=255, - null=True, - verbose_name="Task Name", - ), - ), - migrations.AddField( - model_name="paperlesstask", - name="task_kwargs", - field=models.JSONField( - help_text="JSON representation of the named arguments used with the task", - null=True, - verbose_name="Task Named Arguments", - ), - ), - migrations.AddField( - model_name="paperlesstask", - name="task_name", - field=models.CharField( - help_text="Name of the Task which was run", - max_length=255, - null=True, - verbose_name="Task Name", - ), - ), - migrations.AlterField( - model_name="paperlesstask", - name="acknowledged", - field=models.BooleanField( - default=False, - help_text="If the task is acknowledged via the frontend or API", - verbose_name="Acknowledged", - ), - ), - migrations.AlterField( - model_name="paperlesstask", - name="task_id", - field=models.CharField( - help_text="Celery ID for the Task that was run", - max_length=255, - unique=True, - verbose_name="Task ID", - ), - ), - ] diff --git a/src/documents/migrations/1028_remove_paperlesstask_task_args_and_more.py b/src/documents/migrations/1028_remove_paperlesstask_task_args_and_more.py deleted file mode 100644 index 6e03c124b..000000000 --- a/src/documents/migrations/1028_remove_paperlesstask_task_args_and_more.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 4.1.3 on 2022-11-22 17:50 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1027_remove_paperlesstask_attempted_task_and_more"), - ] - - operations = [ - migrations.RemoveField( - model_name="paperlesstask", - name="task_args", - ), - migrations.RemoveField( - model_name="paperlesstask", - name="task_kwargs", - ), - ] diff --git a/src/documents/migrations/1029_alter_document_archive_serial_number.py b/src/documents/migrations/1029_alter_document_archive_serial_number.py deleted file mode 100644 index 57848b2dc..000000000 --- a/src/documents/migrations/1029_alter_document_archive_serial_number.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 4.1.4 on 2023-01-24 17:56 - -import django.core.validators -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1028_remove_paperlesstask_task_args_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="document", - name="archive_serial_number", - field=models.PositiveIntegerField( - blank=True, - db_index=True, - help_text="The position of this document in your physical document archive.", - null=True, - unique=True, - validators=[ - django.core.validators.MaxValueValidator(4294967295), - django.core.validators.MinValueValidator(0), - ], - verbose_name="archive serial number", - ), - ), - ] diff --git a/src/documents/migrations/1030_alter_paperlesstask_task_file_name.py b/src/documents/migrations/1030_alter_paperlesstask_task_file_name.py deleted file mode 100644 index 37e918bee..000000000 --- a/src/documents/migrations/1030_alter_paperlesstask_task_file_name.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 4.1.5 on 2023-02-03 21:53 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1029_alter_document_archive_serial_number"), - ] - - operations = [ - migrations.AlterField( - model_name="paperlesstask", - name="task_file_name", - field=models.CharField( - help_text="Name of the file which the Task was run for", - max_length=255, - null=True, - verbose_name="Task Filename", - ), - ), - ] diff --git a/src/documents/migrations/1031_remove_savedview_user_correspondent_owner_and_more.py b/src/documents/migrations/1031_remove_savedview_user_correspondent_owner_and_more.py deleted file mode 100644 index 56e4355ef..000000000 --- a/src/documents/migrations/1031_remove_savedview_user_correspondent_owner_and_more.py +++ /dev/null @@ -1,87 +0,0 @@ -# Generated by Django 4.1.4 on 2022-02-03 04:24 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("documents", "1030_alter_paperlesstask_task_file_name"), - ] - - operations = [ - migrations.RenameField( - model_name="savedview", - old_name="user", - new_name="owner", - ), - migrations.AlterField( - model_name="savedview", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AddField( - model_name="correspondent", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AddField( - model_name="document", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AddField( - model_name="documenttype", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AddField( - model_name="storagepath", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AddField( - model_name="tag", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - ] diff --git a/src/documents/migrations/1032_alter_correspondent_matching_algorithm_and_more.py b/src/documents/migrations/1032_alter_correspondent_matching_algorithm_and_more.py deleted file mode 100644 index 3d1c5658a..000000000 --- a/src/documents/migrations/1032_alter_correspondent_matching_algorithm_and_more.py +++ /dev/null @@ -1,81 +0,0 @@ -# Generated by Django 4.1.7 on 2023-02-22 00:45 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1031_remove_savedview_user_correspondent_owner_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="correspondent", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (0, "None"), - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - (6, "Automatic"), - ], - default=1, - verbose_name="matching algorithm", - ), - ), - migrations.AlterField( - model_name="documenttype", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (0, "None"), - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - (6, "Automatic"), - ], - default=1, - verbose_name="matching algorithm", - ), - ), - migrations.AlterField( - model_name="storagepath", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (0, "None"), - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - (6, "Automatic"), - ], - default=1, - verbose_name="matching algorithm", - ), - ), - migrations.AlterField( - model_name="tag", - name="matching_algorithm", - field=models.PositiveIntegerField( - choices=[ - (0, "None"), - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - (6, "Automatic"), - ], - default=1, - verbose_name="matching algorithm", - ), - ), - ] diff --git a/src/documents/migrations/1034_alter_savedviewfilterrule_rule_type.py b/src/documents/migrations/1034_alter_savedviewfilterrule_rule_type.py deleted file mode 100644 index b648ac839..000000000 --- a/src/documents/migrations/1034_alter_savedviewfilterrule_rule_type.py +++ /dev/null @@ -1,54 +0,0 @@ -# Generated by Django 4.1.5 on 2023-03-15 07:10 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1033_alter_documenttype_options_alter_tag_options_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - (18, "does not have ASN"), - (19, "title or content contains"), - (20, "fulltext query"), - (21, "more like this"), - (22, "has tags in"), - (23, "ASN greater than"), - (24, "ASN less than"), - (25, "storage path is"), - (26, "has correspondent in"), - (27, "does not have correspondent in"), - (28, "has document type in"), - (29, "does not have document type in"), - (30, "has storage path in"), - (31, "does not have storage path in"), - ], - verbose_name="rule type", - ), - ), - ] diff --git a/src/documents/migrations/1035_rename_comment_note.py b/src/documents/migrations/1035_rename_comment_note.py deleted file mode 100644 index 9f9aaca94..000000000 --- a/src/documents/migrations/1035_rename_comment_note.py +++ /dev/null @@ -1,62 +0,0 @@ -# Generated by Django 4.1.5 on 2023-03-17 22:15 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("documents", "1034_alter_savedviewfilterrule_rule_type"), - ] - - operations = [ - migrations.RenameModel( - old_name="Comment", - new_name="Note", - ), - migrations.RenameField(model_name="note", old_name="comment", new_name="note"), - migrations.AlterModelOptions( - name="note", - options={ - "ordering": ("created",), - "verbose_name": "note", - "verbose_name_plural": "notes", - }, - ), - migrations.AlterField( - model_name="note", - name="document", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="notes", - to="documents.document", - verbose_name="document", - ), - ), - migrations.AlterField( - model_name="note", - name="note", - field=models.TextField( - blank=True, - help_text="Note for the document", - verbose_name="content", - ), - ), - migrations.AlterField( - model_name="note", - name="user", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="notes", - to=settings.AUTH_USER_MODEL, - verbose_name="user", - ), - ), - ] diff --git a/src/documents/migrations/1036_alter_savedviewfilterrule_rule_type.py b/src/documents/migrations/1036_alter_savedviewfilterrule_rule_type.py deleted file mode 100644 index e65586ad8..000000000 --- a/src/documents/migrations/1036_alter_savedviewfilterrule_rule_type.py +++ /dev/null @@ -1,58 +0,0 @@ -# Generated by Django 4.1.7 on 2023-05-04 04:11 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1035_rename_comment_note"), - ] - - operations = [ - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - (18, "does not have ASN"), - (19, "title or content contains"), - (20, "fulltext query"), - (21, "more like this"), - (22, "has tags in"), - (23, "ASN greater than"), - (24, "ASN less than"), - (25, "storage path is"), - (26, "has correspondent in"), - (27, "does not have correspondent in"), - (28, "has document type in"), - (29, "does not have document type in"), - (30, "has storage path in"), - (31, "does not have storage path in"), - (32, "owner is"), - (33, "has owner in"), - (34, "does not have owner"), - (35, "does not have owner in"), - ], - verbose_name="rule type", - ), - ), - ] diff --git a/src/documents/migrations/1037_webp_encrypted_thumbnail_conversion.py b/src/documents/migrations/1037_webp_encrypted_thumbnail_conversion.py deleted file mode 100644 index 13996132f..000000000 --- a/src/documents/migrations/1037_webp_encrypted_thumbnail_conversion.py +++ /dev/null @@ -1,164 +0,0 @@ -# Generated by Django 4.1.9 on 2023-06-29 19:29 -import logging -import multiprocessing.pool -import shutil -import tempfile -import time -from pathlib import Path - -import gnupg -from django.conf import settings -from django.db import migrations - -from documents.parsers import run_convert - -logger = logging.getLogger("paperless.migrations") - - -def _do_convert(work_package) -> None: - ( - existing_encrypted_thumbnail, - converted_encrypted_thumbnail, - passphrase, - ) = work_package - - try: - gpg = gnupg.GPG(gnupghome=settings.GNUPG_HOME) - - logger.info(f"Decrypting thumbnail: {existing_encrypted_thumbnail}") - - # Decrypt png - decrypted_thumbnail = existing_encrypted_thumbnail.with_suffix("").resolve() - - with existing_encrypted_thumbnail.open("rb") as existing_encrypted_file: - raw_thumb = gpg.decrypt_file( - existing_encrypted_file, - passphrase=passphrase, - always_trust=True, - ).data - with Path(decrypted_thumbnail).open("wb") as decrypted_file: - decrypted_file.write(raw_thumb) - - converted_decrypted_thumbnail = Path( - str(converted_encrypted_thumbnail).replace("webp.gpg", "webp"), - ).resolve() - - logger.info(f"Converting decrypted thumbnail: {decrypted_thumbnail}") - - # Convert to webp - run_convert( - density=300, - scale="500x5000>", - alpha="remove", - strip=True, - trim=False, - auto_orient=True, - input_file=f"{decrypted_thumbnail}[0]", - output_file=str(converted_decrypted_thumbnail), - ) - - logger.info( - f"Encrypting converted thumbnail: {converted_decrypted_thumbnail}", - ) - - # Encrypt webp - with Path(converted_decrypted_thumbnail).open("rb") as converted_decrypted_file: - encrypted = gpg.encrypt_file( - fileobj_or_path=converted_decrypted_file, - recipients=None, - passphrase=passphrase, - symmetric=True, - always_trust=True, - ).data - - with Path(converted_encrypted_thumbnail).open( - "wb", - ) as converted_encrypted_file: - converted_encrypted_file.write(encrypted) - - # Copy newly created thumbnail to thumbnail directory - shutil.copy(converted_encrypted_thumbnail, existing_encrypted_thumbnail.parent) - - # Remove the existing encrypted PNG version - existing_encrypted_thumbnail.unlink() - - # Remove the decrypted PNG version - decrypted_thumbnail.unlink() - - # Remove the decrypted WebP version - converted_decrypted_thumbnail.unlink() - - logger.info( - "Conversion to WebP completed, " - f"replaced {existing_encrypted_thumbnail.name} with {converted_encrypted_thumbnail.name}", - ) - - except Exception as e: - logger.error(f"Error converting thumbnail (existing file unchanged): {e}") - - -def _convert_encrypted_thumbnails_to_webp(apps, schema_editor) -> None: - start: float = time.time() - - with tempfile.TemporaryDirectory() as tempdir: - work_packages = [] - - if len(list(Path(settings.THUMBNAIL_DIR).glob("*.png.gpg"))) > 0: - passphrase = settings.PASSPHRASE - - if not passphrase: - raise Exception( - "Passphrase not defined, encrypted thumbnails cannot be migrated" - "without this", - ) - - for file in Path(settings.THUMBNAIL_DIR).glob("*.png.gpg"): - existing_thumbnail: Path = file.resolve() - - # Change the existing filename suffix from png to webp - converted_thumbnail_name: str = Path( - str(existing_thumbnail).replace(".png.gpg", ".webp.gpg"), - ).name - - # Create the expected output filename in the tempdir - converted_thumbnail: Path = ( - Path(tempdir) / Path(converted_thumbnail_name) - ).resolve() - - # Package up the necessary info - work_packages.append( - (existing_thumbnail, converted_thumbnail, passphrase), - ) - - if work_packages: - logger.info( - "\n\n" - " This is a one-time only migration to convert thumbnails for all of your\n" - " *encrypted* documents into WebP format. If you have a lot of encrypted documents, \n" - " this may take a while, so a coffee break may be in order." - "\n", - ) - - with multiprocessing.pool.Pool( - processes=min(multiprocessing.cpu_count(), 4), - maxtasksperchild=4, - ) as pool: - pool.map(_do_convert, work_packages) - - end: float = time.time() - duration: float = end - start - - logger.info(f"Conversion completed in {duration:.3f}s") - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1036_alter_savedviewfilterrule_rule_type"), - ] - - operations = [ - migrations.RunPython( - code=_convert_encrypted_thumbnails_to_webp, - reverse_code=migrations.RunPython.noop, - ), - ] diff --git a/src/documents/migrations/1038_sharelink.py b/src/documents/migrations/1038_sharelink.py deleted file mode 100644 index fa2860b6f..000000000 --- a/src/documents/migrations/1038_sharelink.py +++ /dev/null @@ -1,126 +0,0 @@ -# Generated by Django 4.1.10 on 2023-08-14 14:51 - -import django.db.models.deletion -import django.utils.timezone -from django.conf import settings -from django.contrib.auth.management import create_permissions -from django.contrib.auth.models import Group -from django.contrib.auth.models import Permission -from django.contrib.auth.models import User -from django.db import migrations -from django.db import models -from django.db.models import Q - - -def add_sharelink_permissions(apps, schema_editor): - # create permissions without waiting for post_migrate signal - for app_config in apps.get_app_configs(): - app_config.models_module = True - create_permissions(app_config, apps=apps, verbosity=0) - app_config.models_module = None - - add_permission = Permission.objects.get(codename="add_document") - sharelink_permissions = Permission.objects.filter(codename__contains="sharelink") - - for user in User.objects.filter(Q(user_permissions=add_permission)).distinct(): - user.user_permissions.add(*sharelink_permissions) - - for group in Group.objects.filter(Q(permissions=add_permission)).distinct(): - group.permissions.add(*sharelink_permissions) - - -def remove_sharelink_permissions(apps, schema_editor): - sharelink_permissions = Permission.objects.filter(codename__contains="sharelink") - - for user in User.objects.all(): - user.user_permissions.remove(*sharelink_permissions) - - for group in Group.objects.all(): - group.permissions.remove(*sharelink_permissions) - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("documents", "1037_webp_encrypted_thumbnail_conversion"), - ] - - operations = [ - migrations.CreateModel( - name="ShareLink", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "created", - models.DateTimeField( - blank=True, - db_index=True, - default=django.utils.timezone.now, - editable=False, - verbose_name="created", - ), - ), - ( - "expiration", - models.DateTimeField( - blank=True, - db_index=True, - null=True, - verbose_name="expiration", - ), - ), - ( - "slug", - models.SlugField( - blank=True, - editable=False, - unique=True, - verbose_name="slug", - ), - ), - ( - "file_version", - models.CharField( - choices=[("archive", "Archive"), ("original", "Original")], - default="archive", - max_length=50, - ), - ), - ( - "document", - models.ForeignKey( - blank=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="share_links", - to="documents.document", - verbose_name="document", - ), - ), - ( - "owner", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="share_links", - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - ], - options={ - "verbose_name": "share link", - "verbose_name_plural": "share links", - "ordering": ("created",), - }, - ), - migrations.RunPython(add_sharelink_permissions, remove_sharelink_permissions), - ] diff --git a/src/documents/migrations/1039_consumptiontemplate.py b/src/documents/migrations/1039_consumptiontemplate.py deleted file mode 100644 index cf8b9fd91..000000000 --- a/src/documents/migrations/1039_consumptiontemplate.py +++ /dev/null @@ -1,219 +0,0 @@ -# Generated by Django 4.1.11 on 2023-09-16 18:04 - -import django.db.models.deletion -import multiselectfield.db.fields -from django.conf import settings -from django.contrib.auth.management import create_permissions -from django.contrib.auth.models import Group -from django.contrib.auth.models import Permission -from django.contrib.auth.models import User -from django.db import migrations -from django.db import models -from django.db.models import Q - - -def add_consumptiontemplate_permissions(apps, schema_editor): - # create permissions without waiting for post_migrate signal - for app_config in apps.get_app_configs(): - app_config.models_module = True - create_permissions(app_config, apps=apps, verbosity=0) - app_config.models_module = None - - add_permission = Permission.objects.get(codename="add_document") - consumptiontemplate_permissions = Permission.objects.filter( - codename__contains="consumptiontemplate", - ) - - for user in User.objects.filter(Q(user_permissions=add_permission)).distinct(): - user.user_permissions.add(*consumptiontemplate_permissions) - - for group in Group.objects.filter(Q(permissions=add_permission)).distinct(): - group.permissions.add(*consumptiontemplate_permissions) - - -def remove_consumptiontemplate_permissions(apps, schema_editor): - consumptiontemplate_permissions = Permission.objects.filter( - codename__contains="consumptiontemplate", - ) - - for user in User.objects.all(): - user.user_permissions.remove(*consumptiontemplate_permissions) - - for group in Group.objects.all(): - group.permissions.remove(*consumptiontemplate_permissions) - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("auth", "0012_alter_user_first_name_max_length"), - ("documents", "1038_sharelink"), - ("paperless_mail", "0021_alter_mailaccount_password"), - ] - - operations = [ - migrations.CreateModel( - name="ConsumptionTemplate", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "name", - models.CharField(max_length=256, unique=True, verbose_name="name"), - ), - ("order", models.IntegerField(default=0, verbose_name="order")), - ( - "sources", - multiselectfield.db.fields.MultiSelectField( - choices=[ - (1, "Consume Folder"), - (2, "Api Upload"), - (3, "Mail Fetch"), - ], - default="1,2,3", - max_length=3, - ), - ), - ( - "filter_path", - models.CharField( - blank=True, - help_text="Only consume documents with a path that matches this if specified. Wildcards specified as * are allowed. Case insensitive.", - max_length=256, - null=True, - verbose_name="filter path", - ), - ), - ( - "filter_filename", - models.CharField( - blank=True, - help_text="Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.", - max_length=256, - null=True, - verbose_name="filter filename", - ), - ), - ( - "filter_mailrule", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="paperless_mail.mailrule", - verbose_name="filter documents from this mail rule", - ), - ), - ( - "assign_change_groups", - models.ManyToManyField( - blank=True, - related_name="+", - to="auth.group", - verbose_name="grant change permissions to these groups", - ), - ), - ( - "assign_change_users", - models.ManyToManyField( - blank=True, - related_name="+", - to=settings.AUTH_USER_MODEL, - verbose_name="grant change permissions to these users", - ), - ), - ( - "assign_correspondent", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.correspondent", - verbose_name="assign this correspondent", - ), - ), - ( - "assign_document_type", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.documenttype", - verbose_name="assign this document type", - ), - ), - ( - "assign_owner", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="+", - to=settings.AUTH_USER_MODEL, - verbose_name="assign this owner", - ), - ), - ( - "assign_storage_path", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.storagepath", - verbose_name="assign this storage path", - ), - ), - ( - "assign_tags", - models.ManyToManyField( - blank=True, - to="documents.tag", - verbose_name="assign this tag", - ), - ), - ( - "assign_title", - models.CharField( - blank=True, - help_text="Assign a document title, can include some placeholders, see documentation.", - max_length=256, - null=True, - verbose_name="assign title", - ), - ), - ( - "assign_view_groups", - models.ManyToManyField( - blank=True, - related_name="+", - to="auth.group", - verbose_name="grant view permissions to these groups", - ), - ), - ( - "assign_view_users", - models.ManyToManyField( - blank=True, - related_name="+", - to=settings.AUTH_USER_MODEL, - verbose_name="grant view permissions to these users", - ), - ), - ], - options={ - "verbose_name": "consumption template", - "verbose_name_plural": "consumption templates", - }, - ), - migrations.RunPython( - add_consumptiontemplate_permissions, - remove_consumptiontemplate_permissions, - ), - ] diff --git a/src/documents/migrations/1040_customfield_customfieldinstance_and_more.py b/src/documents/migrations/1040_customfield_customfieldinstance_and_more.py deleted file mode 100644 index ecd715a57..000000000 --- a/src/documents/migrations/1040_customfield_customfieldinstance_and_more.py +++ /dev/null @@ -1,171 +0,0 @@ -# Generated by Django 4.2.6 on 2023-11-02 17:38 - -import django.db.models.deletion -import django.utils.timezone -from django.contrib.auth.management import create_permissions -from django.contrib.auth.models import Group -from django.contrib.auth.models import Permission -from django.contrib.auth.models import User -from django.db import migrations -from django.db import models -from django.db.models import Q - - -def add_customfield_permissions(apps, schema_editor): - # create permissions without waiting for post_migrate signal - for app_config in apps.get_app_configs(): - app_config.models_module = True - create_permissions(app_config, apps=apps, verbosity=0) - app_config.models_module = None - - add_permission = Permission.objects.get(codename="add_document") - customfield_permissions = Permission.objects.filter( - codename__contains="customfield", - ) - - for user in User.objects.filter(Q(user_permissions=add_permission)).distinct(): - user.user_permissions.add(*customfield_permissions) - - for group in Group.objects.filter(Q(permissions=add_permission)).distinct(): - group.permissions.add(*customfield_permissions) - - -def remove_customfield_permissions(apps, schema_editor): - customfield_permissions = Permission.objects.filter( - codename__contains="customfield", - ) - - for user in User.objects.all(): - user.user_permissions.remove(*customfield_permissions) - - for group in Group.objects.all(): - group.permissions.remove(*customfield_permissions) - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1039_consumptiontemplate"), - ] - - operations = [ - migrations.CreateModel( - name="CustomField", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "created", - models.DateTimeField( - db_index=True, - default=django.utils.timezone.now, - editable=False, - verbose_name="created", - ), - ), - ("name", models.CharField(max_length=128)), - ( - "data_type", - models.CharField( - choices=[ - ("string", "String"), - ("url", "URL"), - ("date", "Date"), - ("boolean", "Boolean"), - ("integer", "Integer"), - ("float", "Float"), - ("monetary", "Monetary"), - ], - editable=False, - max_length=50, - verbose_name="data type", - ), - ), - ], - options={ - "verbose_name": "custom field", - "verbose_name_plural": "custom fields", - "ordering": ("created",), - }, - ), - migrations.CreateModel( - name="CustomFieldInstance", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "created", - models.DateTimeField( - db_index=True, - default=django.utils.timezone.now, - editable=False, - verbose_name="created", - ), - ), - ("value_text", models.CharField(max_length=128, null=True)), - ("value_bool", models.BooleanField(null=True)), - ("value_url", models.URLField(null=True)), - ("value_date", models.DateField(null=True)), - ("value_int", models.IntegerField(null=True)), - ("value_float", models.FloatField(null=True)), - ( - "value_monetary", - models.DecimalField(decimal_places=2, max_digits=12, null=True), - ), - ( - "document", - models.ForeignKey( - editable=False, - on_delete=django.db.models.deletion.CASCADE, - related_name="custom_fields", - to="documents.document", - ), - ), - ( - "field", - models.ForeignKey( - editable=False, - on_delete=django.db.models.deletion.CASCADE, - related_name="fields", - to="documents.customfield", - ), - ), - ], - options={ - "verbose_name": "custom field instance", - "verbose_name_plural": "custom field instances", - "ordering": ("created",), - }, - ), - migrations.AddConstraint( - model_name="customfield", - constraint=models.UniqueConstraint( - fields=("name",), - name="documents_customfield_unique_name", - ), - ), - migrations.AddConstraint( - model_name="customfieldinstance", - constraint=models.UniqueConstraint( - fields=("document", "field"), - name="documents_customfieldinstance_unique_document_field", - ), - ), - migrations.RunPython( - add_customfield_permissions, - remove_customfield_permissions, - ), - ] diff --git a/src/documents/migrations/1041_alter_consumptiontemplate_sources.py b/src/documents/migrations/1041_alter_consumptiontemplate_sources.py deleted file mode 100644 index c96dc53cf..000000000 --- a/src/documents/migrations/1041_alter_consumptiontemplate_sources.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 4.2.7 on 2023-11-30 14:29 - -import multiselectfield.db.fields -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1040_customfield_customfieldinstance_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="consumptiontemplate", - name="sources", - field=multiselectfield.db.fields.MultiSelectField( - choices=[(1, "Consume Folder"), (2, "Api Upload"), (3, "Mail Fetch")], - default="1,2,3", - max_length=5, - ), - ), - ] diff --git a/src/documents/migrations/1042_consumptiontemplate_assign_custom_fields_and_more.py b/src/documents/migrations/1042_consumptiontemplate_assign_custom_fields_and_more.py deleted file mode 100644 index ffd0dbefa..000000000 --- a/src/documents/migrations/1042_consumptiontemplate_assign_custom_fields_and_more.py +++ /dev/null @@ -1,47 +0,0 @@ -# Generated by Django 4.2.7 on 2023-12-04 04:03 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1041_alter_consumptiontemplate_sources"), - ] - - operations = [ - migrations.AddField( - model_name="consumptiontemplate", - name="assign_custom_fields", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="documents.customfield", - verbose_name="assign these custom fields", - ), - ), - migrations.AddField( - model_name="customfieldinstance", - name="value_document_ids", - field=models.JSONField(null=True), - ), - migrations.AlterField( - model_name="customfield", - name="data_type", - field=models.CharField( - choices=[ - ("string", "String"), - ("url", "URL"), - ("date", "Date"), - ("boolean", "Boolean"), - ("integer", "Integer"), - ("float", "Float"), - ("monetary", "Monetary"), - ("documentlink", "Document Link"), - ], - editable=False, - max_length=50, - verbose_name="data type", - ), - ), - ] diff --git a/src/documents/migrations/1043_alter_savedviewfilterrule_rule_type.py b/src/documents/migrations/1043_alter_savedviewfilterrule_rule_type.py deleted file mode 100644 index bd62673df..000000000 --- a/src/documents/migrations/1043_alter_savedviewfilterrule_rule_type.py +++ /dev/null @@ -1,60 +0,0 @@ -# Generated by Django 4.2.7 on 2023-12-09 18:13 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1042_consumptiontemplate_assign_custom_fields_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - (18, "does not have ASN"), - (19, "title or content contains"), - (20, "fulltext query"), - (21, "more like this"), - (22, "has tags in"), - (23, "ASN greater than"), - (24, "ASN less than"), - (25, "storage path is"), - (26, "has correspondent in"), - (27, "does not have correspondent in"), - (28, "has document type in"), - (29, "does not have document type in"), - (30, "has storage path in"), - (31, "does not have storage path in"), - (32, "owner is"), - (33, "has owner in"), - (34, "does not have owner"), - (35, "does not have owner in"), - (36, "has custom field value"), - (37, "is shared by me"), - ], - verbose_name="rule type", - ), - ), - ] diff --git a/src/documents/migrations/1044_workflow_workflowaction_workflowtrigger_and_more.py b/src/documents/migrations/1044_workflow_workflowaction_workflowtrigger_and_more.py deleted file mode 100644 index 2cdd631bb..000000000 --- a/src/documents/migrations/1044_workflow_workflowaction_workflowtrigger_and_more.py +++ /dev/null @@ -1,524 +0,0 @@ -# Generated by Django 4.2.7 on 2023-12-23 22:51 - -import django.db.models.deletion -import multiselectfield.db.fields -from django.conf import settings -from django.contrib.auth.management import create_permissions -from django.db import migrations -from django.db import models -from django.db import transaction -from django.db.models import Q - - -def add_workflow_permissions(apps, schema_editor): - app_name = "auth" - User = apps.get_model(app_label=app_name, model_name="User") - Group = apps.get_model(app_label=app_name, model_name="Group") - Permission = apps.get_model(app_label=app_name, model_name="Permission") - # create permissions without waiting for post_migrate signal - for app_config in apps.get_app_configs(): - app_config.models_module = True - create_permissions(app_config, apps=apps, verbosity=0) - app_config.models_module = None - - add_permission = Permission.objects.get(codename="add_document") - workflow_permissions = Permission.objects.filter( - codename__contains="workflow", - ) - - for user in User.objects.filter(Q(user_permissions=add_permission)).distinct(): - user.user_permissions.add(*workflow_permissions) - - for group in Group.objects.filter(Q(permissions=add_permission)).distinct(): - group.permissions.add(*workflow_permissions) - - -def remove_workflow_permissions(apps, schema_editor): - app_name = "auth" - User = apps.get_model(app_label=app_name, model_name="User") - Group = apps.get_model(app_label=app_name, model_name="Group") - Permission = apps.get_model(app_label=app_name, model_name="Permission") - workflow_permissions = Permission.objects.filter( - codename__contains="workflow", - ) - - for user in User.objects.all(): - user.user_permissions.remove(*workflow_permissions) - - for group in Group.objects.all(): - group.permissions.remove(*workflow_permissions) - - -def migrate_consumption_templates(apps, schema_editor): - """ - Migrate consumption templates to workflows. At this point ConsumptionTemplate still exists - but objects are not returned as their true model so we have to manually do that - """ - app_name = "documents" - - ConsumptionTemplate = apps.get_model( - app_label=app_name, - model_name="ConsumptionTemplate", - ) - Workflow = apps.get_model(app_label=app_name, model_name="Workflow") - WorkflowAction = apps.get_model(app_label=app_name, model_name="WorkflowAction") - WorkflowTrigger = apps.get_model(app_label=app_name, model_name="WorkflowTrigger") - DocumentType = apps.get_model(app_label=app_name, model_name="DocumentType") - Correspondent = apps.get_model(app_label=app_name, model_name="Correspondent") - StoragePath = apps.get_model(app_label=app_name, model_name="StoragePath") - Tag = apps.get_model(app_label=app_name, model_name="Tag") - CustomField = apps.get_model(app_label=app_name, model_name="CustomField") - MailRule = apps.get_model(app_label="paperless_mail", model_name="MailRule") - User = apps.get_model(app_label="auth", model_name="User") - Group = apps.get_model(app_label="auth", model_name="Group") - - with transaction.atomic(): - for template in ConsumptionTemplate.objects.all(): - trigger = WorkflowTrigger( - type=1, # WorkflowTriggerType.CONSUMPTION - sources=template.sources, - filter_path=template.filter_path, - filter_filename=template.filter_filename, - ) - if template.filter_mailrule is not None: - trigger.filter_mailrule = MailRule.objects.get( - id=template.filter_mailrule.id, - ) - trigger.save() - - action = WorkflowAction.objects.create( - assign_title=template.assign_title, - ) - if template.assign_document_type is not None: - action.assign_document_type = DocumentType.objects.get( - id=template.assign_document_type.id, - ) - if template.assign_correspondent is not None: - action.assign_correspondent = Correspondent.objects.get( - id=template.assign_correspondent.id, - ) - if template.assign_storage_path is not None: - action.assign_storage_path = StoragePath.objects.get( - id=template.assign_storage_path.id, - ) - if template.assign_owner is not None: - action.assign_owner = User.objects.get(id=template.assign_owner.id) - if template.assign_tags is not None: - action.assign_tags.set( - Tag.objects.filter( - id__in=[t.id for t in template.assign_tags.all()], - ).all(), - ) - if template.assign_view_users is not None: - action.assign_view_users.set( - User.objects.filter( - id__in=[u.id for u in template.assign_view_users.all()], - ).all(), - ) - if template.assign_view_groups is not None: - action.assign_view_groups.set( - Group.objects.filter( - id__in=[g.id for g in template.assign_view_groups.all()], - ).all(), - ) - if template.assign_change_users is not None: - action.assign_change_users.set( - User.objects.filter( - id__in=[u.id for u in template.assign_change_users.all()], - ).all(), - ) - if template.assign_change_groups is not None: - action.assign_change_groups.set( - Group.objects.filter( - id__in=[g.id for g in template.assign_change_groups.all()], - ).all(), - ) - if template.assign_custom_fields is not None: - action.assign_custom_fields.set( - CustomField.objects.filter( - id__in=[cf.id for cf in template.assign_custom_fields.all()], - ).all(), - ) - action.save() - - workflow = Workflow.objects.create( - name=template.name, - order=template.order, - ) - workflow.triggers.set([trigger]) - workflow.actions.set([action]) - workflow.save() - - -def unmigrate_consumption_templates(apps, schema_editor): - app_name = "documents" - - ConsumptionTemplate = apps.get_model( - app_label=app_name, - model_name="ConsumptionTemplate", - ) - Workflow = apps.get_model(app_label=app_name, model_name="Workflow") - - for workflow in Workflow.objects.all(): - template = ConsumptionTemplate.objects.create( - name=workflow.name, - order=workflow.order, - sources=workflow.triggers.first().sources, - filter_path=workflow.triggers.first().filter_path, - filter_filename=workflow.triggers.first().filter_filename, - filter_mailrule=workflow.triggers.first().filter_mailrule, - assign_title=workflow.actions.first().assign_title, - assign_document_type=workflow.actions.first().assign_document_type, - assign_correspondent=workflow.actions.first().assign_correspondent, - assign_storage_path=workflow.actions.first().assign_storage_path, - assign_owner=workflow.actions.first().assign_owner, - ) - template.assign_tags.set(workflow.actions.first().assign_tags.all()) - template.assign_view_users.set(workflow.actions.first().assign_view_users.all()) - template.assign_view_groups.set( - workflow.actions.first().assign_view_groups.all(), - ) - template.assign_change_users.set( - workflow.actions.first().assign_change_users.all(), - ) - template.assign_change_groups.set( - workflow.actions.first().assign_change_groups.all(), - ) - template.assign_custom_fields.set( - workflow.actions.first().assign_custom_fields.all(), - ) - template.save() - - -def delete_consumption_template_content_type(apps, schema_editor): - with transaction.atomic(): - apps.get_model("contenttypes", "ContentType").objects.filter( - app_label="documents", - model="consumptiontemplate", - ).delete() - - -def undelete_consumption_template_content_type(apps, schema_editor): - apps.get_model("contenttypes", "ContentType").objects.create( - app_label="documents", - model="consumptiontemplate", - ) - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0023_remove_mailrule_filter_attachment_filename_and_more"), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("auth", "0012_alter_user_first_name_max_length"), - ("documents", "1043_alter_savedviewfilterrule_rule_type"), - ] - - operations = [ - migrations.CreateModel( - name="Workflow", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "name", - models.CharField(max_length=256, unique=True, verbose_name="name"), - ), - ("order", models.IntegerField(default=0, verbose_name="order")), - ( - "enabled", - models.BooleanField(default=True, verbose_name="enabled"), - ), - ], - ), - migrations.CreateModel( - name="WorkflowAction", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "type", - models.PositiveIntegerField( - choices=[(1, "Assignment")], - default=1, - verbose_name="Workflow Action Type", - ), - ), - ( - "assign_title", - models.CharField( - blank=True, - help_text="Assign a document title, can include some placeholders, see documentation.", - max_length=256, - null=True, - verbose_name="assign title", - ), - ), - ( - "assign_change_groups", - models.ManyToManyField( - blank=True, - related_name="+", - to="auth.group", - verbose_name="grant change permissions to these groups", - ), - ), - ( - "assign_change_users", - models.ManyToManyField( - blank=True, - related_name="+", - to=settings.AUTH_USER_MODEL, - verbose_name="grant change permissions to these users", - ), - ), - ( - "assign_correspondent", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.correspondent", - verbose_name="assign this correspondent", - ), - ), - ( - "assign_custom_fields", - models.ManyToManyField( - blank=True, - related_name="+", - to="documents.customfield", - verbose_name="assign these custom fields", - ), - ), - ( - "assign_document_type", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.documenttype", - verbose_name="assign this document type", - ), - ), - ( - "assign_owner", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="+", - to=settings.AUTH_USER_MODEL, - verbose_name="assign this owner", - ), - ), - ( - "assign_storage_path", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.storagepath", - verbose_name="assign this storage path", - ), - ), - ( - "assign_tags", - models.ManyToManyField( - blank=True, - to="documents.tag", - verbose_name="assign this tag", - ), - ), - ( - "assign_view_groups", - models.ManyToManyField( - blank=True, - related_name="+", - to="auth.group", - verbose_name="grant view permissions to these groups", - ), - ), - ( - "assign_view_users", - models.ManyToManyField( - blank=True, - related_name="+", - to=settings.AUTH_USER_MODEL, - verbose_name="grant view permissions to these users", - ), - ), - ], - options={ - "verbose_name": "workflow action", - "verbose_name_plural": "workflow actions", - }, - ), - migrations.CreateModel( - name="WorkflowTrigger", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "type", - models.PositiveIntegerField( - choices=[ - (1, "Consumption Started"), - (2, "Document Added"), - (3, "Document Updated"), - ], - default=1, - verbose_name="Workflow Trigger Type", - ), - ), - ( - "sources", - multiselectfield.db.fields.MultiSelectField( - choices=[ - (1, "Consume Folder"), - (2, "Api Upload"), - (3, "Mail Fetch"), - ], - default="1,2,3", - max_length=5, - ), - ), - ( - "filter_path", - models.CharField( - blank=True, - help_text="Only consume documents with a path that matches this if specified. Wildcards specified as * are allowed. Case insensitive.", - max_length=256, - null=True, - verbose_name="filter path", - ), - ), - ( - "filter_filename", - models.CharField( - blank=True, - help_text="Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.", - max_length=256, - null=True, - verbose_name="filter filename", - ), - ), - ( - "filter_mailrule", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="paperless_mail.mailrule", - verbose_name="filter documents from this mail rule", - ), - ), - ( - "matching_algorithm", - models.PositiveIntegerField( - choices=[ - (0, "None"), - (1, "Any word"), - (2, "All words"), - (3, "Exact match"), - (4, "Regular expression"), - (5, "Fuzzy word"), - ], - default=0, - verbose_name="matching algorithm", - ), - ), - ( - "match", - models.CharField(blank=True, max_length=256, verbose_name="match"), - ), - ( - "is_insensitive", - models.BooleanField(default=True, verbose_name="is insensitive"), - ), - ( - "filter_has_tags", - models.ManyToManyField( - blank=True, - to="documents.tag", - verbose_name="has these tag(s)", - ), - ), - ( - "filter_has_document_type", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.documenttype", - verbose_name="has this document type", - ), - ), - ( - "filter_has_correspondent", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.correspondent", - verbose_name="has this correspondent", - ), - ), - ], - options={ - "verbose_name": "workflow trigger", - "verbose_name_plural": "workflow triggers", - }, - ), - migrations.RunPython( - add_workflow_permissions, - remove_workflow_permissions, - ), - migrations.AddField( - model_name="workflow", - name="actions", - field=models.ManyToManyField( - related_name="workflows", - to="documents.workflowaction", - verbose_name="actions", - ), - ), - migrations.AddField( - model_name="workflow", - name="triggers", - field=models.ManyToManyField( - related_name="workflows", - to="documents.workflowtrigger", - verbose_name="triggers", - ), - ), - migrations.RunPython( - migrate_consumption_templates, - unmigrate_consumption_templates, - ), - migrations.DeleteModel("ConsumptionTemplate"), - migrations.RunPython( - delete_consumption_template_content_type, - undelete_consumption_template_content_type, - ), - ] diff --git a/src/documents/migrations/1045_alter_customfieldinstance_value_monetary.py b/src/documents/migrations/1045_alter_customfieldinstance_value_monetary.py deleted file mode 100644 index 597fbb7f9..000000000 --- a/src/documents/migrations/1045_alter_customfieldinstance_value_monetary.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.10 on 2024-02-22 03:52 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1044_workflow_workflowaction_workflowtrigger_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="customfieldinstance", - name="value_monetary", - field=models.CharField(max_length=128, null=True), - ), - ] diff --git a/src/documents/migrations/1045_alter_customfieldinstance_value_monetary_squashed_1049_document_deleted_at_document_restored_at.py b/src/documents/migrations/1045_alter_customfieldinstance_value_monetary_squashed_1049_document_deleted_at_document_restored_at.py deleted file mode 100644 index 2987e4812..000000000 --- a/src/documents/migrations/1045_alter_customfieldinstance_value_monetary_squashed_1049_document_deleted_at_document_restored_at.py +++ /dev/null @@ -1,331 +0,0 @@ -# Generated by Django 4.2.13 on 2024-06-28 19:39 - -import django.core.validators -import django.db.models.deletion -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - replaces = [ - ("documents", "1045_alter_customfieldinstance_value_monetary"), - ("documents", "1046_workflowaction_remove_all_correspondents_and_more"), - ("documents", "1047_savedview_display_mode_and_more"), - ("documents", "1048_alter_savedviewfilterrule_rule_type"), - ("documents", "1049_document_deleted_at_document_restored_at"), - ] - - dependencies = [ - ("documents", "1044_workflow_workflowaction_workflowtrigger_and_more"), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("auth", "0012_alter_user_first_name_max_length"), - ] - - operations = [ - migrations.AlterField( - model_name="customfieldinstance", - name="value_monetary", - field=models.CharField(max_length=128, null=True), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_all_correspondents", - field=models.BooleanField( - default=False, - verbose_name="remove all correspondents", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_all_custom_fields", - field=models.BooleanField( - default=False, - verbose_name="remove all custom fields", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_all_document_types", - field=models.BooleanField( - default=False, - verbose_name="remove all document types", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_all_owners", - field=models.BooleanField(default=False, verbose_name="remove all owners"), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_all_permissions", - field=models.BooleanField( - default=False, - verbose_name="remove all permissions", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_all_storage_paths", - field=models.BooleanField( - default=False, - verbose_name="remove all storage paths", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_all_tags", - field=models.BooleanField(default=False, verbose_name="remove all tags"), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_change_groups", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="auth.group", - verbose_name="remove change permissions for these groups", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_change_users", - field=models.ManyToManyField( - blank=True, - related_name="+", - to=settings.AUTH_USER_MODEL, - verbose_name="remove change permissions for these users", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_correspondents", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="documents.correspondent", - verbose_name="remove these correspondent(s)", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_custom_fields", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="documents.customfield", - verbose_name="remove these custom fields", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_document_types", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="documents.documenttype", - verbose_name="remove these document type(s)", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_owners", - field=models.ManyToManyField( - blank=True, - related_name="+", - to=settings.AUTH_USER_MODEL, - verbose_name="remove these owner(s)", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_storage_paths", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="documents.storagepath", - verbose_name="remove these storage path(s)", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_tags", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="documents.tag", - verbose_name="remove these tag(s)", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_view_groups", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="auth.group", - verbose_name="remove view permissions for these groups", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_view_users", - field=models.ManyToManyField( - blank=True, - related_name="+", - to=settings.AUTH_USER_MODEL, - verbose_name="remove view permissions for these users", - ), - ), - migrations.AlterField( - model_name="workflowaction", - name="assign_correspondent", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="+", - to="documents.correspondent", - verbose_name="assign this correspondent", - ), - ), - migrations.AlterField( - model_name="workflowaction", - name="assign_document_type", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="+", - to="documents.documenttype", - verbose_name="assign this document type", - ), - ), - migrations.AlterField( - model_name="workflowaction", - name="assign_storage_path", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="+", - to="documents.storagepath", - verbose_name="assign this storage path", - ), - ), - migrations.AlterField( - model_name="workflowaction", - name="assign_tags", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="documents.tag", - verbose_name="assign this tag", - ), - ), - migrations.AlterField( - model_name="workflowaction", - name="type", - field=models.PositiveIntegerField( - choices=[(1, "Assignment"), (2, "Removal")], - default=1, - verbose_name="Workflow Action Type", - ), - ), - migrations.AddField( - model_name="savedview", - name="display_mode", - field=models.CharField( - blank=True, - choices=[ - ("table", "Table"), - ("smallCards", "Small Cards"), - ("largeCards", "Large Cards"), - ], - max_length=128, - null=True, - verbose_name="View display mode", - ), - ), - migrations.AddField( - model_name="savedview", - name="page_size", - field=models.PositiveIntegerField( - blank=True, - null=True, - validators=[django.core.validators.MinValueValidator(1)], - verbose_name="View page size", - ), - ), - migrations.AddField( - model_name="savedview", - name="display_fields", - field=models.JSONField( - blank=True, - null=True, - verbose_name="Document display fields", - ), - ), - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - (18, "does not have ASN"), - (19, "title or content contains"), - (20, "fulltext query"), - (21, "more like this"), - (22, "has tags in"), - (23, "ASN greater than"), - (24, "ASN less than"), - (25, "storage path is"), - (26, "has correspondent in"), - (27, "does not have correspondent in"), - (28, "has document type in"), - (29, "does not have document type in"), - (30, "has storage path in"), - (31, "does not have storage path in"), - (32, "owner is"), - (33, "has owner in"), - (34, "does not have owner"), - (35, "does not have owner in"), - (36, "has custom field value"), - (37, "is shared by me"), - (38, "has custom fields"), - (39, "has custom field in"), - (40, "does not have custom field in"), - (41, "does not have custom field"), - ], - verbose_name="rule type", - ), - ), - migrations.AddField( - model_name="document", - name="deleted_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name="document", - name="restored_at", - field=models.DateTimeField(blank=True, null=True), - ), - ] diff --git a/src/documents/migrations/1046_workflowaction_remove_all_correspondents_and_more.py b/src/documents/migrations/1046_workflowaction_remove_all_correspondents_and_more.py deleted file mode 100644 index 3ab010a3c..000000000 --- a/src/documents/migrations/1046_workflowaction_remove_all_correspondents_and_more.py +++ /dev/null @@ -1,222 +0,0 @@ -# Generated by Django 4.2.10 on 2024-02-21 21:19 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("auth", "0012_alter_user_first_name_max_length"), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("documents", "1045_alter_customfieldinstance_value_monetary"), - ] - - operations = [ - migrations.AddField( - model_name="workflowaction", - name="remove_all_correspondents", - field=models.BooleanField( - default=False, - verbose_name="remove all correspondents", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_all_custom_fields", - field=models.BooleanField( - default=False, - verbose_name="remove all custom fields", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_all_document_types", - field=models.BooleanField( - default=False, - verbose_name="remove all document types", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_all_owners", - field=models.BooleanField(default=False, verbose_name="remove all owners"), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_all_permissions", - field=models.BooleanField( - default=False, - verbose_name="remove all permissions", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_all_storage_paths", - field=models.BooleanField( - default=False, - verbose_name="remove all storage paths", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_all_tags", - field=models.BooleanField(default=False, verbose_name="remove all tags"), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_change_groups", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="auth.group", - verbose_name="remove change permissions for these groups", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_change_users", - field=models.ManyToManyField( - blank=True, - related_name="+", - to=settings.AUTH_USER_MODEL, - verbose_name="remove change permissions for these users", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_correspondents", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="documents.correspondent", - verbose_name="remove these correspondent(s)", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_custom_fields", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="documents.customfield", - verbose_name="remove these custom fields", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_document_types", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="documents.documenttype", - verbose_name="remove these document type(s)", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_owners", - field=models.ManyToManyField( - blank=True, - related_name="+", - to=settings.AUTH_USER_MODEL, - verbose_name="remove these owner(s)", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_storage_paths", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="documents.storagepath", - verbose_name="remove these storage path(s)", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_tags", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="documents.tag", - verbose_name="remove these tag(s)", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_view_groups", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="auth.group", - verbose_name="remove view permissions for these groups", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="remove_view_users", - field=models.ManyToManyField( - blank=True, - related_name="+", - to=settings.AUTH_USER_MODEL, - verbose_name="remove view permissions for these users", - ), - ), - migrations.AlterField( - model_name="workflowaction", - name="assign_correspondent", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="+", - to="documents.correspondent", - verbose_name="assign this correspondent", - ), - ), - migrations.AlterField( - model_name="workflowaction", - name="assign_document_type", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="+", - to="documents.documenttype", - verbose_name="assign this document type", - ), - ), - migrations.AlterField( - model_name="workflowaction", - name="assign_storage_path", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="+", - to="documents.storagepath", - verbose_name="assign this storage path", - ), - ), - migrations.AlterField( - model_name="workflowaction", - name="assign_tags", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="documents.tag", - verbose_name="assign this tag", - ), - ), - migrations.AlterField( - model_name="workflowaction", - name="type", - field=models.PositiveIntegerField( - choices=[(1, "Assignment"), (2, "Removal")], - default=1, - verbose_name="Workflow Action Type", - ), - ), - ] diff --git a/src/documents/migrations/1047_savedview_display_mode_and_more.py b/src/documents/migrations/1047_savedview_display_mode_and_more.py deleted file mode 100644 index 904f86bb1..000000000 --- a/src/documents/migrations/1047_savedview_display_mode_and_more.py +++ /dev/null @@ -1,48 +0,0 @@ -# Generated by Django 4.2.11 on 2024-04-16 18:35 - -import django.core.validators -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1046_workflowaction_remove_all_correspondents_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="savedview", - name="display_mode", - field=models.CharField( - blank=True, - choices=[ - ("table", "Table"), - ("smallCards", "Small Cards"), - ("largeCards", "Large Cards"), - ], - max_length=128, - null=True, - verbose_name="View display mode", - ), - ), - migrations.AddField( - model_name="savedview", - name="page_size", - field=models.PositiveIntegerField( - blank=True, - null=True, - validators=[django.core.validators.MinValueValidator(1)], - verbose_name="View page size", - ), - ), - migrations.AddField( - model_name="savedview", - name="display_fields", - field=models.JSONField( - blank=True, - null=True, - verbose_name="Document display fields", - ), - ), - ] diff --git a/src/documents/migrations/1048_alter_savedviewfilterrule_rule_type.py b/src/documents/migrations/1048_alter_savedviewfilterrule_rule_type.py deleted file mode 100644 index 904ad242c..000000000 --- a/src/documents/migrations/1048_alter_savedviewfilterrule_rule_type.py +++ /dev/null @@ -1,64 +0,0 @@ -# Generated by Django 4.2.11 on 2024-04-24 04:58 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1047_savedview_display_mode_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - (18, "does not have ASN"), - (19, "title or content contains"), - (20, "fulltext query"), - (21, "more like this"), - (22, "has tags in"), - (23, "ASN greater than"), - (24, "ASN less than"), - (25, "storage path is"), - (26, "has correspondent in"), - (27, "does not have correspondent in"), - (28, "has document type in"), - (29, "does not have document type in"), - (30, "has storage path in"), - (31, "does not have storage path in"), - (32, "owner is"), - (33, "has owner in"), - (34, "does not have owner"), - (35, "does not have owner in"), - (36, "has custom field value"), - (37, "is shared by me"), - (38, "has custom fields"), - (39, "has custom field in"), - (40, "does not have custom field in"), - (41, "does not have custom field"), - ], - verbose_name="rule type", - ), - ), - ] diff --git a/src/documents/migrations/1049_document_deleted_at_document_restored_at.py b/src/documents/migrations/1049_document_deleted_at_document_restored_at.py deleted file mode 100644 index 39fb41353..000000000 --- a/src/documents/migrations/1049_document_deleted_at_document_restored_at.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 4.2.11 on 2024-04-23 07:56 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1048_alter_savedviewfilterrule_rule_type"), - ] - - operations = [ - migrations.AddField( - model_name="document", - name="deleted_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name="document", - name="restored_at", - field=models.DateTimeField(blank=True, null=True), - ), - ] diff --git a/src/documents/migrations/1050_customfield_extra_data_and_more.py b/src/documents/migrations/1050_customfield_extra_data_and_more.py deleted file mode 100644 index 0c6a77ccc..000000000 --- a/src/documents/migrations/1050_customfield_extra_data_and_more.py +++ /dev/null @@ -1,48 +0,0 @@ -# Generated by Django 4.2.13 on 2024-07-04 01:02 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1049_document_deleted_at_document_restored_at"), - ] - - operations = [ - migrations.AddField( - model_name="customfield", - name="extra_data", - field=models.JSONField( - blank=True, - help_text="Extra data for the custom field, such as select options", - null=True, - verbose_name="extra data", - ), - ), - migrations.AddField( - model_name="customfieldinstance", - name="value_select", - field=models.PositiveSmallIntegerField(null=True), - ), - migrations.AlterField( - model_name="customfield", - name="data_type", - field=models.CharField( - choices=[ - ("string", "String"), - ("url", "URL"), - ("date", "Date"), - ("boolean", "Boolean"), - ("integer", "Integer"), - ("float", "Float"), - ("monetary", "Monetary"), - ("documentlink", "Document Link"), - ("select", "Select"), - ], - editable=False, - max_length=50, - verbose_name="data type", - ), - ), - ] diff --git a/src/documents/migrations/1051_alter_correspondent_owner_alter_document_owner_and_more.py b/src/documents/migrations/1051_alter_correspondent_owner_alter_document_owner_and_more.py deleted file mode 100644 index e8f0bb97c..000000000 --- a/src/documents/migrations/1051_alter_correspondent_owner_alter_document_owner_and_more.py +++ /dev/null @@ -1,88 +0,0 @@ -# Generated by Django 4.2.13 on 2024-07-09 16:39 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("documents", "1050_customfield_extra_data_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="correspondent", - name="owner", - field=models.ForeignKey( - blank=True, - default=None, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AlterField( - model_name="document", - name="owner", - field=models.ForeignKey( - blank=True, - default=None, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AlterField( - model_name="documenttype", - name="owner", - field=models.ForeignKey( - blank=True, - default=None, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AlterField( - model_name="savedview", - name="owner", - field=models.ForeignKey( - blank=True, - default=None, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AlterField( - model_name="storagepath", - name="owner", - field=models.ForeignKey( - blank=True, - default=None, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AlterField( - model_name="tag", - name="owner", - field=models.ForeignKey( - blank=True, - default=None, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - ] diff --git a/src/documents/migrations/1052_document_transaction_id.py b/src/documents/migrations/1052_document_transaction_id.py deleted file mode 100644 index 5eb8e2ef9..000000000 --- a/src/documents/migrations/1052_document_transaction_id.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.15 on 2024-08-20 02:41 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1051_alter_correspondent_owner_alter_document_owner_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="document", - name="transaction_id", - field=models.UUIDField(blank=True, null=True), - ), - ] diff --git a/src/documents/migrations/1053_document_page_count.py b/src/documents/migrations/1053_document_page_count.py deleted file mode 100644 index 3a8bc5d79..000000000 --- a/src/documents/migrations/1053_document_page_count.py +++ /dev/null @@ -1,67 +0,0 @@ -# Generated by Django 5.1.1 on 2024-09-28 04:42 - -from pathlib import Path - -import pikepdf -from django.conf import settings -from django.core.validators import MinValueValidator -from django.db import migrations -from django.db import models -from django.utils.termcolors import colorize as colourise - - -def source_path(self): - if self.filename: - fname = str(self.filename) - - return Path(settings.ORIGINALS_DIR / fname).resolve() - - -def add_number_of_pages_to_page_count(apps, schema_editor): - Document = apps.get_model("documents", "Document") - - if not Document.objects.all().exists(): - return - - for doc in Document.objects.filter(mime_type="application/pdf"): - print( - " {} {} {}".format( - colourise("*", fg="green"), - colourise("Calculating number of pages for", fg="white"), - colourise(doc.filename, fg="cyan"), - ), - ) - - try: - with pikepdf.Pdf.open(source_path(doc)) as pdf: - if pdf.pages is not None: - doc.page_count = len(pdf.pages) - doc.save() - except Exception as e: # pragma: no cover - print(f"Error retrieving number of pages for {doc.filename}: {e}") - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1052_document_transaction_id"), - ] - - operations = [ - migrations.AddField( - model_name="document", - name="page_count", - field=models.PositiveIntegerField( - blank=False, - help_text="The number of pages of the document.", - null=True, - unique=False, - validators=[MinValueValidator(1)], - verbose_name="page count", - db_index=False, - ), - ), - migrations.RunPython( - add_number_of_pages_to_page_count, - migrations.RunPython.noop, - ), - ] diff --git a/src/documents/migrations/1054_customfieldinstance_value_monetary_amount_and_more.py b/src/documents/migrations/1054_customfieldinstance_value_monetary_amount_and_more.py deleted file mode 100644 index 92d45de33..000000000 --- a/src/documents/migrations/1054_customfieldinstance_value_monetary_amount_and_more.py +++ /dev/null @@ -1,95 +0,0 @@ -# Generated by Django 5.1.1 on 2024-09-29 16:26 - -import django.db.models.functions.comparison -import django.db.models.functions.text -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1053_document_page_count"), - ] - - operations = [ - migrations.AddField( - model_name="customfieldinstance", - name="value_monetary_amount", - field=models.GeneratedField( - db_persist=True, - expression=models.Case( - models.When( - then=django.db.models.functions.comparison.Cast( - django.db.models.functions.text.Substr("value_monetary", 1), - output_field=models.DecimalField( - decimal_places=2, - max_digits=65, - ), - ), - value_monetary__regex="^\\d+", - ), - default=django.db.models.functions.comparison.Cast( - django.db.models.functions.text.Substr("value_monetary", 4), - output_field=models.DecimalField( - decimal_places=2, - max_digits=65, - ), - ), - output_field=models.DecimalField(decimal_places=2, max_digits=65), - ), - output_field=models.DecimalField(decimal_places=2, max_digits=65), - ), - ), - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - (18, "does not have ASN"), - (19, "title or content contains"), - (20, "fulltext query"), - (21, "more like this"), - (22, "has tags in"), - (23, "ASN greater than"), - (24, "ASN less than"), - (25, "storage path is"), - (26, "has correspondent in"), - (27, "does not have correspondent in"), - (28, "has document type in"), - (29, "does not have document type in"), - (30, "has storage path in"), - (31, "does not have storage path in"), - (32, "owner is"), - (33, "has owner in"), - (34, "does not have owner"), - (35, "does not have owner in"), - (36, "has custom field value"), - (37, "is shared by me"), - (38, "has custom fields"), - (39, "has custom field in"), - (40, "does not have custom field in"), - (41, "does not have custom field"), - (42, "custom fields query"), - ], - verbose_name="rule type", - ), - ), - ] diff --git a/src/documents/migrations/1055_alter_storagepath_path.py b/src/documents/migrations/1055_alter_storagepath_path.py deleted file mode 100644 index 1421bf824..000000000 --- a/src/documents/migrations/1055_alter_storagepath_path.py +++ /dev/null @@ -1,36 +0,0 @@ -# Generated by Django 5.1.1 on 2024-10-03 14:47 - -from django.conf import settings -from django.db import migrations -from django.db import models -from django.db import transaction -from filelock import FileLock - -from documents.templating.utils import convert_format_str_to_template_format - - -def convert_from_format_to_template(apps, schema_editor): - StoragePath = apps.get_model("documents", "StoragePath") - - with transaction.atomic(), FileLock(settings.MEDIA_LOCK): - for storage_path in StoragePath.objects.all(): - storage_path.path = convert_format_str_to_template_format(storage_path.path) - storage_path.save() - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1054_customfieldinstance_value_monetary_amount_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="storagepath", - name="path", - field=models.TextField(verbose_name="path"), - ), - migrations.RunPython( - convert_from_format_to_template, - migrations.RunPython.noop, - ), - ] diff --git a/src/documents/migrations/1056_customfieldinstance_deleted_at_and_more.py b/src/documents/migrations/1056_customfieldinstance_deleted_at_and_more.py deleted file mode 100644 index eba1e4281..000000000 --- a/src/documents/migrations/1056_customfieldinstance_deleted_at_and_more.py +++ /dev/null @@ -1,58 +0,0 @@ -# Generated by Django 5.1.2 on 2024-10-28 01:55 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1055_alter_storagepath_path"), - ] - - operations = [ - migrations.AddField( - model_name="customfieldinstance", - name="deleted_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name="customfieldinstance", - name="restored_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name="customfieldinstance", - name="transaction_id", - field=models.UUIDField(blank=True, null=True), - ), - migrations.AddField( - model_name="note", - name="deleted_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name="note", - name="restored_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name="note", - name="transaction_id", - field=models.UUIDField(blank=True, null=True), - ), - migrations.AddField( - model_name="sharelink", - name="deleted_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name="sharelink", - name="restored_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name="sharelink", - name="transaction_id", - field=models.UUIDField(blank=True, null=True), - ), - ] diff --git a/src/documents/migrations/1057_paperlesstask_owner.py b/src/documents/migrations/1057_paperlesstask_owner.py deleted file mode 100644 index e9f108d3a..000000000 --- a/src/documents/migrations/1057_paperlesstask_owner.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 5.1.1 on 2024-11-04 21:56 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1056_customfieldinstance_deleted_at_and_more"), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AddField( - model_name="paperlesstask", - name="owner", - field=models.ForeignKey( - blank=True, - default=None, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - ] diff --git a/src/documents/migrations/1058_workflowtrigger_schedule_date_custom_field_and_more.py b/src/documents/migrations/1058_workflowtrigger_schedule_date_custom_field_and_more.py deleted file mode 100644 index 05d38578a..000000000 --- a/src/documents/migrations/1058_workflowtrigger_schedule_date_custom_field_and_more.py +++ /dev/null @@ -1,143 +0,0 @@ -# Generated by Django 5.1.1 on 2024-11-05 05:19 - -import django.core.validators -import django.db.models.deletion -import django.utils.timezone -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1057_paperlesstask_owner"), - ] - - operations = [ - migrations.AddField( - model_name="workflowtrigger", - name="schedule_date_custom_field", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.customfield", - verbose_name="schedule date custom field", - ), - ), - migrations.AddField( - model_name="workflowtrigger", - name="schedule_date_field", - field=models.CharField( - choices=[ - ("added", "Added"), - ("created", "Created"), - ("modified", "Modified"), - ("custom_field", "Custom Field"), - ], - default="added", - help_text="The field to check for a schedule trigger.", - max_length=20, - verbose_name="schedule date field", - ), - ), - migrations.AddField( - model_name="workflowtrigger", - name="schedule_is_recurring", - field=models.BooleanField( - default=False, - help_text="If the schedule should be recurring.", - verbose_name="schedule is recurring", - ), - ), - migrations.AddField( - model_name="workflowtrigger", - name="schedule_offset_days", - field=models.PositiveIntegerField( - default=0, - help_text="The number of days to offset the schedule trigger by.", - verbose_name="schedule offset days", - ), - ), - migrations.AddField( - model_name="workflowtrigger", - name="schedule_recurring_interval_days", - field=models.PositiveIntegerField( - default=1, - help_text="The number of days between recurring schedule triggers.", - validators=[django.core.validators.MinValueValidator(1)], - verbose_name="schedule recurring delay in days", - ), - ), - migrations.AlterField( - model_name="workflowtrigger", - name="type", - field=models.PositiveIntegerField( - choices=[ - (1, "Consumption Started"), - (2, "Document Added"), - (3, "Document Updated"), - (4, "Scheduled"), - ], - default=1, - verbose_name="Workflow Trigger Type", - ), - ), - migrations.CreateModel( - name="WorkflowRun", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "type", - models.PositiveIntegerField( - choices=[ - (1, "Consumption Started"), - (2, "Document Added"), - (3, "Document Updated"), - (4, "Scheduled"), - ], - null=True, - verbose_name="workflow trigger type", - ), - ), - ( - "run_at", - models.DateTimeField( - db_index=True, - default=django.utils.timezone.now, - verbose_name="date run", - ), - ), - ( - "document", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="workflow_runs", - to="documents.document", - verbose_name="document", - ), - ), - ( - "workflow", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="runs", - to="documents.workflow", - verbose_name="workflow", - ), - ), - ], - options={ - "verbose_name": "workflow run", - "verbose_name_plural": "workflow runs", - }, - ), - ] diff --git a/src/documents/migrations/1059_workflowactionemail_workflowactionwebhook_and_more.py b/src/documents/migrations/1059_workflowactionemail_workflowactionwebhook_and_more.py deleted file mode 100644 index d94470285..000000000 --- a/src/documents/migrations/1059_workflowactionemail_workflowactionwebhook_and_more.py +++ /dev/null @@ -1,154 +0,0 @@ -# Generated by Django 5.1.3 on 2024-11-26 04:07 - -import django.db.models.deletion -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1058_workflowtrigger_schedule_date_custom_field_and_more"), - ] - - operations = [ - migrations.CreateModel( - name="WorkflowActionEmail", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "subject", - models.CharField( - help_text="The subject of the email, can include some placeholders, see documentation.", - max_length=256, - verbose_name="email subject", - ), - ), - ( - "body", - models.TextField( - help_text="The body (message) of the email, can include some placeholders, see documentation.", - verbose_name="email body", - ), - ), - ( - "to", - models.TextField( - help_text="The destination email addresses, comma separated.", - verbose_name="emails to", - ), - ), - ( - "include_document", - models.BooleanField( - default=False, - verbose_name="include document in email", - ), - ), - ], - ), - migrations.CreateModel( - name="WorkflowActionWebhook", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "url", - models.URLField( - help_text="The destination URL for the notification.", - verbose_name="webhook url", - ), - ), - ( - "use_params", - models.BooleanField(default=True, verbose_name="use parameters"), - ), - ( - "params", - models.JSONField( - blank=True, - help_text="The parameters to send with the webhook URL if body not used.", - null=True, - verbose_name="webhook parameters", - ), - ), - ( - "body", - models.TextField( - blank=True, - help_text="The body to send with the webhook URL if parameters not used.", - null=True, - verbose_name="webhook body", - ), - ), - ( - "headers", - models.JSONField( - blank=True, - help_text="The headers to send with the webhook URL.", - null=True, - verbose_name="webhook headers", - ), - ), - ( - "include_document", - models.BooleanField( - default=False, - verbose_name="include document in webhook", - ), - ), - ], - ), - migrations.AlterField( - model_name="workflowaction", - name="type", - field=models.PositiveIntegerField( - choices=[ - (1, "Assignment"), - (2, "Removal"), - (3, "Email"), - (4, "Webhook"), - ], - default=1, - verbose_name="Workflow Action Type", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="email", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="action", - to="documents.workflowactionemail", - verbose_name="email", - ), - ), - migrations.AddField( - model_name="workflowaction", - name="webhook", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="action", - to="documents.workflowactionwebhook", - verbose_name="webhook", - ), - ), - ] diff --git a/src/documents/migrations/1060_alter_customfieldinstance_value_select.py b/src/documents/migrations/1060_alter_customfieldinstance_value_select.py deleted file mode 100644 index 21f3f8b41..000000000 --- a/src/documents/migrations/1060_alter_customfieldinstance_value_select.py +++ /dev/null @@ -1,79 +0,0 @@ -# Generated by Django 5.1.1 on 2024-11-13 05:14 - -from django.db import migrations -from django.db import models -from django.db import transaction -from django.utils.crypto import get_random_string - - -def migrate_customfield_selects(apps, schema_editor): - """ - Migrate the custom field selects from a simple list of strings to a list of dictionaries with - label and id. Then update all instances of the custom field to use the new format. - """ - CustomFieldInstance = apps.get_model("documents", "CustomFieldInstance") - CustomField = apps.get_model("documents", "CustomField") - - with transaction.atomic(): - for custom_field in CustomField.objects.filter( - data_type="select", - ): # CustomField.FieldDataType.SELECT - old_select_options = custom_field.extra_data["select_options"] - custom_field.extra_data["select_options"] = [ - {"id": get_random_string(16), "label": value} - for value in old_select_options - ] - custom_field.save() - - for instance in CustomFieldInstance.objects.filter(field=custom_field): - if instance.value_select: - instance.value_select = custom_field.extra_data["select_options"][ - int(instance.value_select) - ]["id"] - instance.save() - - -def reverse_migrate_customfield_selects(apps, schema_editor): - """ - Reverse the migration of the custom field selects from a list of dictionaries with label and id - to a simple list of strings. Then update all instances of the custom field to use the old format, - which is just the index of the selected option. - """ - CustomFieldInstance = apps.get_model("documents", "CustomFieldInstance") - CustomField = apps.get_model("documents", "CustomField") - - with transaction.atomic(): - for custom_field in CustomField.objects.all(): - if custom_field.data_type == "select": # CustomField.FieldDataType.SELECT - old_select_options = custom_field.extra_data["select_options"] - custom_field.extra_data["select_options"] = [ - option["label"] - for option in custom_field.extra_data["select_options"] - ] - custom_field.save() - - for instance in CustomFieldInstance.objects.filter(field=custom_field): - instance.value_select = next( - index - for index, option in enumerate(old_select_options) - if option.get("id") == instance.value_select - ) - instance.save() - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1059_workflowactionemail_workflowactionwebhook_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="customfieldinstance", - name="value_select", - field=models.CharField(max_length=16, null=True), - ), - migrations.RunPython( - migrate_customfield_selects, - reverse_migrate_customfield_selects, - ), - ] diff --git a/src/documents/migrations/1061_workflowactionwebhook_as_json.py b/src/documents/migrations/1061_workflowactionwebhook_as_json.py deleted file mode 100644 index f1945cfc1..000000000 --- a/src/documents/migrations/1061_workflowactionwebhook_as_json.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.1.4 on 2025-01-18 19:35 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1060_alter_customfieldinstance_value_select"), - ] - - operations = [ - migrations.AddField( - model_name="workflowactionwebhook", - name="as_json", - field=models.BooleanField(default=False, verbose_name="send as JSON"), - ), - ] diff --git a/src/documents/migrations/1062_alter_savedviewfilterrule_rule_type.py b/src/documents/migrations/1062_alter_savedviewfilterrule_rule_type.py deleted file mode 100644 index c5a6bb90e..000000000 --- a/src/documents/migrations/1062_alter_savedviewfilterrule_rule_type.py +++ /dev/null @@ -1,70 +0,0 @@ -# Generated by Django 5.1.4 on 2025-02-06 05:54 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1061_workflowactionwebhook_as_json"), - ] - - operations = [ - migrations.AlterField( - model_name="savedviewfilterrule", - name="rule_type", - field=models.PositiveIntegerField( - choices=[ - (0, "title contains"), - (1, "content contains"), - (2, "ASN is"), - (3, "correspondent is"), - (4, "document type is"), - (5, "is in inbox"), - (6, "has tag"), - (7, "has any tag"), - (8, "created before"), - (9, "created after"), - (10, "created year is"), - (11, "created month is"), - (12, "created day is"), - (13, "added before"), - (14, "added after"), - (15, "modified before"), - (16, "modified after"), - (17, "does not have tag"), - (18, "does not have ASN"), - (19, "title or content contains"), - (20, "fulltext query"), - (21, "more like this"), - (22, "has tags in"), - (23, "ASN greater than"), - (24, "ASN less than"), - (25, "storage path is"), - (26, "has correspondent in"), - (27, "does not have correspondent in"), - (28, "has document type in"), - (29, "does not have document type in"), - (30, "has storage path in"), - (31, "does not have storage path in"), - (32, "owner is"), - (33, "has owner in"), - (34, "does not have owner"), - (35, "does not have owner in"), - (36, "has custom field value"), - (37, "is shared by me"), - (38, "has custom fields"), - (39, "has custom field in"), - (40, "does not have custom field in"), - (41, "does not have custom field"), - (42, "custom fields query"), - (43, "created to"), - (44, "created from"), - (45, "added to"), - (46, "added from"), - (47, "mime type is"), - ], - verbose_name="rule type", - ), - ), - ] diff --git a/src/documents/migrations/1063_paperlesstask_type_alter_paperlesstask_task_name_and_more.py b/src/documents/migrations/1063_paperlesstask_type_alter_paperlesstask_task_name_and_more.py deleted file mode 100644 index aeedbd6a0..000000000 --- a/src/documents/migrations/1063_paperlesstask_type_alter_paperlesstask_task_name_and_more.py +++ /dev/null @@ -1,92 +0,0 @@ -# Generated by Django 5.1.6 on 2025-02-21 16:34 - -import multiselectfield.db.fields -from django.db import migrations -from django.db import models - - -# WebUI source was added, so all existing APIUpload sources should be updated to include WebUI -def update_workflow_sources(apps, schema_editor): - WorkflowTrigger = apps.get_model("documents", "WorkflowTrigger") - for trigger in WorkflowTrigger.objects.all(): - sources = list(trigger.sources) - if 2 in sources: - sources.append(4) - trigger.sources = sources - trigger.save() - - -def make_existing_tasks_consume_auto(apps, schema_editor): - PaperlessTask = apps.get_model("documents", "PaperlessTask") - PaperlessTask.objects.all().update(type="auto_task", task_name="consume_file") - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1062_alter_savedviewfilterrule_rule_type"), - ] - - operations = [ - migrations.AddField( - model_name="paperlesstask", - name="type", - field=models.CharField( - choices=[ - ("auto_task", "Auto Task"), - ("scheduled_task", "Scheduled Task"), - ("manual_task", "Manual Task"), - ], - default="auto_task", - help_text="The type of task that was run", - max_length=30, - verbose_name="Task Type", - ), - ), - migrations.AlterField( - model_name="paperlesstask", - name="task_name", - field=models.CharField( - choices=[ - ("consume_file", "Consume File"), - ("train_classifier", "Train Classifier"), - ("check_sanity", "Check Sanity"), - ("index_optimize", "Index Optimize"), - ], - help_text="Name of the task that was run", - max_length=255, - null=True, - verbose_name="Task Name", - ), - ), - migrations.RunPython( - code=make_existing_tasks_consume_auto, - reverse_code=migrations.RunPython.noop, - ), - migrations.AlterField( - model_name="workflowactionwebhook", - name="url", - field=models.CharField( - help_text="The destination URL for the notification.", - max_length=256, - verbose_name="webhook url", - ), - ), - migrations.AlterField( - model_name="workflowtrigger", - name="sources", - field=multiselectfield.db.fields.MultiSelectField( - choices=[ - (1, "Consume Folder"), - (2, "Api Upload"), - (3, "Mail Fetch"), - (4, "Web UI"), - ], - default="1,2,3,4", - max_length=7, - ), - ), - migrations.RunPython( - code=update_workflow_sources, - reverse_code=migrations.RunPython.noop, - ), - ] diff --git a/src/documents/migrations/1064_delete_log.py b/src/documents/migrations/1064_delete_log.py deleted file mode 100644 index ec0830a91..000000000 --- a/src/documents/migrations/1064_delete_log.py +++ /dev/null @@ -1,15 +0,0 @@ -# Generated by Django 5.1.6 on 2025-02-28 15:19 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1063_paperlesstask_type_alter_paperlesstask_task_name_and_more"), - ] - - operations = [ - migrations.DeleteModel( - name="Log", - ), - ] diff --git a/src/documents/migrations/1065_workflowaction_assign_custom_fields_values.py b/src/documents/migrations/1065_workflowaction_assign_custom_fields_values.py deleted file mode 100644 index 35fae02be..000000000 --- a/src/documents/migrations/1065_workflowaction_assign_custom_fields_values.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 5.1.6 on 2025-03-01 18:10 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1064_delete_log"), - ] - - operations = [ - migrations.AddField( - model_name="workflowaction", - name="assign_custom_fields_values", - field=models.JSONField( - blank=True, - help_text="Optional values to assign to the custom fields.", - null=True, - verbose_name="custom field values", - default=dict, - ), - ), - ] diff --git a/src/documents/migrations/1066_alter_workflowtrigger_schedule_offset_days.py b/src/documents/migrations/1066_alter_workflowtrigger_schedule_offset_days.py deleted file mode 100644 index eaf23ad64..000000000 --- a/src/documents/migrations/1066_alter_workflowtrigger_schedule_offset_days.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 5.1.7 on 2025-04-15 19:18 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1065_workflowaction_assign_custom_fields_values"), - ] - - operations = [ - migrations.AlterField( - model_name="workflowtrigger", - name="schedule_offset_days", - field=models.IntegerField( - default=0, - help_text="The number of days to offset the schedule trigger by.", - verbose_name="schedule offset days", - ), - ), - ] diff --git a/src/documents/migrations/1067_alter_document_created.py b/src/documents/migrations/1067_alter_document_created.py deleted file mode 100644 index 0f96bce3d..000000000 --- a/src/documents/migrations/1067_alter_document_created.py +++ /dev/null @@ -1,76 +0,0 @@ -# Generated by Django 5.1.7 on 2025-04-04 01:08 - - -import datetime - -from django.db import migrations -from django.db import models -from django.utils.timezone import localtime - - -def migrate_date(apps, schema_editor): - Document = apps.get_model("documents", "Document") - - # Batch to avoid loading all objects into memory at once, - # which would be problematic for large datasets. - batch_size = 500 - updates = [] - total_updated = 0 - total_checked = 0 - - for doc in Document.objects.only("id", "created").iterator(chunk_size=batch_size): - total_checked += 1 - if doc.created: - doc.created_date = localtime(doc.created).date() - updates.append(doc) - - if len(updates) >= batch_size: - Document.objects.bulk_update(updates, ["created_date"]) - total_updated += len(updates) - print( - f"[1067_alter_document_created] {total_updated} of {total_checked} processed...", - ) - updates.clear() - - if updates: - Document.objects.bulk_update(updates, ["created_date"]) - total_updated += len(updates) - print( - f"[1067_alter_document_created] {total_updated} of {total_checked} processed...", - ) - - if total_checked > 0: - print(f"[1067_alter_document_created] completed for {total_checked} documents.") - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1066_alter_workflowtrigger_schedule_offset_days"), - ] - - operations = [ - migrations.AddField( - model_name="document", - name="created_date", - field=models.DateField(null=True), - ), - migrations.RunPython(migrate_date, reverse_code=migrations.RunPython.noop), - migrations.RemoveField( - model_name="document", - name="created", - ), - migrations.RenameField( - model_name="document", - old_name="created_date", - new_name="created", - ), - migrations.AlterField( - model_name="document", - name="created", - field=models.DateField( - db_index=True, - default=datetime.datetime.today, - verbose_name="created", - ), - ), - ] diff --git a/src/documents/migrations/1068_alter_document_created.py b/src/documents/migrations/1068_alter_document_created.py deleted file mode 100644 index b673f6584..000000000 --- a/src/documents/migrations/1068_alter_document_created.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 5.1.8 on 2025-05-23 05:50 - -import datetime - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1067_alter_document_created"), - ] - - operations = [ - migrations.AlterField( - model_name="document", - name="created", - field=models.DateField( - db_index=True, - default=datetime.date.today, - verbose_name="created", - ), - ), - ] diff --git a/src/documents/migrations/1069_workflowtrigger_filter_has_storage_path_and_more.py b/src/documents/migrations/1069_workflowtrigger_filter_has_storage_path_and_more.py deleted file mode 100644 index 47db2fd91..000000000 --- a/src/documents/migrations/1069_workflowtrigger_filter_has_storage_path_and_more.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 5.2.6 on 2025-09-11 17:29 - -import django.db.models.deletion -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1068_alter_document_created"), - ] - - operations = [ - migrations.AddField( - model_name="workflowtrigger", - name="filter_has_storage_path", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.storagepath", - verbose_name="has this storage path", - ), - ), - migrations.AlterField( - model_name="workflowaction", - name="assign_title", - field=models.TextField( - blank=True, - help_text="Assign a document title, must be a Jinja2 template, see documentation.", - null=True, - verbose_name="assign title", - ), - ), - ] diff --git a/src/documents/migrations/1070_customfieldinstance_value_long_text_and_more.py b/src/documents/migrations/1070_customfieldinstance_value_long_text_and_more.py deleted file mode 100644 index 69c77d29a..000000000 --- a/src/documents/migrations/1070_customfieldinstance_value_long_text_and_more.py +++ /dev/null @@ -1,39 +0,0 @@ -# Generated by Django 5.2.6 on 2025-09-13 17:11 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1069_workflowtrigger_filter_has_storage_path_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="customfieldinstance", - name="value_long_text", - field=models.TextField(null=True), - ), - migrations.AlterField( - model_name="customfield", - name="data_type", - field=models.CharField( - choices=[ - ("string", "String"), - ("url", "URL"), - ("date", "Date"), - ("boolean", "Boolean"), - ("integer", "Integer"), - ("float", "Float"), - ("monetary", "Monetary"), - ("documentlink", "Document Link"), - ("select", "Select"), - ("longtext", "Long Text"), - ], - editable=False, - max_length=50, - verbose_name="data type", - ), - ), - ] diff --git a/src/documents/migrations/1071_tag_tn_ancestors_count_tag_tn_ancestors_pks_and_more.py b/src/documents/migrations/1071_tag_tn_ancestors_count_tag_tn_ancestors_pks_and_more.py deleted file mode 100644 index 3e097620e..000000000 --- a/src/documents/migrations/1071_tag_tn_ancestors_count_tag_tn_ancestors_pks_and_more.py +++ /dev/null @@ -1,159 +0,0 @@ -# Generated by Django 5.2.6 on 2025-09-12 18:42 - -import django.core.validators -import django.db.models.deletion -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1070_customfieldinstance_value_long_text_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="tag", - name="tn_ancestors_count", - field=models.PositiveIntegerField( - default=0, - editable=False, - verbose_name="Ancestors count", - ), - ), - migrations.AddField( - model_name="tag", - name="tn_ancestors_pks", - field=models.TextField( - blank=True, - default="", - editable=False, - verbose_name="Ancestors pks", - ), - ), - migrations.AddField( - model_name="tag", - name="tn_children_count", - field=models.PositiveIntegerField( - default=0, - editable=False, - verbose_name="Children count", - ), - ), - migrations.AddField( - model_name="tag", - name="tn_children_pks", - field=models.TextField( - blank=True, - default="", - editable=False, - verbose_name="Children pks", - ), - ), - migrations.AddField( - model_name="tag", - name="tn_depth", - field=models.PositiveIntegerField( - default=0, - editable=False, - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(10), - ], - verbose_name="Depth", - ), - ), - migrations.AddField( - model_name="tag", - name="tn_descendants_count", - field=models.PositiveIntegerField( - default=0, - editable=False, - verbose_name="Descendants count", - ), - ), - migrations.AddField( - model_name="tag", - name="tn_descendants_pks", - field=models.TextField( - blank=True, - default="", - editable=False, - verbose_name="Descendants pks", - ), - ), - migrations.AddField( - model_name="tag", - name="tn_index", - field=models.PositiveIntegerField( - default=0, - editable=False, - verbose_name="Index", - ), - ), - migrations.AddField( - model_name="tag", - name="tn_level", - field=models.PositiveIntegerField( - default=1, - editable=False, - validators=[ - django.core.validators.MinValueValidator(1), - django.core.validators.MaxValueValidator(10), - ], - verbose_name="Level", - ), - ), - migrations.AddField( - model_name="tag", - name="tn_order", - field=models.PositiveIntegerField( - default=0, - editable=False, - verbose_name="Order", - ), - ), - migrations.AddField( - model_name="tag", - name="tn_parent", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="tn_children", - to="documents.tag", - verbose_name="Parent", - ), - ), - migrations.AddField( - model_name="tag", - name="tn_priority", - field=models.PositiveIntegerField( - default=0, - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(9999999999), - ], - verbose_name="Priority", - ), - ), - migrations.AddField( - model_name="tag", - name="tn_siblings_count", - field=models.PositiveIntegerField( - default=0, - editable=False, - verbose_name="Siblings count", - ), - ), - migrations.AddField( - model_name="tag", - name="tn_siblings_pks", - field=models.TextField( - blank=True, - default="", - editable=False, - verbose_name="Siblings pks", - ), - ), - ] diff --git a/src/documents/migrations/1072_workflowtrigger_filter_custom_field_query_and_more.py b/src/documents/migrations/1072_workflowtrigger_filter_custom_field_query_and_more.py deleted file mode 100644 index 1a22f6b4f..000000000 --- a/src/documents/migrations/1072_workflowtrigger_filter_custom_field_query_and_more.py +++ /dev/null @@ -1,73 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-07 18:52 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1071_tag_tn_ancestors_count_tag_tn_ancestors_pks_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="workflowtrigger", - name="filter_custom_field_query", - field=models.TextField( - blank=True, - help_text="JSON-encoded custom field query expression.", - null=True, - verbose_name="filter custom field query", - ), - ), - migrations.AddField( - model_name="workflowtrigger", - name="filter_has_all_tags", - field=models.ManyToManyField( - blank=True, - related_name="workflowtriggers_has_all", - to="documents.tag", - verbose_name="has all of these tag(s)", - ), - ), - migrations.AddField( - model_name="workflowtrigger", - name="filter_has_not_correspondents", - field=models.ManyToManyField( - blank=True, - related_name="workflowtriggers_has_not_correspondent", - to="documents.correspondent", - verbose_name="does not have these correspondent(s)", - ), - ), - migrations.AddField( - model_name="workflowtrigger", - name="filter_has_not_document_types", - field=models.ManyToManyField( - blank=True, - related_name="workflowtriggers_has_not_document_type", - to="documents.documenttype", - verbose_name="does not have these document type(s)", - ), - ), - migrations.AddField( - model_name="workflowtrigger", - name="filter_has_not_storage_paths", - field=models.ManyToManyField( - blank=True, - related_name="workflowtriggers_has_not_storage_path", - to="documents.storagepath", - verbose_name="does not have these storage path(s)", - ), - ), - migrations.AddField( - model_name="workflowtrigger", - name="filter_has_not_tags", - field=models.ManyToManyField( - blank=True, - related_name="workflowtriggers_has_not", - to="documents.tag", - verbose_name="does not have these tag(s)", - ), - ), - ] diff --git a/src/documents/migrations/1073_migrate_workflow_title_jinja.py b/src/documents/migrations/1073_migrate_workflow_title_jinja.py deleted file mode 100644 index 9d80a277f..000000000 --- a/src/documents/migrations/1073_migrate_workflow_title_jinja.py +++ /dev/null @@ -1,63 +0,0 @@ -# Generated by Django 5.2.5 on 2025-08-27 22:02 -import logging - -from django.db import migrations -from django.db import models - -from documents.templating.utils import convert_format_str_to_template_format - -logger = logging.getLogger("paperless.migrations") - - -def convert_from_format_to_template(apps, schema_editor): - WorkflowAction = apps.get_model("documents", "WorkflowAction") - - batch_size = 500 - actions_to_update = [] - - queryset = ( - WorkflowAction.objects.filter(assign_title__isnull=False) - .exclude(assign_title="") - .only("id", "assign_title") - ) - - for action in queryset: - action.assign_title = convert_format_str_to_template_format( - action.assign_title, - ) - logger.debug( - "Converted WorkflowAction id %d title to template format: %s", - action.id, - action.assign_title, - ) - actions_to_update.append(action) - - if actions_to_update: - WorkflowAction.objects.bulk_update( - actions_to_update, - ["assign_title"], - batch_size=batch_size, - ) - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1072_workflowtrigger_filter_custom_field_query_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="workflowaction", - name="assign_title", - field=models.TextField( - blank=True, - help_text="Assign a document title, must be a Jinja2 template, see documentation.", - null=True, - verbose_name="assign title", - ), - ), - migrations.RunPython( - convert_from_format_to_template, - migrations.RunPython.noop, - ), - ] diff --git a/src/documents/migrations/1074_workflowrun_deleted_at_workflowrun_restored_at_and_more.py b/src/documents/migrations/1074_workflowrun_deleted_at_workflowrun_restored_at_and_more.py deleted file mode 100644 index 4381eabb1..000000000 --- a/src/documents/migrations/1074_workflowrun_deleted_at_workflowrun_restored_at_and_more.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-27 15:11 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1073_migrate_workflow_title_jinja"), - ] - - operations = [ - migrations.AddField( - model_name="workflowrun", - name="deleted_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name="workflowrun", - name="restored_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name="workflowrun", - name="transaction_id", - field=models.UUIDField(blank=True, null=True), - ), - ] diff --git a/src/documents/models.py b/src/documents/models.py index 12dab2b6d..72470ef6e 100644 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -20,7 +20,9 @@ if settings.AUDIT_LOG_ENABLED: from auditlog.registry import auditlog from django.db.models import Case +from django.db.models import PositiveIntegerField from django.db.models.functions import Cast +from django.db.models.functions import Length from django.db.models.functions import Substr from django_softdelete.models import SoftDeleteModel @@ -154,13 +156,6 @@ class StoragePath(MatchingModel): class Document(SoftDeleteModel, ModelWithOwner): - STORAGE_TYPE_UNENCRYPTED = "unencrypted" - STORAGE_TYPE_GPG = "gpg" - STORAGE_TYPES = ( - (STORAGE_TYPE_UNENCRYPTED, _("Unencrypted")), - (STORAGE_TYPE_GPG, _("Encrypted with GNU Privacy Guard")), - ) - correspondent = models.ForeignKey( Correspondent, blank=True, @@ -199,6 +194,15 @@ class Document(SoftDeleteModel, ModelWithOwner): ), ) + content_length = models.GeneratedField( + expression=Length("content"), + output_field=PositiveIntegerField(default=0), + db_persist=True, + null=False, + serialize=False, + help_text="Length of the content field in characters. Automatically maintained by the database for faster statistics computation.", + ) + mime_type = models.CharField(_("mime type"), max_length=256, editable=False) tags = models.ManyToManyField( @@ -212,7 +216,6 @@ class Document(SoftDeleteModel, ModelWithOwner): _("checksum"), max_length=32, editable=False, - unique=True, help_text=_("The checksum of the original document."), ) @@ -250,14 +253,6 @@ class Document(SoftDeleteModel, ModelWithOwner): db_index=True, ) - storage_type = models.CharField( - _("storage type"), - max_length=11, - choices=STORAGE_TYPES, - default=STORAGE_TYPE_UNENCRYPTED, - editable=False, - ) - added = models.DateTimeField( _("added"), default=timezone.now, @@ -353,12 +348,7 @@ class Document(SoftDeleteModel, ModelWithOwner): @property def source_path(self) -> Path: - if self.filename: - fname = str(self.filename) - else: - fname = f"{self.pk:07}{self.file_type}" - if self.storage_type == self.STORAGE_TYPE_GPG: - fname += ".gpg" # pragma: no cover + fname = str(self.filename) if self.filename else f"{self.pk:07}{self.file_type}" return (settings.ORIGINALS_DIR / Path(fname)).resolve() @@ -407,8 +397,6 @@ class Document(SoftDeleteModel, ModelWithOwner): @property def thumbnail_path(self) -> Path: webp_file_name = f"{self.pk:07}.webp" - if self.storage_type == self.STORAGE_TYPE_GPG: - webp_file_name += ".gpg" webp_file_path = settings.THUMBNAIL_DIR / Path(webp_file_name) @@ -598,6 +586,7 @@ class PaperlessTask(ModelWithOwner): TRAIN_CLASSIFIER = ("train_classifier", _("Train Classifier")) CHECK_SANITY = ("check_sanity", _("Check Sanity")) INDEX_OPTIMIZE = ("index_optimize", _("Index Optimize")) + LLMINDEX_UPDATE = ("llmindex_update", _("LLM Index Update")) task_id = models.CharField( max_length=255, @@ -777,6 +766,114 @@ class ShareLink(SoftDeleteModel): return f"Share Link for {self.document.title}" +class ShareLinkBundle(models.Model): + class Status(models.TextChoices): + PENDING = ("pending", _("Pending")) + PROCESSING = ("processing", _("Processing")) + READY = ("ready", _("Ready")) + FAILED = ("failed", _("Failed")) + + created = models.DateTimeField( + _("created"), + default=timezone.now, + db_index=True, + blank=True, + editable=False, + ) + + expiration = models.DateTimeField( + _("expiration"), + blank=True, + null=True, + db_index=True, + ) + + slug = models.SlugField( + _("slug"), + db_index=True, + unique=True, + blank=True, + editable=False, + ) + + owner = models.ForeignKey( + User, + blank=True, + null=True, + related_name="share_link_bundles", + on_delete=models.SET_NULL, + verbose_name=_("owner"), + ) + + file_version = models.CharField( + max_length=50, + choices=ShareLink.FileVersion.choices, + default=ShareLink.FileVersion.ARCHIVE, + ) + + status = models.CharField( + max_length=50, + choices=Status.choices, + default=Status.PENDING, + ) + + size_bytes = models.PositiveIntegerField( + _("size (bytes)"), + blank=True, + null=True, + ) + + last_error = models.JSONField( + _("last error"), + blank=True, + null=True, + default=None, + ) + + file_path = models.CharField( + _("file path"), + max_length=512, + blank=True, + ) + + built_at = models.DateTimeField( + _("built at"), + null=True, + blank=True, + ) + + documents = models.ManyToManyField( + "documents.Document", + related_name="share_link_bundles", + verbose_name=_("documents"), + ) + + class Meta: + ordering = ("-created",) + verbose_name = _("share link bundle") + verbose_name_plural = _("share link bundles") + + def __str__(self): + return _("Share link bundle %(slug)s") % {"slug": self.slug} + + @property + def absolute_file_path(self) -> Path | None: + if not self.file_path: + return None + return (settings.SHARE_LINK_BUNDLE_DIR / Path(self.file_path)).resolve() + + def remove_file(self): + if self.absolute_file_path is not None and self.absolute_file_path.exists(): + try: + self.absolute_file_path.unlink() + except OSError: + pass + + def delete(self, using=None, *, keep_parents=False): + self.remove_file() + return super().delete(using=using, keep_parents=keep_parents) + + class CustomField(models.Model): """ Defines the name and type of a custom field @@ -967,7 +1064,7 @@ if settings.AUDIT_LOG_ENABLED: auditlog.register( Document, m2m_fields={"tags"}, - exclude_fields=["modified"], + exclude_fields=["content_length", "modified"], ) auditlog.register(Correspondent) auditlog.register(Tag) @@ -1087,6 +1184,13 @@ class WorkflowTrigger(models.Model): verbose_name=_("has this document type"), ) + filter_has_any_document_types = models.ManyToManyField( + DocumentType, + blank=True, + related_name="workflowtriggers_has_any_document_type", + verbose_name=_("has one of these document types"), + ) + filter_has_not_document_types = models.ManyToManyField( DocumentType, blank=True, @@ -1109,6 +1213,13 @@ class WorkflowTrigger(models.Model): verbose_name=_("does not have these correspondent(s)"), ) + filter_has_any_correspondents = models.ManyToManyField( + Correspondent, + blank=True, + related_name="workflowtriggers_has_any_correspondent", + verbose_name=_("has one of these correspondents"), + ) + filter_has_storage_path = models.ForeignKey( StoragePath, null=True, @@ -1117,6 +1228,13 @@ class WorkflowTrigger(models.Model): verbose_name=_("has this storage path"), ) + filter_has_any_storage_paths = models.ManyToManyField( + StoragePath, + blank=True, + related_name="workflowtriggers_has_any_storage_path", + verbose_name=_("has one of these storage paths"), + ) + filter_has_not_storage_paths = models.ManyToManyField( StoragePath, blank=True, @@ -1294,6 +1412,8 @@ class WorkflowAction(models.Model): default=WorkflowActionType.ASSIGNMENT, ) + order = models.PositiveIntegerField(_("order"), default=0) + assign_title = models.TextField( _("assign title"), null=True, diff --git a/src/documents/permissions.py b/src/documents/permissions.py index ac6d3f9ca..9d5c9eb68 100644 --- a/src/documents/permissions.py +++ b/src/documents/permissions.py @@ -148,13 +148,29 @@ def get_document_count_filter_for_user(user): ) -def get_objects_for_user_owner_aware(user, perms, Model) -> QuerySet: - objects_owned = Model.objects.filter(owner=user) - objects_unowned = Model.objects.filter(owner__isnull=True) +def get_objects_for_user_owner_aware( + user, + perms, + Model, + *, + include_deleted=False, +) -> QuerySet: + """ + Returns objects the user owns, are unowned, or has explicit perms. + When include_deleted is True, soft-deleted items are also included. + """ + manager = ( + Model.global_objects + if include_deleted and hasattr(Model, "global_objects") + else Model.objects + ) + + objects_owned = manager.filter(owner=user) + objects_unowned = manager.filter(owner__isnull=True) objects_with_perms = get_objects_for_user( user=user, perms=perms, - klass=Model, + klass=manager.all(), accept_global_perms=False, ) return objects_owned | objects_unowned | objects_with_perms diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 6e2307c2e..de5f4d33f 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -4,6 +4,7 @@ import logging import math import re from datetime import datetime +from datetime import timedelta from decimal import Decimal from typing import TYPE_CHECKING from typing import Literal @@ -23,7 +24,9 @@ from django.core.validators import MinValueValidator from django.core.validators import RegexValidator from django.core.validators import integer_validator from django.db.models import Count +from django.db.models import Q from django.db.models.functions import Lower +from django.utils import timezone from django.utils.crypto import get_random_string from django.utils.dateparse import parse_datetime from django.utils.text import slugify @@ -61,6 +64,7 @@ from documents.models import PaperlessTask from documents.models import SavedView from documents.models import SavedViewFilterRule from documents.models import ShareLink +from documents.models import ShareLinkBundle from documents.models import StoragePath from documents.models import Tag from documents.models import UiSettings @@ -72,6 +76,7 @@ from documents.models import WorkflowTrigger from documents.parsers import is_mime_type_supported from documents.permissions import get_document_count_filter_for_user from documents.permissions import get_groups_with_only_permission +from documents.permissions import get_objects_for_user_owner_aware from documents.permissions import set_permissions_for_object from documents.regex import validate_regex_pattern from documents.templating.filepath import validate_filepath_template_and_render @@ -82,6 +87,9 @@ from documents.validators import url_validator if TYPE_CHECKING: from collections.abc import Iterable + from django.db.models.query import QuerySet + + logger = logging.getLogger("paperless.serializers") @@ -580,30 +588,34 @@ class TagSerializer(MatchingModelSerializer, OwnedObjectSerializer): ), ) def get_children(self, obj): - filter_q = self.context.get("document_count_filter") - request = self.context.get("request") - if filter_q is None: - user = getattr(request, "user", None) if request else None - filter_q = get_document_count_filter_for_user(user) - self.context["document_count_filter"] = filter_q + children_map = self.context.get("children_map") + if children_map is not None: + children = children_map.get(obj.pk, []) + else: + filter_q = self.context.get("document_count_filter") + request = self.context.get("request") + if filter_q is None: + user = getattr(request, "user", None) if request else None + filter_q = get_document_count_filter_for_user(user) + self.context["document_count_filter"] = filter_q - children_queryset = ( - obj.get_children_queryset() - .select_related("owner") - .annotate(document_count=Count("documents", filter=filter_q)) - ) + children = ( + obj.get_children_queryset() + .select_related("owner") + .annotate(document_count=Count("documents", filter=filter_q)) + ) - view = self.context.get("view") - ordering = ( - OrderingFilter().get_ordering(request, children_queryset, view) - if request and view - else None - ) - ordering = ordering or (Lower("name"),) - children_queryset = children_queryset.order_by(*ordering) + view = self.context.get("view") + ordering = ( + OrderingFilter().get_ordering(request, children, view) + if request and view + else None + ) + ordering = ordering or (Lower("name"),) + children = children.order_by(*ordering) serializer = TagSerializer( - children_queryset, + children, many=True, user=self.user, full_perms=self.full_perms, @@ -1010,6 +1022,32 @@ class NotesSerializer(serializers.ModelSerializer): return ret +def _get_viewable_duplicates( + document: Document, + user: User | None, +) -> QuerySet[Document]: + checksums = {document.checksum} + if document.archive_checksum: + checksums.add(document.archive_checksum) + duplicates = Document.global_objects.filter( + Q(checksum__in=checksums) | Q(archive_checksum__in=checksums), + ).exclude(pk=document.pk) + duplicates = duplicates.order_by("-created") + allowed = get_objects_for_user_owner_aware( + user, + "documents.view_document", + Document, + include_deleted=True, + ) + return duplicates.filter(id__in=allowed) + + +class DuplicateDocumentSummarySerializer(serializers.Serializer): + id = serializers.IntegerField() + title = serializers.CharField() + deleted_at = serializers.DateTimeField(allow_null=True) + + @extend_schema_serializer( deprecate_fields=["created_date"], ) @@ -1027,6 +1065,7 @@ class DocumentSerializer( archived_file_name = SerializerMethodField() created_date = serializers.DateField(required=False) page_count = SerializerMethodField() + duplicate_documents = SerializerMethodField() notes = NotesSerializer(many=True, required=False, read_only=True) @@ -1052,6 +1091,16 @@ class DocumentSerializer( def get_page_count(self, obj) -> int | None: return obj.page_count + @extend_schema_field(DuplicateDocumentSummarySerializer(many=True)) + def get_duplicate_documents(self, obj): + view = self.context.get("view") + if view and getattr(view, "action", None) != "retrieve": + return [] + request = self.context.get("request") + user = request.user if request else None + duplicates = _get_viewable_duplicates(obj, user) + return list(duplicates.values("id", "title", "deleted_at")) + def get_original_file_name(self, obj) -> str | None: return obj.original_filename @@ -1229,6 +1278,7 @@ class DocumentSerializer( "archive_serial_number", "original_file_name", "archived_file_name", + "duplicate_documents", "owner", "permissions", "user_can_change", @@ -2090,10 +2140,12 @@ class TasksViewSerializer(OwnedObjectSerializer): "result", "acknowledged", "related_document", + "duplicate_documents", "owner", ) related_document = serializers.SerializerMethodField() + duplicate_documents = serializers.SerializerMethodField() created_doc_re = re.compile(r"New document id (\d+) created") duplicate_doc_re = re.compile(r"It is a duplicate of .* \(#(\d+)\)") @@ -2118,6 +2170,17 @@ class TasksViewSerializer(OwnedObjectSerializer): return result + @extend_schema_field(DuplicateDocumentSummarySerializer(many=True)) + def get_duplicate_documents(self, obj): + related_document = self.get_related_document(obj) + request = self.context.get("request") + user = request.user if request else None + document = Document.global_objects.filter(pk=related_document).first() + if not related_document or not user or not document: + return [] + duplicates = _get_viewable_duplicates(document, user) + return list(duplicates.values("id", "title", "deleted_at")) + class RunTaskViewSerializer(serializers.Serializer): task_name = serializers.ChoiceField( @@ -2168,6 +2231,104 @@ class ShareLinkSerializer(OwnedObjectSerializer): return super().create(validated_data) +class ShareLinkBundleSerializer(OwnedObjectSerializer): + document_ids = serializers.ListField( + child=serializers.IntegerField(min_value=1), + allow_empty=False, + write_only=True, + ) + expiration_days = serializers.IntegerField( + required=False, + allow_null=True, + min_value=1, + write_only=True, + ) + documents = serializers.PrimaryKeyRelatedField( + many=True, + read_only=True, + ) + document_count = SerializerMethodField() + + class Meta: + model = ShareLinkBundle + fields = ( + "id", + "created", + "expiration", + "expiration_days", + "slug", + "file_version", + "status", + "size_bytes", + "last_error", + "built_at", + "documents", + "document_ids", + "document_count", + ) + read_only_fields = ( + "id", + "created", + "expiration", + "slug", + "status", + "size_bytes", + "last_error", + "built_at", + "documents", + "document_count", + ) + + def validate_document_ids(self, value): + unique_ids = set(value) + if len(unique_ids) != len(value): + raise serializers.ValidationError( + _("Duplicate document identifiers are not allowed."), + ) + return value + + def create(self, validated_data): + document_ids = validated_data.pop("document_ids") + expiration_days = validated_data.pop("expiration_days", None) + validated_data["slug"] = get_random_string(50) + if expiration_days: + validated_data["expiration"] = timezone.now() + timedelta( + days=expiration_days, + ) + else: + validated_data["expiration"] = None + + share_link_bundle = super().create(validated_data) + + documents = list( + Document.objects.filter(pk__in=document_ids).only( + "pk", + ), + ) + documents_by_id = {doc.pk: doc for doc in documents} + missing = [ + str(doc_id) for doc_id in document_ids if doc_id not in documents_by_id + ] + if missing: + raise serializers.ValidationError( + { + "document_ids": _( + "Documents not found: %(ids)s", + ) + % {"ids": ", ".join(missing)}, + }, + ) + + ordered_documents = [documents_by_id[doc_id] for doc_id in document_ids] + share_link_bundle.documents.set(ordered_documents) + share_link_bundle.document_total = len(ordered_documents) + + return share_link_bundle + + def get_document_count(self, obj: ShareLinkBundle) -> int: + return getattr(obj, "document_total") or obj.documents.count() + + class BulkEditObjectsSerializer(SerializerWithPerms, SetPermissionsMixin): objects = serializers.ListField( required=True, @@ -2295,8 +2456,11 @@ class WorkflowTriggerSerializer(serializers.ModelSerializer): "filter_has_all_tags", "filter_has_not_tags", "filter_custom_field_query", + "filter_has_any_correspondents", "filter_has_not_correspondents", + "filter_has_any_document_types", "filter_has_not_document_types", + "filter_has_any_storage_paths", "filter_has_not_storage_paths", "filter_has_correspondent", "filter_has_document_type", @@ -2534,14 +2698,26 @@ class WorkflowSerializer(serializers.ModelSerializer): filter_has_tags = trigger.pop("filter_has_tags", None) filter_has_all_tags = trigger.pop("filter_has_all_tags", None) filter_has_not_tags = trigger.pop("filter_has_not_tags", None) + filter_has_any_correspondents = trigger.pop( + "filter_has_any_correspondents", + None, + ) filter_has_not_correspondents = trigger.pop( "filter_has_not_correspondents", None, ) + filter_has_any_document_types = trigger.pop( + "filter_has_any_document_types", + None, + ) filter_has_not_document_types = trigger.pop( "filter_has_not_document_types", None, ) + filter_has_any_storage_paths = trigger.pop( + "filter_has_any_storage_paths", + None, + ) filter_has_not_storage_paths = trigger.pop( "filter_has_not_storage_paths", None, @@ -2558,14 +2734,26 @@ class WorkflowSerializer(serializers.ModelSerializer): trigger_instance.filter_has_all_tags.set(filter_has_all_tags) if filter_has_not_tags is not None: trigger_instance.filter_has_not_tags.set(filter_has_not_tags) + if filter_has_any_correspondents is not None: + trigger_instance.filter_has_any_correspondents.set( + filter_has_any_correspondents, + ) if filter_has_not_correspondents is not None: trigger_instance.filter_has_not_correspondents.set( filter_has_not_correspondents, ) + if filter_has_any_document_types is not None: + trigger_instance.filter_has_any_document_types.set( + filter_has_any_document_types, + ) if filter_has_not_document_types is not None: trigger_instance.filter_has_not_document_types.set( filter_has_not_document_types, ) + if filter_has_any_storage_paths is not None: + trigger_instance.filter_has_any_storage_paths.set( + filter_has_any_storage_paths, + ) if filter_has_not_storage_paths is not None: trigger_instance.filter_has_not_storage_paths.set( filter_has_not_storage_paths, @@ -2573,7 +2761,8 @@ class WorkflowSerializer(serializers.ModelSerializer): set_triggers.append(trigger_instance) if actions is not None and actions is not serializers.empty: - for action in actions: + for index, action in enumerate(actions): + action["order"] = index assign_tags = action.pop("assign_tags", None) assign_view_users = action.pop("assign_view_users", None) assign_view_groups = action.pop("assign_view_groups", None) @@ -2700,6 +2889,16 @@ class WorkflowSerializer(serializers.ModelSerializer): return instance + def to_representation(self, instance): + data = super().to_representation(instance) + actions = instance.actions.order_by("order", "pk") + data["actions"] = WorkflowActionSerializer( + actions, + many=True, + context=self.context, + ).data + return data + class TrashSerializer(SerializerWithPerms): documents = serializers.ListField( diff --git a/src/documents/signals/handlers.py b/src/documents/signals/handlers.py index 5f2c8b4b2..cfd2f185b 100644 --- a/src/documents/signals/handlers.py +++ b/src/documents/signals/handlers.py @@ -26,6 +26,8 @@ from filelock import FileLock from documents import matching from documents.caching import clear_document_caches +from documents.caching import invalidate_llm_suggestions_cache +from documents.data_models import ConsumableDocument from documents.file_handling import create_source_path_directory from documents.file_handling import delete_empty_directories from documents.file_handling import generate_filename @@ -52,6 +54,7 @@ from documents.workflows.mutations import apply_assignment_to_overrides from documents.workflows.mutations import apply_removal_to_document from documents.workflows.mutations import apply_removal_to_overrides from documents.workflows.utils import get_workflows_for_trigger +from paperless.config import AIConfig if TYPE_CHECKING: from documents.classifier import DocumentClassifier @@ -418,7 +421,15 @@ def update_filename_and_move_files( return instance = instance.document - def validate_move(instance, old_path: Path, new_path: Path): + def validate_move(instance, old_path: Path, new_path: Path, root: Path): + if not new_path.is_relative_to(root): + msg = ( + f"Document {instance!s}: Refusing to move file outside root {root}: " + f"{new_path}." + ) + logger.warning(msg) + raise CannotMoveFilesException(msg) + if not old_path.is_file(): # Can't do anything if the old file does not exist anymore. msg = f"Document {instance!s}: File {old_path} doesn't exist." @@ -507,12 +518,22 @@ def update_filename_and_move_files( return if move_original: - validate_move(instance, old_source_path, instance.source_path) + validate_move( + instance, + old_source_path, + instance.source_path, + settings.ORIGINALS_DIR, + ) create_source_path_directory(instance.source_path) shutil.move(old_source_path, instance.source_path) if move_archive: - validate_move(instance, old_archive_path, instance.archive_path) + validate_move( + instance, + old_archive_path, + instance.archive_path, + settings.ARCHIVE_DIR, + ) create_source_path_directory(instance.archive_path) shutil.move(old_archive_path, instance.archive_path) @@ -638,6 +659,15 @@ def cleanup_custom_field_deletion(sender, instance: CustomField, **kwargs): ) +@receiver(models.signals.post_save, sender=Document) +def update_llm_suggestions_cache(sender, instance, **kwargs): + """ + Invalidate the LLM suggestions cache when a document is saved. + """ + # Invalidate the cache for the document + invalidate_llm_suggestions_cache(instance.pk) + + @receiver(models.signals.post_delete, sender=User) @receiver(models.signals.post_delete, sender=Group) def cleanup_user_deletion(sender, instance: User | Group, **kwargs): @@ -751,7 +781,7 @@ def run_workflows( if matching.document_matches_workflow(document, workflow, trigger_type): action: WorkflowAction - for action in workflow.actions.all(): + for action in workflow.actions.order_by("order", "pk"): message = f"Applying {action} from {workflow}" if not use_overrides: logger.info(message, extra={"group": logging_group}) @@ -944,3 +974,26 @@ def close_connection_pool_on_worker_init(**kwargs): for conn in connections.all(initialized_only=True): if conn.alias == "default" and hasattr(conn, "pool") and conn.pool: conn.close_pool() + + +def add_or_update_document_in_llm_index(sender, document, **kwargs): + """ + Add or update a document in the LLM index when it is created or updated. + """ + ai_config = AIConfig() + if ai_config.llm_index_enabled: + from documents.tasks import update_document_in_llm_index + + update_document_in_llm_index.delay(document) + + +@receiver(models.signals.post_delete, sender=Document) +def delete_document_from_llm_index(sender, instance: Document, **kwargs): + """ + Delete a document from the LLM index when it is deleted. + """ + ai_config = AIConfig() + if ai_config.llm_index_enabled: + from documents.tasks import remove_document_from_llm_index + + remove_document_from_llm_index.delay(instance) diff --git a/src/documents/tasks.py b/src/documents/tasks.py index 6c415ad69..fc8911705 100644 --- a/src/documents/tasks.py +++ b/src/documents/tasks.py @@ -3,8 +3,10 @@ import hashlib import logging import shutil import uuid +import zipfile from pathlib import Path from tempfile import TemporaryDirectory +from tempfile import mkstemp import tqdm from celery import Task @@ -22,6 +24,8 @@ from whoosh.writing import AsyncWriter from documents import index from documents import sanity_checker from documents.barcodes import BarcodePlugin +from documents.bulk_download import ArchiveOnlyStrategy +from documents.bulk_download import OriginalsOnlyStrategy from documents.caching import clear_document_caches from documents.classifier import DocumentClassifier from documents.classifier import load_classifier @@ -39,6 +43,8 @@ from documents.models import CustomFieldInstance from documents.models import Document from documents.models import DocumentType from documents.models import PaperlessTask +from documents.models import ShareLink +from documents.models import ShareLinkBundle from documents.models import StoragePath from documents.models import Tag from documents.models import WorkflowRun @@ -54,6 +60,10 @@ from documents.signals import document_updated from documents.signals.handlers import cleanup_document_deletion from documents.signals.handlers import run_workflows from documents.workflows.utils import get_workflows_for_trigger +from paperless.config import AIConfig +from paperless_ai.indexing import llm_index_add_or_update_document +from paperless_ai.indexing import llm_index_remove_document +from paperless_ai.indexing import update_llm_index if settings.AUDIT_LOG_ENABLED: from auditlog.models import LogEntry @@ -242,6 +252,13 @@ def bulk_update_documents(document_ids): for doc in documents: index.update_document(writer, doc) + ai_config = AIConfig() + if ai_config.llm_index_enabled: + update_llm_index( + progress_bar_disable=True, + rebuild=False, + ) + @shared_task def update_document_content_maybe_archive_file(document_id): @@ -341,6 +358,10 @@ def update_document_content_maybe_archive_file(document_id): with index.open_index_writer() as writer: index.update_document(writer, document) + ai_config = AIConfig() + if ai_config.llm_index_enabled: + llm_index_add_or_update_document(document) + clear_document_caches(document.pk) except Exception: @@ -558,3 +579,169 @@ def update_document_parent_tags(tag: Tag, new_parent: Tag) -> None: if affected: bulk_update_documents.delay(document_ids=list(affected)) + + +@shared_task +def llmindex_index( + *, + progress_bar_disable=True, + rebuild=False, + scheduled=True, + auto=False, +): + ai_config = AIConfig() + if ai_config.llm_index_enabled: + task = PaperlessTask.objects.create( + type=PaperlessTask.TaskType.SCHEDULED_TASK + if scheduled + else PaperlessTask.TaskType.AUTO + if auto + else PaperlessTask.TaskType.MANUAL_TASK, + task_id=uuid.uuid4(), + task_name=PaperlessTask.TaskName.LLMINDEX_UPDATE, + status=states.STARTED, + date_created=timezone.now(), + date_started=timezone.now(), + ) + from paperless_ai.indexing import update_llm_index + + try: + result = update_llm_index( + progress_bar_disable=progress_bar_disable, + rebuild=rebuild, + ) + task.status = states.SUCCESS + task.result = result + except Exception as e: + logger.error("LLM index error: " + str(e)) + task.status = states.FAILURE + task.result = str(e) + + task.date_done = timezone.now() + task.save(update_fields=["status", "result", "date_done"]) + else: + logger.info("LLM index is disabled, skipping update.") + + +@shared_task +def update_document_in_llm_index(document): + llm_index_add_or_update_document(document) + + +@shared_task +def remove_document_from_llm_index(document): + llm_index_remove_document(document) + + +@shared_task +def build_share_link_bundle(bundle_id: int): + try: + bundle = ( + ShareLinkBundle.objects.filter(pk=bundle_id) + .prefetch_related("documents") + .get() + ) + except ShareLinkBundle.DoesNotExist: + logger.warning("Share link bundle %s no longer exists.", bundle_id) + return + + bundle.remove_file() + bundle.status = ShareLinkBundle.Status.PROCESSING + bundle.last_error = None + bundle.size_bytes = None + bundle.built_at = None + bundle.file_path = "" + bundle.save( + update_fields=[ + "status", + "last_error", + "size_bytes", + "built_at", + "file_path", + ], + ) + + documents = list(bundle.documents.all().order_by("pk")) + + _, temp_zip_path_str = mkstemp(suffix=".zip", dir=settings.SCRATCH_DIR) + temp_zip_path = Path(temp_zip_path_str) + + try: + strategy_class = ( + ArchiveOnlyStrategy + if bundle.file_version == ShareLink.FileVersion.ARCHIVE + else OriginalsOnlyStrategy + ) + with zipfile.ZipFile(temp_zip_path, "w", zipfile.ZIP_DEFLATED) as zipf: + strategy = strategy_class(zipf) + for document in documents: + strategy.add_document(document) + + output_dir = settings.SHARE_LINK_BUNDLE_DIR + output_dir.mkdir(parents=True, exist_ok=True) + final_path = (output_dir / f"{bundle.slug}.zip").resolve() + if final_path.exists(): + final_path.unlink() + shutil.move(temp_zip_path, final_path) + + bundle.file_path = f"{bundle.slug}.zip" + bundle.size_bytes = final_path.stat().st_size + bundle.status = ShareLinkBundle.Status.READY + bundle.built_at = timezone.now() + bundle.last_error = None + bundle.save( + update_fields=[ + "file_path", + "size_bytes", + "status", + "built_at", + "last_error", + ], + ) + logger.info("Built share link bundle %s", bundle.pk) + except Exception as exc: + logger.exception( + "Failed to build share link bundle %s: %s", + bundle_id, + exc, + ) + bundle.status = ShareLinkBundle.Status.FAILED + bundle.last_error = { + "bundle_id": bundle_id, + "exception_type": exc.__class__.__name__, + "message": str(exc), + "timestamp": timezone.now().isoformat(), + } + bundle.save(update_fields=["status", "last_error"]) + try: + temp_zip_path.unlink() + except OSError: + pass + raise + finally: + try: + temp_zip_path.unlink(missing_ok=True) + except OSError: + pass + + +@shared_task +def cleanup_expired_share_link_bundles(): + now = timezone.now() + expired_qs = ShareLinkBundle.objects.filter( + expiration__isnull=False, + expiration__lt=now, + ) + count = 0 + for bundle in expired_qs.iterator(): + count += 1 + try: + bundle.delete() + except Exception as exc: + logger.warning( + "Failed to delete expired share link bundle %s: %s", + bundle.pk, + exc, + ) + if count: + logger.info("Deleted %s expired share link bundle(s)", count) diff --git a/src/documents/templating/filepath.py b/src/documents/templating/filepath.py index 7d76e7f31..3647948ea 100644 --- a/src/documents/templating/filepath.py +++ b/src/documents/templating/filepath.py @@ -108,7 +108,6 @@ def create_dummy_document(): page_count=5, created=timezone.now(), modified=timezone.now(), - storage_type=Document.STORAGE_TYPE_UNENCRYPTED, added=timezone.now(), filename="/dummy/filename.pdf", archive_filename="/dummy/archive_filename.pdf", @@ -262,6 +261,17 @@ def get_custom_fields_context( return field_data +def _is_safe_relative_path(value: str) -> bool: + if value == "": + return True + + path = PurePath(value) + if path.is_absolute() or path.drive: + return False + + return ".." not in path.parts + + def validate_filepath_template_and_render( template_string: str, document: Document | None = None, @@ -309,6 +319,12 @@ def validate_filepath_template_and_render( ) rendered_template = template.render(context) + if not _is_safe_relative_path(rendered_template): + logger.warning( + "Template rendered an unsafe path (absolute or containing traversal).", + ) + return None + # We're good! return rendered_template except UndefinedError: diff --git a/src/documents/templating/workflows.py b/src/documents/templating/workflows.py index 67f3ac930..66fd97e01 100644 --- a/src/documents/templating/workflows.py +++ b/src/documents/templating/workflows.py @@ -40,6 +40,7 @@ def parse_w_workflow_placeholders( created: date | None = None, doc_title: str | None = None, doc_url: str | None = None, + doc_id: int | None = None, ) -> str: """ Available title placeholders for Workflows depend on what has already been assigned, @@ -79,6 +80,8 @@ def parse_w_workflow_placeholders( formatting.update({"doc_title": doc_title}) if doc_url is not None: formatting.update({"doc_url": doc_url}) + if doc_id is not None: + formatting.update({"doc_id": str(doc_id)}) logger.debug(f"Parsing Workflow Jinja template: {text}") try: diff --git a/src/documents/tests/samples/barcodes/split-by-tag-basic.pdf b/src/documents/tests/samples/barcodes/split-by-tag-basic.pdf new file mode 100644 index 000000000..e7959fab9 --- /dev/null +++ b/src/documents/tests/samples/barcodes/split-by-tag-basic.pdf @@ -0,0 +1,191 @@ +%PDF-1.3 +% ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 4 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/Contents 15 0 R /MediaBox [ 0 0 612 792 ] /Parent 14 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +4 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +5 0 obj +<< +/BitsPerComponent 8 /ColorSpace /DeviceRGB /Filter [ /ASCII85Decode /FlateDecode ] /Height 290 /Length 3461 /Subtype /Image + /Type /XObject /Width 290 +>> +stream +Gb"0M0bW:r$j4o4s3aL9.o/:sRKC1+V[Po_hnP="8Wk>jOsEV^,Y=.E8Wk>jOsEV^,Y=.E8Wk>jOsEV^,Y=.E8Wk>jOsEV^,Y=.EM=P;M`ictLh5:'u?=R'nd;IE]5Hh=BmqB2p^7X$0Q$9UiFPkD[m)hEDD7]3!20S(%m5Eepo,>73Ncpo[qg"0,Gt5J@p\hbEY.UOcVYbgK@oqO7DN/KYR@egT:O\Y"S^Nmne(=m!@M;\.mreAj`lssm2RjQmR*'f[]=0V/jtsN_^"C8&k'PptV(jd(Ymp-?-DiQUlg??aR5p7DE%a+(Q2+a1De[G>Bl&EKZ&,I(pUY]E@qJJG)r-?G9P(rih-1dREuNfk?>O(#o=aSKd[6HOfEV(Z'2t=fFn_3AbT-'E'3sZfj1^M@pYhQ7E1%B!q_i'CLMJZ]APP)MgR*7.Y/pg53RP?TA*/3L-50YH7,u"@RJ5[/9Q6C5NVbVGhM5l%_.?@umb=+S+0N]gQTFE$MV_[_Y246E[o=\bnb0967Q$FISai'U8mksuCAo?M*bkl?R-I0h_YM$B?F8J^DhM5l%EG"?[c+]I2gNP.=5$X;.1Gdp(p8uQo^/LHoiL3GZR2\raAeSG3ICLU;>is%i\_.+PGos32"IH[hA8X4IM[5E1-IZRS7[g)c,U.'3s[J\0_kok/NUqf`[Xe+0N]gQauWsDDo=BhM5l%_.@LHR@?oiRJ5[/9Q6C=:Zc7&>ipIE-50YH`fmsd"IFD+`\u,TrPk$]NL;edD'YO[I2buE1hPl,[ZP+_p2)p[e!QQPfLD$lgUH]`:1Im2@iJ!ODVrHt3K9FeNGTr/\U>Dmjtp]41q&NWk4WXSRF@Oke(@-QRG54@A56WH:1G57Ao?MGP<"Vj3K7l$RCR_b:ZaKGk$5CC.+u(L[aFc^qb6b_]O]p>fgaTjmPE\no9+M@B,b.F]?bTVcV*tKS8EA]mlo3K5;1^!EOO9f^ACUurOc[u`n=fDB;AhhFPj09^)KAJ38&9VV?L8MpH&M<8.ldJV05RX^_no.Ld9qHZg`b02a-m$D"%2.\6nf;,`[G2:]5WQ\V2c@4Gh=&YtOF%n^mA_13^REE`2l0OaBG;Wq]1Y8G/?Zt8UPc;l3PKnX1F]VM=136/NqdnAb9ps/J2L9Q0S)iGGeB)@_DF)%_Cm',a;^\2o]*8-oZUsS%9V$PXmM>H\bU0m00m3&T\6I=`1RmI^`mi+Cibh&sc>8Yj)cJ,VM7Wri3jVEGD+pLJ-LMZAlc^]d[kW$rRCHJJs-pTpHZ/#)PI^.2/_/Gu9ldRQT$2WWCT5#pBp+rKo47:$?VC&L8X%rrR4!(5rE?5)8Xe^PcTIWmmak?b:!t:GHfiH*GJBI/CQ^$TfeZFd^AG<;?^!=gc(929pYE$LqO43ODYD;<\aOu!e^l'@EjKDMb^K5$WP0]nZLJ^%HY#u61\3[enc_V2NHb$M)gp)%RGYQ;01^D,]VFZHi02I1r6C:L6.0i7*Bj-$T6+]-GAcILP+EW]kd`YIUbagAF!G%Ro\=[]cb7.BSXK;E)u5)]kJfT0mL;AEbfoP2a;6*b2r;r'Dt$>2Aq&o4^*)[NnW'2fK24Nao/eo%"\I%"GP'Z0I+"FNhmnk&i,4+8D*45U9lOt5(Y:H%gNYJ4S)E#I0RZO*cDrSi.cAfFP.AeSDgqSi-Obr20;bpKqYoS`%'Rr(9URn[j=kSMi,2qrR42k/aZcu)c8-TRTajWn-2h:0V>:?H.K8QTXcol?4Z\QM\UQ.esGSE+3uQBQEeG#L%A3LQAu,[ID*eB:EYk%6VF=)'\eEfuWs=\dD1g.f8NjCE.oPBA92cbB:e#8i1-tFRQ=g8G;/Vi_h'@1H2o>ZY116t>@KpYJMpA7)Ji/leW#F/+)#V*VC?f+jW%d?qJl]slE4fpD#^99j27h!!U!Boq])FiC1L1hLXTfG0bKqW+XE:1[0q:;4>9@iJ\8WL&g=b[jOE:;4>9@iJ\8WL&g=b[jOE:;4>9@iJ\8WL&g=b[jOE:;4>9@iJ\8WL&g=b[jOE:;4>9@iJ\8WL&g=b[jOE:;4>9@iJ\8WL&g=b[jOE:;4>9@iJ\8WL&g=b[jOE:;4>9@iJ\8WL&g=b[jOE:;4>9@iJ\8WL&g=b[jOE:;4>9j"6dh3D"AU^/LHoRCY]P]2P,]<+kV\Q$K"$)s"^pPrVEYk.Xc^pR/TYm^lDcP>l2_4-b)`W>jp44-_ftFlpD:RJ3,\612?`R?LT_mQ6\ZT;`dj^,qT?8Tj10;jmBJ\j>br;jihKBC7jHH(V&TjM!^@3D"AU^/LHoRCY]P]2P,]<+kV\Q$K"$)s"`VbpKshhJ;heIJ#,$L#d]nmrG`@m^r4^I;<3g8o>f_?gbP]CkDQP]k60U=20o&8FDiA/iT9X^3d':\+\@Uj;*pUjhAp_-FiO$C\FlYoddS,jF4Z.EjH)?]D%bBCL@$4DBZPtm^q7jK)=uLB&DH`u,3p=6A-(6\RV^<=bJ\F89ip8rc9/%LApI_"ofZO-'3pR6MG?iVY"m@=Eq[a)Y.q"N1qoK.Z\e#:l3*)"BA[ObqR\dSisd?'T6oD_R;-aleNSse-CL'A2.`f0WDraO2OS)NhURji-Dsc/e(A2o3I+\)VOF#I[81:r8`o)>9poa:.b-_B9dZ9lG;Ws3af/8:1cCb4:>XNcW@"N@mF0]uOu[eh;l6"R9!qH)P=aot>tp`%E[oU'ND1afPBSlqWl_5>q`ONacB7HTe^^)FjdQ)cmKTR7qbD9Vk'+?_^P9A:.ET;&?(LdsY0!m+DK&4Rmo3A$I[=j@CUb=RP3b9\eX>=VRf")l#,`aD:3C^AGI]'8L:b8NahC\ZSbZQoafjZ@E([G)<**^]QYZ/-\/Us$loWbJRG[+pr#4u-V^2.7F`lhj\L&UoOsEV^,Y=.E8Wk>jOsEV^,Y=.E8Wk>jOsEV^,Y=.E8Wk>jOsEV^,Y=.E8Wk>j''C?8XL9P~>endstream +endobj +6 0 obj +<< +/Contents 16 0 R /MediaBox [ 0 0 612 792 ] /Parent 14 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /XObject << +/FormXob.1ec20b3a96e40a35266a0a8a0d634adb 5 0 R +>> +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +7 0 obj +<< +/Contents 17 0 R /MediaBox [ 0 0 612 792 ] /Parent 14 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +8 0 obj +<< +/BitsPerComponent 8 /ColorSpace /DeviceRGB /Filter [ /ASCII85Decode /FlateDecode ] /Height 290 /Length 3500 /Subtype /Image + /Type /XObject /Width 290 +>> +stream +Gb"0M0s\bV$q&G/J($uOju)%+%1&)hn*)-NT`"8nzzzzzzzzzzzz!!'grT65NupmP_`]63jRce!oT8TqIFGMi(@D>9Q18%WpBeutHu8YIA4O7SpKc+sL9F0lZs.b3omCWORUeq#Fn]1ff7pJ#G-kItht;A6pmP_`]63jRce!oT8TqIF@N3&*FmnO#.ck97`6:E%D03I(`QWUU&i9D1[aFc>'f5%G8^-ObfLFJ>\r;M/@@JB>r)?I1e@5,du+nSeX'Eoh!BoPLr@VHWJ@\f-`;Z:LY8Kmo_Ad?D#0[5)F,u]k>=.H$p;]q^?Xu6pgRT0&;GI9.]Q(k==7(su_^mKeN$]^*XT/b_iRI^9\/Rk'_VO.X[X!?($+R'u%ohpmP1W1+Tpkqp$[=RJ65/WUOJ"FCk0:>qq.oTiPM!n,14O/tI1k<0>3<$5]2)lT?d&ATH1smHj(k't2X`iUD'W$A9g"p/H/Qm_8!9o-U&'oK;S+h0mmRk"Rt-k]u$5])/Y.baWi8dIY"AkBe/\3oGMcAUk_L);rMA#.X2i!H.gHJ/`tUi5T+.\FGmdDZ"(U:O*o%bOL")-=7^7DrE74n%8HFqc1)_qsI.l2X9/9=XrGe)G9:D2aqfB^S]TJQh-2j3jnnI0b'oU-pqAhRYDp-&E0eZ@h0kOd.U2CjG:$Z9F`64iQ1)?^./R#Qi;;q9^,G95_HAAGGP@N9hjJ)bTkmPnBP&2>q6mNRbVk[p.ML'C@j^(Kp6jTgZ9`&rR;L1/gVQ-1gJBf,9Jj)8R=&5kB4`+*#*k$W[Pk<$n'e`_=)ulnbsWAV8,n1Y\;=[tT6B[\7M6R:p1O1\nJ`cce;3%4W%9Cu];YgRj<\hRi@\^S#q;\'`3BG@'2DFDp_.g3E)3$iGVE:#8>Yn(i8??dQL.gM#W\4"p(2\i4mRD7k)U"b&c3-?#Z=p[5]00Bh9RD7&iiSJV&)h4)':2Vu(;!l(CTPIJrZHZrfS**lhrDLp#UDVjQe.sCQCb]ds]kIE*doS^q;DVjQe.sCQCb]ds]kIE*doS^q;DVjQe.sCQCb]ds]kIE*doS^q;DVjQe.sCQCb]ds]kIE*doS^q;DVjQe.sCQCb]dt()PtR=ZX#Ne^?[k]n7>Yqk"X@5,N$b[n+tfaEOuZ]=tTY?Y5"1hF(X2o%i[0Y4&I/QW`::2c81eHoLr:lT;0:AQJTg:"6Qqhp&n(qT^RT40.j^1..d;(9\b4QZdNohWf>5rbj0%"E=9M)9$`?o2DU%CYHQ'd/bh(O4X[8`a;i@8^*XN&i6/4oS>^0IF"$YVRS;Lg0=0)JU8j3sU!2h<13!]9bY$3.m58[g;k8V]Y5^+\)>H2oUMjp,BG:)qO1+5JhOIYF/#[obb<8HCGKl;^1-_DuEi6Cq2J1g9.'X4-oCLWfGBu>fA*2$m'&-5<5G.=`Vmk,5B&9%+Ymi#No@Ya?H95tRk08_qgJBXiTC$Zs\'m6IhOI,Nj6S(_kfW7]G@i>d]6GuK^GF1_VGb-dpCd3^o5%kcjh#ajEPFrts0m[M:a=eTY?+/Q$@*@YXq:#sL!:q!ThdT+nZPdC66nmtiM>M)I1WbY,IfmOP01+SS@m%\[Q[3Of"^576*(!7h)CHLZU3?^5"\m^4&XAlS&gq!Tkn-ZV5pa>F_*aPG"-*$7)!s@;;.u'G3*prREq=mOkD[UDr,o,2X7_Vq-@@iZY!i\p.aV;G9mdq<$L'BqoS#Q/D7G5&5=2B&?"jH1t1iW7uLWGC>n*R[oSo2j&%8I1k:217u7s3aHUt;K^6R.'Y9Ko@YXqe('1+7"Oc'p,d.jaf?/$.4ML+cQY]SR95;DOlX_E(t>pel7ZRjbNl-1fe?XOG^S03-W:M%[Eu17u7s3aHUt;K^6R.'Y9Ko@YXqe('1+apUs(p,X02DEh7SfseP+,u1V;r+DqE82-sb)nbWE/3Y5NP"]T:Lk9Zk"(]B\*gf?F9qzzzzzzzzzzzz5k,pqe-_`~>endstream +endobj +9 0 obj +<< +/Contents 18 0 R /MediaBox [ 0 0 612 792 ] /Parent 14 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /XObject << +/FormXob.1280b7d13f0587f75dbba24117c3e12a 8 0 R +>> +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +10 0 obj +<< +/Contents 19 0 R /MediaBox [ 0 0 612 792 ] /Parent 14 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +11 0 obj +<< +/Contents 20 0 R /MediaBox [ 0 0 612 792 ] /Parent 14 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +12 0 obj +<< +/PageMode /UseNone /Pages 14 0 R /Type /Catalog +>> +endobj +13 0 obj +<< +/Author (anonymous) /CreationDate (D:20251216142815+00'00') /Creator (anonymous) /Keywords () /ModDate (D:20251216142815+00'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (unspecified) /Title (untitled) /Trapped /False +>> +endobj +14 0 obj +<< +/Count 6 /Kids [ 3 0 R 6 0 R 7 0 R 9 0 R 10 0 R 11 0 R ] /Type /Pages +>> +endobj +15 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 180 +>> +stream +GarW2_$YcZ&4#]5`B.*sm?lG7-r1F@qZF9DA`.IipA]su.E!o]Gjlb!W-Y8MVLYd\SdH+*Db)WVcj08*lF:SRr1h[EQATgu\mTX&mM6\8TBJjRrY+[@;2SB>n%endstream +endobj +16 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 247 +>> +stream +GarW49oHkR%#46B/)I%IW6)D&Ib[t)(Vt`^F7>Vna$l[3>H5Gfa')=U#ta'/k95LEWOSG3"r7^/'n\C!!F0.=g]gEL*]\ARI\tm.$t5=QeJN,a-U84$:2lt0pl4os1D+HLYKtgFoDtT3?g;TJZ"p1Ms8-mnFZu9hhtp+9>=dda($9HW?h<;`raPb)D!6nUI04P3H\NHB@T2`*R;q_bb$8O:'I:6:edBMuW3U[lh[8WZ3d+9:~>endstream +endobj +17 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 174 +>> +stream +GarW05mkIo$q9nR`F#VQ:"lUo>6R=/74qEVW"iqLlKJ$aC5XZ78-0=Y@o+._YHbl4Yh1ZiKj;Fh4GOo\Q)*Tmo(5G_$S2VM\7.0Qendstream +endobj +18 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 244 +>> +stream +GarW4b6l*?&4Q?hMRuh(23Tpmkt^cRET&,M[qV:g6EQRMs2I6!U^T$](X?G+!r#.NZmDs+Qu<6UTF4R17*n#s2&gSmk2Mk.09@0r[k9DgRu9WjKt]k!Ic1n'J.q+%HiE61]07.7.f1^O]tp[&Fn>&8hUMD]+;spqhR>>LX`Bap0=GTNaa\,&7Zqt_h]FP:T4bD(VQ.g\Pm&EGSuJ6sK$-Os9'08g6q!hekXZP?.6B5f2_iendstream +endobj +19 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 177 +>> +stream +GarW05mkI_&4Q=V`F#VQSZ/`o[HbngaBTj`WC0EX,i7E\>AQT[REn!?T0[2$]@m:4_endstream +endobj +20 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 167 +>> +stream +GarWpYmu@>'Lq&PV`:G4BJ1Rt7[P0S\M$3?0c4(6nGPe/#`385LHkTp@/;7gD`sVT>qS[,<_0MjcjQ72dr'n'riIp[%YeJCl<.DN]-CUV%s0VJJ"dSm@n9+>F4SmfNcSuChM%!&%Rn8_]8SB3ren*ZdT2U-SH5HD>?EhY~>endstream +endobj +xref +0 21 +0000000000 65535 f +0000000061 00000 n +0000000102 00000 n +0000000209 00000 n +0000000404 00000 n +0000000516 00000 n +0000004168 00000 n +0000004426 00000 n +0000004621 00000 n +0000008312 00000 n +0000008570 00000 n +0000008766 00000 n +0000008962 00000 n +0000009032 00000 n +0000009294 00000 n +0000009386 00000 n +0000009657 00000 n +0000009995 00000 n +0000010260 00000 n +0000010595 00000 n +0000010863 00000 n +trailer +<< +/ID +[<93a746516153ebd4bfbd42147dac7019><93a746516153ebd4bfbd42147dac7019>] +% ReportLab generated PDF document -- digest (opensource) + +/Info 13 0 R +/Root 12 0 R +/Size 21 +>> +startxref +11121 +%%EOF diff --git a/src/documents/tests/samples/barcodes/split-by-tag-mixed-asn.pdf b/src/documents/tests/samples/barcodes/split-by-tag-mixed-asn.pdf new file mode 100644 index 000000000..8bdca9ffc --- /dev/null +++ b/src/documents/tests/samples/barcodes/split-by-tag-mixed-asn.pdf @@ -0,0 +1,251 @@ +%PDF-1.3 +% ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/BitsPerComponent 8 /ColorSpace /DeviceRGB /Filter [ /ASCII85Decode /FlateDecode ] /Height 290 /Length 3565 /Subtype /Image + /Type /XObject /Width 290 +>> +stream +Gb"0M0s\bV$q&G/J(%!d.nqi3%"dr>?4mOts8EB+zzzzzzzzzzzz!*oM+-e25^Cnl29l&b?1]i8(4Z]3i/UdM`^pJP:nc8L!XP7Hikhb/*W3nrlS:-,3JIP95Ol>35>46_jpqn5s1WO%T@mA*+\n+T^cV9UuF\!%DS+Prd[`h+g#,qgL3K&Nmea[s=/iNchX.++@iJ&&?TXb*R+\CBNRSd!GW)BQZVP'MW@t^MCeA2LT>ice?YncUPfCp2NGYK*fZ(6HNL>25gIQatNFu0.1]'#;OnrN+c_`O2p1\=fou/h\9khiWj?]th(k*4B.GJ2o'A_tJZUr&HY,rP-sml,W^p[L,?n7>?dc$S(>%:A7GHqc)2gh%fnMR<8APPhAHe8-(?flca9e(OY;c<5DomXfFGopCWMT3kt`d*!#p[L,?B[!PkSCa*Q+((4h`ls=[H$u+TnMR:ZbcGKJf9^b)?^$Aeb_jDcI@9j"25t95XD"-UQ['Z[PD[B(MT0piRJ65/WG'MWBUm\@GopCWMT3kt`d*!#p[L,?B[!PkSCa*QT>id:gK2Uh2l/78:Lb>9k4;`pb^!,*1]'#;Tk$?Z9k4;`pb^!,*1]+i^b,/"tD*6S:4RYJu^>DlOD*6S:4RYJu^>DlOD*6S:4RYJu^>DlOD*6S:4RYJu^>DlOD*6S:4RYJu^>DlOD*6S:4RYJu^>DlOD*6S:4RYJu^>DlOD*6S:4RYJu^>DlOD*6S:4RYJu^>DlOD*6S:4>5EnIDCiW$uk5["5N[p!&sm4^l?);E8rSRh"]%m2uYSJNP>nZ4+DCq2sL42I@5NP.]F?D#Lof[P5%gF/jYQ2SRj89<,>J(1q`4::6/2`]"iN8Tg?Q_T]1.WV03Cpgta&WQnCGR+^"MeNQSEhe])7eQHVp1V5]Gg.,l;RuOeN3_O+'E+BLNdNcEVXe;i3RP$%mDsKm@k(jJR1oOfe;eo82l?.WSiP3%8aXTCF(6\,k*Zn]Rad@F2`IW)AY(morkEmYot@DJjn#L]gNe59mc3'j^2HC'b3)cC\+BIQh8d"^IG]e-Q-ZMfDWg]+]8pmFqn5G9>^kpO*uQ=$Eg6]:IRJmQSA`me\o,Y'aRKFuunrh0j\Jh=kb7?2;.um<2gn]DMBs]C4KN](1-sg[2YfGPCXpGMfm'FQN+o\(c1Vn*]5jn%N^-l,r0hDn#?7hJLcP6!rcB2`JI/^Lm3;l:\Y34S.^TGOa;/SmQmP\!^mch"WN$Rb@;50B"h\baTR/`EtEJIe>#BAtF9hj1G)%B$[ZpK\O^,Df0q=J'I?UeX(6EH03A2n)CO=4hph+DDTVO\a`u'27)II?c$[BR5[(=Mn`ltr9?qbbrkU[`cn]Bb\tDn[ATgh_uqm2NGlQ/0;Y,U">dfPDnc-&V4,=*1V\of^AKX\R`#=b^Q!0Gr%Fn8_uqm2NGlQ/0;Y,U">dfPDnc-&V4,=*1V\of^AKX\R`#=b^Q!0Gr%Fn8_uqm2NGlQ/0;Y,-.D:`8P^-25mE:I'Hs9-=k"U!+0/NUuD';?92q?HmbkTAjKbS+T<*/rPj"K:*;NuH*?1#go^O06V;k2Y&`)n7A2:Z_HtqV9)3@-5F8=1N-;N^8j*DaOd6)Q[.`ANgrQrLKeIMI%HX\.QT920,N"Lq^240*VXc-(AOhjpaO%4/@LXQ!ME0B[2"GG`AEo+f[B6sbm`mc9m#]q`O#$/_S\InlY.6XO1]m^XzzzzzzzzzzzzDZ'feLL1)~>endstream +endobj +5 0 obj +<< +/Contents 19 0 R /MediaBox [ 0 0 612 792 ] /Parent 18 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /XObject << +/FormXob.969a0a278dab8164403e924542d002c7 4 0 R +>> +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/Contents 20 0 R /MediaBox [ 0 0 612 792 ] /Parent 18 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +7 0 obj +<< +/BitsPerComponent 8 /ColorSpace /DeviceRGB /Filter [ /ASCII85Decode /FlateDecode ] /Height 290 /Length 3730 /Subtype /Image + /Type /XObject /Width 290 +>> +stream +Gb"0M_/\Be$q&G0^ZfPD]&P!ICnq2'o`LkpkCLkpkCLkpkCLkpkCLkpkCLkpkCLkpkCLkpkCLkpkCLkpkCLkpkCLkpkCLkpkCLkpkCLkpkCLkpkC#aKo*\@Q^OSZ$ER1V!=bkN^34G+uR=bi%p$SuXBnfno!;15!4-HEODUB.'_6aUIUOlaaTISXl]s-Sr:0Y35)mF`sLjS]p$[]:Q&Rf@c#GFu"]^ms;.>>\>FagW9ll^?`>@Eh/c"DC76Mql],Ib&qm%\t.&!Z&R?j^GbOE'AIa`o5!sc1U2qk1;>a94ql[q/bGV/N&oBnE>NRor#2X`gCgNY=AC\-sMZ&R?j^GbOE'AIa`o5!scUY@ukb,dZtkV?'BplZgKBG'"Z:>Y'KJ^^S/i`O02&@QeB>8mp5B@FIDmN2+d!e]=@":lhuPjo00`4$!h-Zf=Z=@hp>rk8(T#[bO"5WpTF60]$1'8RO!!C?!R!XomT8pGop=UKJ'ThCYl\[Ds/tFUMH%>ATi(G'"Z:MgZY'JQ$FHWPCtQ0cSg*HEb4)tf-I'5gpU[B.@!FI<80r]$*=lkaBSYGop=UKJ%>l2L;6@A_q'/)nO7;hq=!\oBK#nX>@*jfCqu2S6'?4At=cq\@uUMkI`hh1[@_N_t?/+4m1@`Qs"'2)m8XtkW,il`6:7UgMhSe7*g.$->(hsfS8^=-@3t<1GQ5]`&:lX1XU8MSV`bW-FVn0Sr''1l:fF'1U.D@pIt4L=(buSS\"9g$Sq1hZP/nmh8e>q4YlKEZP/nmh8e>q4YlKEZP/nmh8e>q4YlKEZP/nmh8e>q4YlKEZP/nmh8e>q4YlKEZP/nmh8e>q4YlKEZP/nmh8e>q4YlKEZP/nmh8e>q4YlKEZP/nmh8e>q4YlKEZP/nmNTnOXoArC<7fK\![JXSJCAH$seFJqpW9#[j:s32]TpEGE67m[iKNeA\pj;0WLN'q"ON]k7-=$$-j4_68?%QVTfe_T5G9>H1FmfqgF3gEaf=Upi[]b%E+IRTM9Xrs8a0g?O]*-72ZY./JmQRf@leWpYk+4^LY'9V[D.!#i5r/3$R;fjOO)+X(G6sGCB?u7sh0iP_f=ah=bWU;">jq+@g:lu\Jn4<&1Z[2I)[Eg"5iOQZRkmX#m@t_j,M>I6HJ98N4`&]!Jt+>ab?>fCRgbFG%6,40*/7Q$K!M4l:ZZ2orVBZar&"RJ5CdoA,aTK=n6HX)#V[:@slSb$oB%P_-c4fU*lDeT+\#EUk]?^+lSnK3I=&o./:$&t>Fge4>7/OL*WnQ'A.bDqJZTXj,g;h`De\5p"\ScS]mHHa\5-D&gp2ukIFgLCM:ZmT.EHP9hYM3$ZRpBZR3`!,DX/i7Kh555KloNnMH(o^X)nIACmca(3]&?8XH^EO#&*`Ae%A$K1Y#MuVaj'^EY$Tl@3>LLFV0eQa]FX8l[1/^+&[#*EGKJ,,OKMpEi&MP28u6m$gq40^lEq(m;\?03\p)9@qiIsegYb(,iC]qnQe]4al0>YcSl66(<4[:qc23U*?JFBu(#_c]H^EO#&*`Ae%A$K1Y#MuVaj'_TgceTc9^P6Z<$X7[40]52>oU?qkq_QOd\S.Uak;Mf\EMYnm7+^7iM(*T[j,0jh*ehR,VqGBYmhLT)nI'S1@$:8V';>LfnjRahf+!d4Yr:$&RTWeXR:GdAL=B#^?E:5h=n"o9N"<,d=&:2moc!FUl+rAqdO^*ZS"%*(%F`e`Tk0M?"T6Lif!5_eU>cTbTp3,(Yon.qjqqu8ZIS"\h@+Y4mOke<"WJZ?``[Ben$2-iqLN\dhA/$Fk1]AleV0Wqc%F.%l1?SXQsKM]B>[XWS0lbA7!kjf?LIBcSt;k[?3Yo]WSTDRpGKSDr.C,,CPZ!?"[G%]A>^.]MI'IqB5C@cFRn:]mIJ=T;mnC60<<\FkuD1pO)Qr8CoXP0f)Bi\2(\YSOh?g0ekIU7CY+bk+,&p1GLU?8O^-E]B5YTf/#-91J($F1:u\>8CoXP0f)Bi\2(\YSOh?g0ekIU7CY+bk+,&p1GLU?8O^-E]B5YTf/',Dm$``@G,6D((A8nW\TUBd\gAVh?)\+8KmcKnXrWY2Y),\]="8f<@3829p8`OAQe?M&cZ_r1h7Gh42TU>in*2[?DXR#HV?"\:bWHu##e&8C[B%L0+=/qj/oJEg'P1atYa-EZ;)a-p^MI^n$eT%GNfg\aN]-OFjh.acpEIVr$H!NAt6b;R)2(s6aDP*3c\o-A[FOcRj?Li!i4tGf=h7O9_W[HE2jP@Cq6N>k=,&lh4IAYNt>!867sbjsBKImB^h>f?O2SY'P-59AZf*BB?e/S+e(3p#MR.]N_kkb'@8O7moa56psF26psF26psF26psF26psF26psF26psF26psF26psF26psF26psF26psF26psF26psF26psF26psEGnK3us@Di~>endstream +endobj +8 0 obj +<< +/Contents 21 0 R /MediaBox [ 0 0 612 792 ] /Parent 18 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /XObject << +/FormXob.f6e745526a61cd8416256ca62679b160 7 0 R +>> +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +9 0 obj +<< +/Contents 22 0 R /MediaBox [ 0 0 612 792 ] /Parent 18 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +10 0 obj +<< +/BitsPerComponent 8 /ColorSpace /DeviceRGB /Filter [ /ASCII85Decode /FlateDecode ] /Height 290 /Length 3461 /Subtype /Image + /Type /XObject /Width 290 +>> +stream +Gb"0M5n_s,&44e"s3f#4`]d?gQe*Pf:j1Mf#9(fYt0GMj\u/gIQatMe>s(1]&`3OnrN'c_`7*p/u2Vou/PT9j,^,Ze.b4Md8H7ZC'F.^O'MkF4Ohqqb?,=QF6qiQIaSReL+VDtl*Fa4I-(QT6?Ln%7H6n7B=pY=XF$F%U3(GJ1cMbBtC=P6t[ZPgmA7H@Ni!q*)M>'u%m@WHmCore<)tI=+a=I$mo2o=jPh%:A8ppmP1W18gr1IFn?8^(1]&`3Tk&U9X6?'4AMLO;@N.r%kHg&kGuGJ7q^f1LA:?_CkaXSC0sf28)bT8UZ\m%R;\h1d/@M6_IQjgrS2Y"6Zd3)"9'5(1]*^>b$$4b/tQA@4R[^HI@mF]]6JMHo=XfhpO/Vgn)E"hch$!@h=l3;]SgD=4jSJIFPl71o.HV?oK;SjdX@acbe&+CdIY$F8b[Y)1@O5U7F1S^-f+NBb.NXF(Hp)kT6/uR.J#0`?1^8JIFm@j"CMipr`Xrr\D9K^C5)A0*C#)b5aL#1AlXJ?_-a^qS$TLB&C3mWl91IT'qls;.jLu]1T*BGEncgPM/lHB&C3mWl91IT'qls;.jLu]1T*BGEncgPM/lHB&C3mWl91IT'qls;.jLu]1T*BGEncgPM/lHB&C3mWl91IT'qls;.jLu]1T*BGEncgPM/lHB&C3mWl91IT'qls;.jLu]1T*BGEncgPM2/fXD_E6B349YHfs51pA)9lSb:7.kj7TiI.+`&ffibEWO'jCk#HMHF`+p;hX,-OYH-/Uhn4ilY0P;dcF]Y7o6$uT3N5u9n)>&=(6k;:?VW*$ik%1h[ftl*hj9O![J2@LnSLS&g%XU(;D#V@;g_eGDYAK=WDnfEiY:8cPad).:+FfR]/[d="m'@RQ]k%Lpb#>3D*b_cpZ82K/\[-AHs9Ebp3es#th\K+SGuJ#)QPmPc>=_WR9m(ScB)M4:+[qeXe^])9n]SCq'P6-#=8-:G0AdOCP^-25h3T"oHs9-=k"Tu@?Y96,D';?92q?H,RISJ0KbS+T<*/g*`\63b;NuH*?/:hb^D<=2Aa]K]I&U"V^Lj`$?fpO@;dK2sV9XP7@TU:fcSqc5ntesLrL1^.I$pUF:3GYkPree"Md?-65Mr!rb*OT#p1kGUnn!9a3RsBJEr<2m32Ze1NroCL5+Y'd3HB>8&`.pqH%Ro$%9`G4KKHiQLSWk\FIH^>$c=6%ptuDkaFXebO)1hZah0beAqJelCnFSU;AUUb_hm.Q;i/o6l!*oPJ%,XE*pa1>g-]6/NFncRLl'/b=C<)t[uUnZ6d&f.u7([mAme(=+HhIkC8?HlM]p@I^F>o@GbTa1"@l()G4G2h*FNI=*"+D.2t9Eq@mg-]6/NHBZ\m7#^73Gke+.B(hZah0b_jn<^73Gke+.B(hZah0b_jn<^73Gke+.B(hZah0b_jn<^73Gke+.B(hZah0b_jn<^73Gke+.B(hZah0b_jn<^73Gke+.B(hZah0b_jn<^73Gke+.B(hZah0b_jn<^73Gke+.B(hZah0b_jn<^73Gke+.B(hZah0b_jn<^73Gke+.CS`ATcl]'#RI\URqX`ATcds*QUcpWr03fLG&tT5UUircro-gZ[Glo?Hp_5HiCB)kG`5^6tSikOhtoUN?=Y2q)jJ]KV6jFDh-SApWm;f4V@JcL!jR9[E\/qCK)&\c(]\l+):MT>`]L\,Uh34nm:gUO4#O;Y0and()GRhQKN)cYtuRad@F;Y0and()GRhQKNazzzzzzzzzzzz!'^D_bZ%t[~>endstream +endobj +11 0 obj +<< +/Contents 23 0 R /MediaBox [ 0 0 612 792 ] /Parent 18 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /XObject << +/FormXob.e28945bebb594f11c83bbac90aba8c31 10 0 R +>> +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +12 0 obj +<< +/Contents 24 0 R /MediaBox [ 0 0 612 792 ] /Parent 18 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +13 0 obj +<< +/BitsPerComponent 8 /ColorSpace /DeviceRGB /Filter [ /ASCII85Decode /FlateDecode ] /Height 290 /Length 3621 /Subtype /Image + /Type /XObject /Width 290 +>> +stream +Gb"0M0s^+9$q&Fqs/,gl02gI,?@ebho'Q]Jl/07UWiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiDfkGE/^Sf@`Q543?FKH_.8gf@`Q543?FKH_.8gf@`Q543?FKH_.8gf@`Q543?FKH_.8gf@`Q543?FKH_.8gf@`Q543?FKH_.8gf@`Q543?FKH_.8gf@`Q543?FKH_.8gf@`Q543?FKH_.8gf@`Q543ipCC-5Bct(Y?=Wp&5CQo0$?B$8/ZCCYnN+rs"hc*j/'8mruHpK?JWQf;+#Am,FK.]AC<=A\Nq8;ScpfER"Adn)*%Yhnq=Ni%hpSk5DIWDfD-5iL3_bR9a(hIb4HU\BO\d9e*Zs0!9K7lhqc-k'OTb'oA=nM=YbUn)9^7,\C(&cHBe5LMKeMQs#P20:2JUY";&kAjt^l>ipCC-5DB*'oDO&RIB+'<$Z'V,gOSE^5$K%OdK:63%pWo*j/('@V/'g]NjTG\0_rIR2(SMEgcCTRT(PE<[>u6A\KOs%;u'#Sb'E::1>/7k3'(.Sb'E;RdE]tk]1T*AGEnbLc."ZGm"eT\9[DDGII>0Njr'D=Vku4qWj\h^:>I@'mJ5_S%&BCB:Ei-LeG^XFlDJb^Y[Z+RlfV-EG&ReH0_YX[e]'G\5_6d(N/++l^_n,\K`^AGLhJ;\,L\dRr@_N9?!HEiT@8WhV%%\45s.XB3i#mlZ;O]qHH$/Ei%K8ZdQjs;0Kn23KYiBcC:(WIZ:WXcH_8OWHR."[..pr*H?6tj@ZiOAu1JoU[3fCF!=\cSMr'9r#0-:S=L>(;XM,"C#den46#UsaH^N'bt8qi7bJAdklE\XNG6UN8LphGmJ@4M14IHaoZ4ZeUS*GEl,hcgJIA*j0/;/@Qkc>p\%HZTE*>L[/)'bO%iG=%Tgkm*;*hiHePME][1UY'\l*6G@jY0C3"'YZ]r[+)[^I,`B1E(E&BY`bumYNV%!SeOlDqb;n/9dg?'q4ZdOS[Rn3(plmZ16:2P'Basg/)`%.GgIUKr`=pD9c$7.r?]EM%oF&T#0%=e2CqgR?]ZEgdI$DAl%B#Eb_)MPfu`=$>q',40t93l`5*_bh8Da2V@gYE&Vi]VQ()KrX7N+S=QWoK:W#DG;ElEWnLkD^VJ[LfpSiuf_I1>2fj/>WXX.N$f'!sSZ-@VSc8b81M:@#g0j*Y^tm5fRAr0tq(H\GF*=G78_NQ-gq8J&/+4sePah.#2X&$&Rs;:1Y*)Dfq&XF+0&4*r2_4>HWK`n@iATo:c'rsanue-J\bR:aK.brlM)QsR.]JZ%n)61g_TeQpZgt8^9"Fa=DM'Rs]SSQtmjb<$S+#;okebT6GdHtDP"N:\*c?T_gq8J&/+4sePah.#2X&$&Rs;:1Y*)Dfq4>O->a^k$GP6E&U7Dq/Eb2uZ:#;@QVo5`CRI>QfpEnTCYq&o9SJ&PTA\Rn5hOA2V`3*@5msUDo-JBoR.p(k[hrMJ-G"V,DQ208qH#KFJ'"`0q^@t@YRqO1PB*FWiRqV!0gtDND[j)'CR@@mQc.q"K*4I-rQ^O=VkKVf8qAcsh"dbXHB]mXqg%l\t,6aF%RqV!0gtDND[j)'CR@@mQc.q"K*4I-rQ^O=VkKVf8qAcsh"dbXHB]mXqg%l\t,6aF%RqV!0gtDND[j)'CR@@mQc.q"K*4I/H/+[u@c*aft?=b>Bh0TdDi&]&hZ#a5_,3@X-'"?d[bI+Tr?=b>Bh0TdDi&]&hZ#a5_,3@X-'"?d[bI+Tr?=b>Bh0TdDi&]&hZ#a5_,3@X-'"?d[bI+Tr?=b>Bh0TdDi&]&hZ#a5_,3@X-'"?d[bI+Tr?=b>Bh0TdDi&]&hZ#a5_,3@X-'"=O(9l,HWFN$+_O4)&'[O9tGf*4b0MJMc(V2`$&:VM1Z%?AjOfAo(e)fsc*I.pqd*2P0gaa971k-dGKm_(M<8lo8]pF'n&D0tj0HYm_dY.,hI;;.Dlp>ij[g>FAbED>;bZn]&Wa`m3]_,7f'R_2.)@rC%bn(qi4UNFI4H&pCngpSTf^"3HYl&'9IT#mbEg^]8if:NeP&#\sQciW4&pM9BP?]'^]3OKeP.2b)S)i;]hW6NEPf+S^h^$eXpF_fCVBkScq^/j3\9GL-2'm64gc*?__7e$'Xso2hikA!e%IRLSF%o6]&bf"?D/eraR;5FSNjs"jCP-u#b?Ru1mI-jpS(JIBR;;BF-sP=qR5_B$/i2,?Y>Ma4P;7&c='T@?a^:fZ@4,XC`3-Vo>a)olftpM,]STp6RCOYoNO"8/P^*qSi\GRLrYf>U4"9bJG/\%TRf#%c(3WU<:$db\bVf33PV)6tba[6"Q^MX[f-jkU8XVNB.kWX0A5uW0endstream +endobj +14 0 obj +<< +/Contents 25 0 R /MediaBox [ 0 0 612 792 ] /Parent 18 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /XObject << +/FormXob.3d82fbc7fb155441d517c93ed9ec525d 13 0 R +>> +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +15 0 obj +<< +/Contents 26 0 R /MediaBox [ 0 0 612 792 ] /Parent 18 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +16 0 obj +<< +/PageMode /UseNone /Pages 18 0 R /Type /Catalog +>> +endobj +17 0 obj +<< +/Author (anonymous) /CreationDate (D:20251216142815+00'00') /Creator (anonymous) /Keywords () /ModDate (D:20251216142815+00'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (unspecified) /Title (untitled) /Trapped /False +>> +endobj +18 0 obj +<< +/Count 8 /Kids [ 5 0 R 6 0 R 8 0 R 9 0 R 11 0 R 12 0 R 14 0 R 15 0 R ] /Type /Pages +>> +endobj +19 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 261 +>> +stream +GarW495=S`'SZ;W'k^[])"5]poB8qH=%/@qU>)&lKpEPq`^ToUj7jAr2hAmFc3IcS3]u1M!)nO?K#&(3$W`[Iq8D'E927i,D^h:d1ndTQ!o?uOgOi!M5;;R#n;G^&c9s)+r7(.`dIamA_=.6`V?\;0&JO?e+g!YX&8_+8XHlF=W/Ia'Xk$Qrk@3aA0PQh(F/A?*p4>/mR.MH!>TZ1)N#/`,?ZD:[7;^/hZ>/d(i^,*:I4^^bZ6b[:`%6eRJG9?mY#W4~>endstream +endobj +20 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 170 +>> +stream +GarWp5mr90&-_"(^Z$9?6d9G5GL7PscscphYcOP./#`K?ei9A^4+L=D=F(B57X3>:A[Gj.'WC58euaPELhe*k>#kjh012B&[7,F!'.Fi#)NkQ>n`2Yd~>endstream +endobj +21 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 242 +>> +stream +GarW44U]+\&;KrWMK`$TRZ#Ejh%W'h#K7+SBoIXGUUC)N@r5(2Nk9o[Sql>31)AEh'u8=l^EAVi3IoO'Kiqi%7%;@\=.b]qJX4AdRf].g4aIOJ0IN7E2uH2"iZoh[jaDCb(OO.Q\4>L2't0AZX'_QXW`mKcn@qdY_Po+mf_[DIA3%@^p+P5rOHsf;mM@n1YBPg%,*^&!jN2t$Q>3PS1:d?;-!$J*A]<=N-^O:%Yn7OC#!>ZE~>endstream +endobj +22 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 167 +>> +stream +GarW05mkI_&4Q=V`F#VQSZ,K\70UM]Mn-VGWC30XdbspQV83pXpp557*eT4-nNT1`#^08Rf8pba[K^cm7B44=jRnT)'K?Y!_o-'Xd$KGh6f*pL>lGBg3YUISI%0sL[)4K&0mV"rL1P41m3aEpO(Oendstream +endobj +23 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 262 +>> +stream +GarW4bA+pK&4Q?iMRuQnE&;l#G'=F5'q3C45Kp)<'Bl(2:?1Sq>(.;"Lm=KDjendstream +endobj +24 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 167 +>> +stream +GarW0YmS?5&-_rY`KY,2[kYm/Lmd;U4n)8L^>CNbM-K67PO4$pIc#YA[N="7;k`*bg6HG-3H1I:Oc@?^rETqVk2S`hAuD3XjG$hrY$;o0o!@8O*,PX[%Xbdo219TR[PYjYB"V@;M6R`QhNDP7g@'?7l*!B~>endstream +endobj +25 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 245 +>> +stream +GarW4]*cD?&4QKpMHd)Lgt?4sg)58p(5c>K-G(3oLqu4Qh_c`/,/_X?Kl\)c+FR*G9`ZLXdk-+bF:O1[#^VPk(.0^d>^]N8"Y:8_eceNuo\pFCTl8;ADtoB8^e%/BH9Q'/leU)Ketm'gGAhG8d]3*X.^/,jZXEc>aFc1K9PN;qHF6sh^&:Uu&B+.aq>DL)I_A*B[q/R0b:Eqd>ihB1o8K'3Lb)J@(88&BB3;G3Vi%7"GQK1T:[J~>endstream +endobj +26 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 172 +>> +stream +GapQh0E=F,0U\H3T\pNYT^QKk?tc>IP,;W#U1^23ihPEM_?CT3!/hd>6k,goQl7B?"*$l7Jjr"7HE)RNVF!h>6AQD_Ah8\RZl9o#.%!endstream +endobj +xref +0 27 +0000000000 65535 f +0000000061 00000 n +0000000102 00000 n +0000000209 00000 n +0000000321 00000 n +0000004077 00000 n +0000004335 00000 n +0000004530 00000 n +0000008451 00000 n +0000008709 00000 n +0000008904 00000 n +0000012557 00000 n +0000012817 00000 n +0000013013 00000 n +0000016826 00000 n +0000017086 00000 n +0000017282 00000 n +0000017352 00000 n +0000017614 00000 n +0000017720 00000 n +0000018072 00000 n +0000018333 00000 n +0000018666 00000 n +0000018924 00000 n +0000019277 00000 n +0000019535 00000 n +0000019871 00000 n +trailer +<< +/ID +[<11a4452bbe31319e89fba7a743537da0><11a4452bbe31319e89fba7a743537da0>] +% ReportLab generated PDF document -- digest (opensource) + +/Info 17 0 R +/Root 16 0 R +/Size 27 +>> +startxref +20134 +%%EOF diff --git a/src/documents/tests/samples/barcodes/split-by-tag-multiple-per-page.pdf b/src/documents/tests/samples/barcodes/split-by-tag-multiple-per-page.pdf new file mode 100644 index 000000000..5c4d97c63 --- /dev/null +++ b/src/documents/tests/samples/barcodes/split-by-tag-multiple-per-page.pdf @@ -0,0 +1,181 @@ +%PDF-1.3 +% ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 4 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/Contents 15 0 R /MediaBox [ 0 0 612 792 ] /Parent 14 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +4 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +5 0 obj +<< +/BitsPerComponent 8 /ColorSpace /DeviceRGB /Filter [ /ASCII85Decode /FlateDecode ] /Height 290 /Length 3461 /Subtype /Image + /Type /XObject /Width 290 +>> +stream +Gb"0M0bW:r$j4o4s3aL9.o/:sRKC1+V[Po_hnP="8Wk>jOsEV^,Y=.E8Wk>jOsEV^,Y=.E8Wk>jOsEV^,Y=.E8Wk>jOsEV^,Y=.EM=P;M`ictLh5:'u?=R'nd;IE]5Hh=BmqB2p^7X$0Q$9UiFPkD[m)hEDD7]3!20S(%m5Eepo,>73Ncpo[qg"0,Gt5J@p\hbEY.UOcVYbgK@oqO7DN/KYR@egT:O\Y"S^Nmne(=m!@M;\.mreAj`lssm2RjQmR*'f[]=0V/jtsN_^"C8&k'PptV(jd(Ymp-?-DiQUlg??aR5p7DE%a+(Q2+a1De[G>Bl&EKZ&,I(pUY]E@qJJG)r-?G9P(rih-1dREuNfk?>O(#o=aSKd[6HOfEV(Z'2t=fFn_3AbT-'E'3sZfj1^M@pYhQ7E1%B!q_i'CLMJZ]APP)MgR*7.Y/pg53RP?TA*/3L-50YH7,u"@RJ5[/9Q6C5NVbVGhM5l%_.?@umb=+S+0N]gQTFE$MV_[_Y246E[o=\bnb0967Q$FISai'U8mksuCAo?M*bkl?R-I0h_YM$B?F8J^DhM5l%EG"?[c+]I2gNP.=5$X;.1Gdp(p8uQo^/LHoiL3GZR2\raAeSG3ICLU;>is%i\_.+PGos32"IH[hA8X4IM[5E1-IZRS7[g)c,U.'3s[J\0_kok/NUqf`[Xe+0N]gQauWsDDo=BhM5l%_.@LHR@?oiRJ5[/9Q6C=:Zc7&>ipIE-50YH`fmsd"IFD+`\u,TrPk$]NL;edD'YO[I2buE1hPl,[ZP+_p2)p[e!QQPfLD$lgUH]`:1Im2@iJ!ODVrHt3K9FeNGTr/\U>Dmjtp]41q&NWk4WXSRF@Oke(@-QRG54@A56WH:1G57Ao?MGP<"Vj3K7l$RCR_b:ZaKGk$5CC.+u(L[aFc^qb6b_]O]p>fgaTjmPE\no9+M@B,b.F]?bTVcV*tKS8EA]mlo3K5;1^!EOO9f^ACUurOc[u`n=fDB;AhhFPj09^)KAJ38&9VV?L8MpH&M<8.ldJV05RX^_no.Ld9qHZg`b02a-m$D"%2.\6nf;,`[G2:]5WQ\V2c@4Gh=&YtOF%n^mA_13^REE`2l0OaBG;Wq]1Y8G/?Zt8UPc;l3PKnX1F]VM=136/NqdnAb9ps/J2L9Q0S)iGGeB)@_DF)%_Cm',a;^\2o]*8-oZUsS%9V$PXmM>H\bU0m00m3&T\6I=`1RmI^`mi+Cibh&sc>8Yj)cJ,VM7Wri3jVEGD+pLJ-LMZAlc^]d[kW$rRCHJJs-pTpHZ/#)PI^.2/_/Gu9ldRQT$2WWCT5#pBp+rKo47:$?VC&L8X%rrR4!(5rE?5)8Xe^PcTIWmmak?b:!t:GHfiH*GJBI/CQ^$TfeZFd^AG<;?^!=gc(929pYE$LqO43ODYD;<\aOu!e^l'@EjKDMb^K5$WP0]nZLJ^%HY#u61\3[enc_V2NHb$M)gp)%RGYQ;01^D,]VFZHi02I1r6C:L6.0i7*Bj-$T6+]-GAcILP+EW]kd`YIUbagAF!G%Ro\=[]cb7.BSXK;E)u5)]kJfT0mL;AEbfoP2a;6*b2r;r'Dt$>2Aq&o4^*)[NnW'2fK24Nao/eo%"\I%"GP'Z0I+"FNhmnk&i,4+8D*45U9lOt5(Y:H%gNYJ4S)E#I0RZO*cDrSi.cAfFP.AeSDgqSi-Obr20;bpKqYoS`%'Rr(9URn[j=kSMi,2qrR42k/aZcu)c8-TRTajWn-2h:0V>:?H.K8QTXcol?4Z\QM\UQ.esGSE+3uQBQEeG#L%A3LQAu,[ID*eB:EYk%6VF=)'\eEfuWs=\dD1g.f8NjCE.oPBA92cbB:e#8i1-tFRQ=g8G;/Vi_h'@1H2o>ZY116t>@KpYJMpA7)Ji/leW#F/+)#V*VC?f+jW%d?qJl]slE4fpD#^99j27h!!U!Boq])FiC1L1hLXTfG0bKqW+XE:1[0q:;4>9@iJ\8WL&g=b[jOE:;4>9@iJ\8WL&g=b[jOE:;4>9@iJ\8WL&g=b[jOE:;4>9@iJ\8WL&g=b[jOE:;4>9@iJ\8WL&g=b[jOE:;4>9@iJ\8WL&g=b[jOE:;4>9@iJ\8WL&g=b[jOE:;4>9@iJ\8WL&g=b[jOE:;4>9@iJ\8WL&g=b[jOE:;4>9j"6dh3D"AU^/LHoRCY]P]2P,]<+kV\Q$K"$)s"^pPrVEYk.Xc^pR/TYm^lDcP>l2_4-b)`W>jp44-_ftFlpD:RJ3,\612?`R?LT_mQ6\ZT;`dj^,qT?8Tj10;jmBJ\j>br;jihKBC7jHH(V&TjM!^@3D"AU^/LHoRCY]P]2P,]<+kV\Q$K"$)s"`VbpKshhJ;heIJ#,$L#d]nmrG`@m^r4^I;<3g8o>f_?gbP]CkDQP]k60U=20o&8FDiA/iT9X^3d':\+\@Uj;*pUjhAp_-FiO$C\FlYoddS,jF4Z.EjH)?]D%bBCL@$4DBZPtm^q7jK)=uLB&DH`u,3p=6A-(6\RV^<=bJ\F89ip8rc9/%LApI_"ofZO-'3pR6MG?iVY"m@=Eq[a)Y.q"N1qoK.Z\e#:l3*)"BA[ObqR\dSisd?'T6oD_R;-aleNSse-CL'A2.`f0WDraO2OS)NhURji-Dsc/e(A2o3I+\)VOF#I[81:r8`o)>9poa:.b-_B9dZ9lG;Ws3af/8:1cCb4:>XNcW@"N@mF0]uOu[eh;l6"R9!qH)P=aot>tp`%E[oU'ND1afPBSlqWl_5>q`ONacB7HTe^^)FjdQ)cmKTR7qbD9Vk'+?_^P9A:.ET;&?(LdsY0!m+DK&4Rmo3A$I[=j@CUb=RP3b9\eX>=VRf")l#,`aD:3C^AGI]'8L:b8NahC\ZSbZQoafjZ@E([G)<**^]QYZ/-\/Us$loWbJRG[+pr#4u-V^2.7F`lhj\L&UoOsEV^,Y=.E8Wk>jOsEV^,Y=.E8Wk>jOsEV^,Y=.E8Wk>jOsEV^,Y=.E8Wk>j''C?8XL9P~>endstream +endobj +6 0 obj +<< +/BitsPerComponent 8 /ColorSpace /DeviceRGB /Filter [ /ASCII85Decode /FlateDecode ] /Height 290 /Length 3802 /Subtype /Image + /Type /XObject /Width 290 +>> +stream +Gb"0M0s\bV$q&G/J(%!dMEdPCi+mj":+lnVm=5//INI)Ff49*4S%J.PEG]YhG"90TqgKU<#1mC0[%"\rkAb?X9m0%=\bggsf*9i;GI3jOn)n\-E(tLG]=Iff*nLVJA9YE8`&:.D17um52nS3pR@'BuYmi#Nq(-`rCL3?aR5kr8:bnZhE]:VmFd\Vb1U.B8oD'q]ZYNm6M4$@;gJBYtcZ1tVk&m)ZR5;)W-1gp`GI)'kQ\h+j'AH>=T?hO:]B47(R$fb8%41WA/9eF9?C-M:3S\hTaNT%`ls#mgH!Qj4iL3_VB"i'SXj#S5Y0?c^9e%nSh_k:3Ao:pVk'a`d'q(KYRXH\B2m4b'5$X;/bfk:U^6P+Uf9LV'Y";&ok.>6_b08,rb.J8:U`qVd?*eH\k2pethM5lEOdZ$Jfs`_Xo=?^G__nF7b^A%/>FE$I?((HGRT*g0^2*GEbhTk6bkl?R._*^*2DS2[>]!0Q26#Lh]@t>"V/Wgs&_Eh1bNrsg1R5i-`^F'q$8/ZcNQb?/&$ab&l]9,+WK$1e;f;-9Kq6%=KRI_oab&l]9,+WK$1gQ`&9B[J%dp!1M:0/cGVUr^Y&/&R@-&K1NOnOOnltfDpHSNR@'BuZ&Qs\p3^pnB$S?=S;D)nI^/(*1Ga)!B')d',P-gVhjg+&1GL[u@N1_Bm.oefbaaNX3>:/gr*mu2B4g:"bflL-7ckM6^,[u*B4>Ju`&9Bcf^m@UR5kp:F-LSfP;;UFZP/nmh8e@Go9=MJSt5(-mlp0RT;Tmpna&[,H1u=QrB+ZJM.\1scb#7Mn)l.k:-:VcH/B.5*DOc%9uNl+135C(NGD#t1NO4LWKbW^c!TMbdDX8a6sM2f1O&HB\99\`1H,o49$,<5r&(Vt:!CgU`2-?eF#ST.CI]$oEt`PmPg%q-p$8So50Gf:pTDB`o>IdeE>qlek2kH"\9ab!<@2U9$J2rG]*_Wc'>)E*D,B2[Q[;fml?Qi?RCE\U>k<").U5,4lm`[X6%s)\mTkOIkDuX%_:Q's;hmG879P-[(c3gRuD@0DDWdeap_^13"74\E;o0\@(5+gZOeMCiI)k!\8S'PaAEV7PDfLBI&mcO,oI9\pV-FT&)MS&\3Frsf3S;D)nVW)JWTA@Q<1M:/9RAm]ccY)ulouriV7V4.Wm-T[ZQu8]D6ht+'6dYM/?e#ibgrPh8e&%B4;gJWOSGDBD58Lk*jNL;.^ci]A-QNk'`Js\0a"UPhCf^/pC@,Gork5Y0"m.A[\;FbFMXNhFHgfCGKs$R;FA@EbCZ,pJJN]R`sPaAfJn)gf`b$I?^nE-8Tk1fnT&MG?28"+/LKjaikc:[\:@WSQ0Ra8*PnH135sRD(+jnB9et\;7bbUbhQ'-)p5eJ=lndoPchMC1O#):L@Qs@gLR;Y3f#4pO$#Xc20A'ccXM8m8&-(Hre).q__X)b0@*V:OMnagLR;Y3f#4pO$#Xc20A'ccXM8m8&-(Hre).q__X)b0@*V:OMnagLR;Y3f#4pO$#Xc!(Lam41+kr*hH\<Shj;F!q+f>DSH"O1;;qq<0`"22O'^"ri;"H3AXY]i4]Z^k.CXCj*T'F<=-0R6b3$\^WQ>C1K.9Tmb:QU^AG)h^?<^>R?[Y^;Wd)d.\>tgaipnG-K6rIU\<*[bZ+I5Ca1b-3>Il`\N)Ir4ql\lb^<1)AQE]o9XW3b2DR(\;fS4jRkn"11U2q`bi_r1B'&1!<;T@*,;\pO'pi(63A]2L'ALUPHqqXlY\ujlXcdqf"VpE1(!K.&do)^!E<0'A'RBbNrt28Yrr_IK$mI\=OF?AlfpD.`G]^oB4B:orR(,]@p(Z.IMMS5AqmOB*FXTeV95ZES(*cGr'G'/%Eg+2O%0u]AD_sRu-H6_.@etTqkTAofdl^9O,mN0!hSf-'dJmp\FDmid+XER9aYXW>'Qic_!-0f<^(PltSmZV7>i>rk+Z/Se\EPgf`b$b^$bCqb5e^`3[V2RIbHepR/OFM.`*C1[!jTmk\qk'@/-eB?n\3hIkg`-D"4TcCC6E][+MJ9K]B2S2i6hH(TptR!;ZB3HJLZo0*hs0_)5bF6:,?k'\Ro@H(GOk0/+]bkk)h_Sie'c'e0DRJ3,Z"m%9odCu(bY'2\4p<1.m[D?G"]NO3>2j8$lgq-f1>is8"'s;a:\b=4[bI,/tcFd<=H8h%'^YHG+)dMOLRh`)M1V*5&^!!h"A^7qkiHdGHCVSZ:>T6r1baT?MG;Hf'bIu*,_.Cp)=lFZcPA@qg]3H:[k00;0Y'2\4p<1.m[D?G"]NO3>2q,]'Pg">YiEL/`q0^cD#5>99UA?`e$VKRQ=]ZQt]<7"Uf>K6hREoMOD/esT-E:Dd\"I7qT67QX^$D,cIOSW0->m.mkKFrd'7J+eqg&o70@t:-Njsq[k+-q6M49jt3HI^G6soQ2^>?fQbOqC9,As6ZH"UK&io]?KcJ0!jdFZ%;Y;^ImE]n('Ln!UDhV$MM\9YWV1O$c3oJQ+(lV60I>gJg"i[4MjGP:\VDI0L/bT1[:IEakNH4r4jf5p)7\;@5cWiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!WiE)!Wf$BiN/nET~>endstream +endobj +7 0 obj +<< +/Contents 16 0 R /MediaBox [ 0 0 612 792 ] /Parent 14 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /XObject << +/FormXob.1ec20b3a96e40a35266a0a8a0d634adb 5 0 R /FormXob.bbe0a4b6628a0e8125ab26a62825893b 6 0 R +>> +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +8 0 obj +<< +/Contents 17 0 R /MediaBox [ 0 0 612 792 ] /Parent 14 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +9 0 obj +<< +/BitsPerComponent 8 /ColorSpace /DeviceRGB /Filter [ /ASCII85Decode /FlateDecode ] /Height 290 /Length 3500 /Subtype /Image + /Type /XObject /Width 290 +>> +stream +Gb"0M0s\bV$q&G/J($uOju)%+%1&)hn*)-NT`"8nzzzzzzzzzzzz!!'grT65NupmP_`]63jRce!oT8TqIFGMi(@D>9Q18%WpBeutHu8YIA4O7SpKc+sL9F0lZs.b3omCWORUeq#Fn]1ff7pJ#G-kItht;A6pmP_`]63jRce!oT8TqIF@N3&*FmnO#.ck97`6:E%D03I(`QWUU&i9D1[aFc>'f5%G8^-ObfLFJ>\r;M/@@JB>r)?I1e@5,du+nSeX'Eoh!BoPLr@VHWJ@\f-`;Z:LY8Kmo_Ad?D#0[5)F,u]k>=.H$p;]q^?Xu6pgRT0&;GI9.]Q(k==7(su_^mKeN$]^*XT/b_iRI^9\/Rk'_VO.X[X!?($+R'u%ohpmP1W1+Tpkqp$[=RJ65/WUOJ"FCk0:>qq.oTiPM!n,14O/tI1k<0>3<$5]2)lT?d&ATH1smHj(k't2X`iUD'W$A9g"p/H/Qm_8!9o-U&'oK;S+h0mmRk"Rt-k]u$5])/Y.baWi8dIY"AkBe/\3oGMcAUk_L);rMA#.X2i!H.gHJ/`tUi5T+.\FGmdDZ"(U:O*o%bOL")-=7^7DrE74n%8HFqc1)_qsI.l2X9/9=XrGe)G9:D2aqfB^S]TJQh-2j3jnnI0b'oU-pqAhRYDp-&E0eZ@h0kOd.U2CjG:$Z9F`64iQ1)?^./R#Qi;;q9^,G95_HAAGGP@N9hjJ)bTkmPnBP&2>q6mNRbVk[p.ML'C@j^(Kp6jTgZ9`&rR;L1/gVQ-1gJBf,9Jj)8R=&5kB4`+*#*k$W[Pk<$n'e`_=)ulnbsWAV8,n1Y\;=[tT6B[\7M6R:p1O1\nJ`cce;3%4W%9Cu];YgRj<\hRi@\^S#q;\'`3BG@'2DFDp_.g3E)3$iGVE:#8>Yn(i8??dQL.gM#W\4"p(2\i4mRD7k)U"b&c3-?#Z=p[5]00Bh9RD7&iiSJV&)h4)':2Vu(;!l(CTPIJrZHZrfS**lhrDLp#UDVjQe.sCQCb]ds]kIE*doS^q;DVjQe.sCQCb]ds]kIE*doS^q;DVjQe.sCQCb]ds]kIE*doS^q;DVjQe.sCQCb]ds]kIE*doS^q;DVjQe.sCQCb]dt()PtR=ZX#Ne^?[k]n7>Yqk"X@5,N$b[n+tfaEOuZ]=tTY?Y5"1hF(X2o%i[0Y4&I/QW`::2c81eHoLr:lT;0:AQJTg:"6Qqhp&n(qT^RT40.j^1..d;(9\b4QZdNohWf>5rbj0%"E=9M)9$`?o2DU%CYHQ'd/bh(O4X[8`a;i@8^*XN&i6/4oS>^0IF"$YVRS;Lg0=0)JU8j3sU!2h<13!]9bY$3.m58[g;k8V]Y5^+\)>H2oUMjp,BG:)qO1+5JhOIYF/#[obb<8HCGKl;^1-_DuEi6Cq2J1g9.'X4-oCLWfGBu>fA*2$m'&-5<5G.=`Vmk,5B&9%+Ymi#No@Ya?H95tRk08_qgJBXiTC$Zs\'m6IhOI,Nj6S(_kfW7]G@i>d]6GuK^GF1_VGb-dpCd3^o5%kcjh#ajEPFrts0m[M:a=eTY?+/Q$@*@YXq:#sL!:q!ThdT+nZPdC66nmtiM>M)I1WbY,IfmOP01+SS@m%\[Q[3Of"^576*(!7h)CHLZU3?^5"\m^4&XAlS&gq!Tkn-ZV5pa>F_*aPG"-*$7)!s@;;.u'G3*prREq=mOkD[UDr,o,2X7_Vq-@@iZY!i\p.aV;G9mdq<$L'BqoS#Q/D7G5&5=2B&?"jH1t1iW7uLWGC>n*R[oSo2j&%8I1k:217u7s3aHUt;K^6R.'Y9Ko@YXqe('1+7"Oc'p,d.jaf?/$.4ML+cQY]SR95;DOlX_E(t>pel7ZRjbNl-1fe?XOG^S03-W:M%[Eu17u7s3aHUt;K^6R.'Y9Ko@YXqe('1+apUs(p,X02DEh7SfseP+,u1V;r+DqE82-sb)nbWE/3Y5NP"]T:Lk9Zk"(]B\*gf?F9qzzzzzzzzzzzz5k,pqe-_`~>endstream +endobj +10 0 obj +<< +/Contents 18 0 R /MediaBox [ 0 0 612 792 ] /Parent 14 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /XObject << +/FormXob.1280b7d13f0587f75dbba24117c3e12a 9 0 R +>> +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +11 0 obj +<< +/Contents 19 0 R /MediaBox [ 0 0 612 792 ] /Parent 14 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +12 0 obj +<< +/PageMode /UseNone /Pages 14 0 R /Type /Catalog +>> +endobj +13 0 obj +<< +/Author (anonymous) /CreationDate (D:20251216142815+00'00') /Creator (anonymous) /Keywords () /ModDate (D:20251216142815+00'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (unspecified) /Title (untitled) /Trapped /False +>> +endobj +14 0 obj +<< +/Count 5 /Kids [ 3 0 R 7 0 R 8 0 R 10 0 R 11 0 R ] /Type /Pages +>> +endobj +15 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 164 +>> +stream +GarW05mkI_&4Q=V`ET>Mc.&b?;-+rnF+G1',/ca*pUAWf>EIgiipknB9RY?:(LXAhG(PtB're&F\nA'4a%aIp+-h"W@9D(-_!#HM*Z:^`FOHNUN-;cL_m8qodCk0^Akq$V4PUhHkMQ7^Ejj*/Mgoa@anH4$%uMYhOT~>endstream +endobj +16 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 306 +>> +stream +GasbT4\rsL&4$!fMAlpjH?ZXMZl9aE"XK0lYb%&R$>PaP&&cf78Q/KO<^N*hq5!cWA-.^JqQm+$jrVMM!b1Zf5UJAHTA[O5a#Wj7rghI[YR$H&I1;qf=XrnMp^cl'VsjlS<tc"96#@mAfhO_96j,pMI1=[Fq,J31>/c9Cp=j48@sD.G@[NB&N,!B3"QRendstream +endobj +17 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 185 +>> +stream +GarW0:CDb>&4c2endstream +endobj +18 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 244 +>> +stream +GarW4b6l*?&4Q?hMRuh(23Tpmkt^cRET&,M[qV:g6EQRMs2I6!U^T$](X?G+!r#.NZmDs+Qu<6UTF4R17*n#s2&gSmk2Mk.09@0r[k9DgRu9WjKt]k!Ic1n'J.q+%HiE61]07.7.f1^O]tp[&Fn>&8hUMD]+;spqhR>>LX`Bap0=GTNaa\,&7Zqt_h]FP:T4bD(VQ.g\Pm&EGSuJ6sK$-Os9'08g6q!hekXZP?.6B5f2_iendstream +endobj +19 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 164 +>> +stream +GapQh0E=F,0U\H3T\pNYT^QKk?tc>IP,;W#U1^23ihPEM_?CT3!/hd>6k,goQl7B?"*$l7Jjr"7HE)RrVF!h>6AQD_Ah8\RZl9o#.%!/H~>endstream +endobj +xref +0 20 +0000000000 65535 f +0000000061 00000 n +0000000102 00000 n +0000000209 00000 n +0000000404 00000 n +0000000516 00000 n +0000004168 00000 n +0000008161 00000 n +0000008467 00000 n +0000008662 00000 n +0000012353 00000 n +0000012612 00000 n +0000012808 00000 n +0000012878 00000 n +0000013140 00000 n +0000013226 00000 n +0000013481 00000 n +0000013878 00000 n +0000014154 00000 n +0000014489 00000 n +trailer +<< +/ID +[<3a098456c5603f47bfc8fb47cd3d7621><3a098456c5603f47bfc8fb47cd3d7621>] +% ReportLab generated PDF document -- digest (opensource) + +/Info 13 0 R +/Root 12 0 R +/Size 20 +>> +startxref +14744 +%%EOF diff --git a/src/documents/tests/samples/documents/originals/0000004.pdf b/src/documents/tests/samples/documents/originals/0000004.pdf new file mode 100644 index 000000000..953bb88ab Binary files /dev/null and b/src/documents/tests/samples/documents/originals/0000004.pdf differ diff --git a/src/documents/tests/samples/documents/originals/0000004.pdf.gpg b/src/documents/tests/samples/documents/originals/0000004.pdf.gpg deleted file mode 100644 index 754efcbf6..000000000 Binary files a/src/documents/tests/samples/documents/originals/0000004.pdf.gpg and /dev/null differ diff --git a/src/documents/tests/samples/documents/thumbnails/0000004.webp b/src/documents/tests/samples/documents/thumbnails/0000004.webp new file mode 100644 index 000000000..a7ff623b2 Binary files /dev/null and b/src/documents/tests/samples/documents/thumbnails/0000004.webp differ diff --git a/src/documents/tests/samples/documents/thumbnails/0000004.webp.gpg b/src/documents/tests/samples/documents/thumbnails/0000004.webp.gpg deleted file mode 100644 index 3abc69d36..000000000 Binary files a/src/documents/tests/samples/documents/thumbnails/0000004.webp.gpg and /dev/null differ diff --git a/src/documents/tests/test_api_app_config.py b/src/documents/tests/test_api_app_config.py index 6f487f5b0..f2ed902f4 100644 --- a/src/documents/tests/test_api_app_config.py +++ b/src/documents/tests/test_api_app_config.py @@ -1,6 +1,7 @@ import json from io import BytesIO from pathlib import Path +from unittest.mock import patch from django.contrib.auth.models import User from django.core.files.uploadedfile import SimpleUploadedFile @@ -66,6 +67,14 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase): "barcode_max_pages": None, "barcode_enable_tag": None, "barcode_tag_mapping": None, + "barcode_tag_split": None, + "ai_enabled": False, + "llm_embedding_backend": None, + "llm_embedding_model": None, + "llm_backend": None, + "llm_model": None, + "llm_api_key": None, + "llm_endpoint": None, }, ) @@ -611,3 +620,76 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase): ) self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) self.assertEqual(ApplicationConfiguration.objects.count(), 1) + + def test_update_llm_api_key(self): + """ + GIVEN: + - Existing config with llm_api_key specified + WHEN: + - API to update llm_api_key is called with all *s + - API to update llm_api_key is called with empty string + THEN: + - llm_api_key is unchanged + - llm_api_key is set to None + """ + config = ApplicationConfiguration.objects.first() + config.llm_api_key = "1234567890" + config.save() + + # Test with all * + response = self.client.patch( + f"{self.ENDPOINT}1/", + json.dumps( + { + "llm_api_key": "*" * 32, + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + config.refresh_from_db() + self.assertEqual(config.llm_api_key, "1234567890") + # Test with empty string + response = self.client.patch( + f"{self.ENDPOINT}1/", + json.dumps( + { + "llm_api_key": "", + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + config.refresh_from_db() + self.assertEqual(config.llm_api_key, None) + + def test_enable_ai_index_triggers_update(self): + """ + GIVEN: + - Existing config with AI disabled + WHEN: + - Config is updated to enable AI with llm_embedding_backend + THEN: + - LLM index is triggered to update + """ + config = ApplicationConfiguration.objects.first() + config.ai_enabled = False + config.llm_embedding_backend = None + config.save() + + with ( + patch("documents.tasks.llmindex_index.delay") as mock_update, + patch("paperless_ai.indexing.vector_store_file_exists") as mock_exists, + ): + mock_exists.return_value = False + self.client.patch( + f"{self.ENDPOINT}1/", + json.dumps( + { + "ai_enabled": True, + "llm_embedding_backend": "openai", + }, + ), + content_type="application/json", + ) + mock_update.assert_called_once() diff --git a/src/documents/tests/test_api_documents.py b/src/documents/tests/test_api_documents.py index f40ef157f..96d22dc2c 100644 --- a/src/documents/tests/test_api_documents.py +++ b/src/documents/tests/test_api_documents.py @@ -131,6 +131,10 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): self.assertIn("content", results_full[0]) self.assertIn("id", results_full[0]) + # Content length is used internally for performance reasons. + # No need to expose this field. + self.assertNotIn("content_length", results_full[0]) + response = self.client.get("/api/documents/?fields=id", format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) results = response.data["results"] diff --git a/src/documents/tests/test_api_objects.py b/src/documents/tests/test_api_objects.py index 014dd3c2a..0eb99f023 100644 --- a/src/documents/tests/test_api_objects.py +++ b/src/documents/tests/test_api_objects.py @@ -219,6 +219,30 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase): self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(StoragePath.objects.count(), 1) + def test_api_create_storage_path_rejects_traversal(self): + """ + GIVEN: + - API request to create a storage paths + - Storage path attempts directory traversal + WHEN: + - API is called + THEN: + - Correct HTTP 400 response + - No storage path is created + """ + response = self.client.post( + self.ENDPOINT, + json.dumps( + { + "name": "Traversal path", + "path": "../../../../../tmp/proof", + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(StoragePath.objects.count(), 1) + def test_api_storage_path_placeholders(self): """ GIVEN: diff --git a/src/documents/tests/test_api_status.py b/src/documents/tests/test_api_status.py index 9b7bf37ad..8e29c53d2 100644 --- a/src/documents/tests/test_api_status.py +++ b/src/documents/tests/test_api_status.py @@ -1,4 +1,6 @@ import os +import shutil +import tempfile from pathlib import Path from unittest import mock @@ -16,9 +18,19 @@ class TestSystemStatus(APITestCase): ENDPOINT = "/api/status/" def setUp(self): + super().setUp() self.user = User.objects.create_superuser( username="temp_admin", ) + self.tmp_dir = Path(tempfile.mkdtemp()) + self.override = override_settings(MEDIA_ROOT=self.tmp_dir) + self.override.enable() + + def tearDown(self): + super().tearDown() + + self.override.disable() + shutil.rmtree(self.tmp_dir) def test_system_status(self): """ @@ -310,3 +322,69 @@ class TestSystemStatus(APITestCase): "ERROR", ) self.assertIsNotNone(response.data["tasks"]["sanity_check_error"]) + + def test_system_status_ai_disabled(self): + """ + GIVEN: + - The AI feature is disabled + WHEN: + - The user requests the system status + THEN: + - The response contains the correct AI status + """ + with override_settings(AI_ENABLED=False): + self.client.force_login(self.user) + response = self.client.get(self.ENDPOINT) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["tasks"]["llmindex_status"], "DISABLED") + self.assertIsNone(response.data["tasks"]["llmindex_error"]) + + def test_system_status_ai_enabled(self): + """ + GIVEN: + - The AI index feature is enabled, but no tasks are found + - The AI index feature is enabled and a task is found + WHEN: + - The user requests the system status + THEN: + - The response contains the correct AI status + """ + with override_settings(AI_ENABLED=True, LLM_EMBEDDING_BACKEND="openai"): + self.client.force_login(self.user) + + # No tasks found + response = self.client.get(self.ENDPOINT) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["tasks"]["llmindex_status"], "WARNING") + + PaperlessTask.objects.create( + type=PaperlessTask.TaskType.SCHEDULED_TASK, + status=states.SUCCESS, + task_name=PaperlessTask.TaskName.LLMINDEX_UPDATE, + ) + response = self.client.get(self.ENDPOINT) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["tasks"]["llmindex_status"], "OK") + self.assertIsNone(response.data["tasks"]["llmindex_error"]) + + def test_system_status_ai_error(self): + """ + GIVEN: + - The AI index feature is enabled and a task is found with an error + WHEN: + - The user requests the system status + THEN: + - The response contains the correct AI status + """ + with override_settings(AI_ENABLED=True, LLM_EMBEDDING_BACKEND="openai"): + PaperlessTask.objects.create( + type=PaperlessTask.TaskType.SCHEDULED_TASK, + status=states.FAILURE, + task_name=PaperlessTask.TaskName.LLMINDEX_UPDATE, + result="AI index update failed", + ) + self.client.force_login(self.user) + response = self.client.get(self.ENDPOINT) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["tasks"]["llmindex_status"], "ERROR") + self.assertIsNotNone(response.data["tasks"]["llmindex_error"]) diff --git a/src/documents/tests/test_api_tasks.py b/src/documents/tests/test_api_tasks.py index aa42577c4..6429ef44f 100644 --- a/src/documents/tests/test_api_tasks.py +++ b/src/documents/tests/test_api_tasks.py @@ -7,6 +7,7 @@ from django.contrib.auth.models import User from rest_framework import status from rest_framework.test import APITestCase +from documents.models import Document from documents.models import PaperlessTask from documents.tests.utils import DirectoriesMixin from documents.views import TasksViewSet @@ -258,7 +259,7 @@ class TestTasks(DirectoriesMixin, APITestCase): task_id=str(uuid.uuid4()), task_file_name="task_one.pdf", status=celery.states.FAILURE, - result="test.pdf: Not consuming test.pdf: It is a duplicate.", + result="test.pdf: Unexpected error during ingestion.", ) response = self.client.get(self.ENDPOINT) @@ -270,7 +271,7 @@ class TestTasks(DirectoriesMixin, APITestCase): self.assertEqual( returned_data["result"], - "test.pdf: Not consuming test.pdf: It is a duplicate.", + "test.pdf: Unexpected error during ingestion.", ) def test_task_name_webui(self): @@ -325,20 +326,34 @@ class TestTasks(DirectoriesMixin, APITestCase): self.assertEqual(returned_data["task_file_name"], "anothertest.pdf") - def test_task_result_failed_duplicate_includes_related_doc(self): + def test_task_result_duplicate_warning_includes_count(self): """ GIVEN: - - A celery task failed with a duplicate error + - A celery task succeeds, but a duplicate exists WHEN: - API call is made to get tasks THEN: - - The returned data includes a related document link + - The returned data includes duplicate warning metadata """ + checksum = "duplicate-checksum" + Document.objects.create( + title="Existing", + content="", + mime_type="application/pdf", + checksum=checksum, + ) + created_doc = Document.objects.create( + title="Created", + content="", + mime_type="application/pdf", + checksum=checksum, + archive_checksum="another-checksum", + ) PaperlessTask.objects.create( task_id=str(uuid.uuid4()), task_file_name="task_one.pdf", - status=celery.states.FAILURE, - result="Not consuming task_one.pdf: It is a duplicate of task_one_existing.pdf (#1234).", + status=celery.states.SUCCESS, + result=f"Success. New document id {created_doc.pk} created", ) response = self.client.get(self.ENDPOINT) @@ -348,7 +363,7 @@ class TestTasks(DirectoriesMixin, APITestCase): returned_data = response.data[0] - self.assertEqual(returned_data["related_document"], "1234") + self.assertEqual(returned_data["related_document"], str(created_doc.pk)) def test_run_train_classifier_task(self): """ diff --git a/src/documents/tests/test_api_uisettings.py b/src/documents/tests/test_api_uisettings.py index 26c6f17ae..c733315e6 100644 --- a/src/documents/tests/test_api_uisettings.py +++ b/src/documents/tests/test_api_uisettings.py @@ -49,6 +49,7 @@ class TestApiUiSettings(DirectoriesMixin, APITestCase): "backend_setting": "default", }, "email_enabled": False, + "ai_enabled": False, }, ) diff --git a/src/documents/tests/test_api_workflows.py b/src/documents/tests/test_api_workflows.py index 9efdb8451..1d3efd457 100644 --- a/src/documents/tests/test_api_workflows.py +++ b/src/documents/tests/test_api_workflows.py @@ -186,8 +186,11 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase): "filter_has_tags": [self.t1.id], "filter_has_all_tags": [self.t2.id], "filter_has_not_tags": [self.t3.id], + "filter_has_any_correspondents": [self.c.id], "filter_has_not_correspondents": [self.c2.id], + "filter_has_any_document_types": [self.dt.id], "filter_has_not_document_types": [self.dt2.id], + "filter_has_any_storage_paths": [self.sp.id], "filter_has_not_storage_paths": [self.sp2.id], "filter_custom_field_query": json.dumps( [ @@ -248,14 +251,26 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase): set(trigger.filter_has_not_tags.values_list("id", flat=True)), {self.t3.id}, ) + self.assertSetEqual( + set(trigger.filter_has_any_correspondents.values_list("id", flat=True)), + {self.c.id}, + ) self.assertSetEqual( set(trigger.filter_has_not_correspondents.values_list("id", flat=True)), {self.c2.id}, ) + self.assertSetEqual( + set(trigger.filter_has_any_document_types.values_list("id", flat=True)), + {self.dt.id}, + ) self.assertSetEqual( set(trigger.filter_has_not_document_types.values_list("id", flat=True)), {self.dt2.id}, ) + self.assertSetEqual( + set(trigger.filter_has_any_storage_paths.values_list("id", flat=True)), + {self.sp.id}, + ) self.assertSetEqual( set(trigger.filter_has_not_storage_paths.values_list("id", flat=True)), {self.sp2.id}, @@ -419,8 +434,11 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase): "filter_has_tags": [self.t1.id], "filter_has_all_tags": [self.t2.id], "filter_has_not_tags": [self.t3.id], + "filter_has_any_correspondents": [self.c.id], "filter_has_not_correspondents": [self.c2.id], + "filter_has_any_document_types": [self.dt.id], "filter_has_not_document_types": [self.dt2.id], + "filter_has_any_storage_paths": [self.sp.id], "filter_has_not_storage_paths": [self.sp2.id], "filter_custom_field_query": json.dumps( ["AND", [[self.cf1.id, "exact", "value"]]], @@ -450,14 +468,26 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase): workflow.triggers.first().filter_has_not_tags.first(), self.t3, ) + self.assertEqual( + workflow.triggers.first().filter_has_any_correspondents.first(), + self.c, + ) self.assertEqual( workflow.triggers.first().filter_has_not_correspondents.first(), self.c2, ) + self.assertEqual( + workflow.triggers.first().filter_has_any_document_types.first(), + self.dt, + ) self.assertEqual( workflow.triggers.first().filter_has_not_document_types.first(), self.dt2, ) + self.assertEqual( + workflow.triggers.first().filter_has_any_storage_paths.first(), + self.sp, + ) self.assertEqual( workflow.triggers.first().filter_has_not_storage_paths.first(), self.sp2, diff --git a/src/documents/tests/test_barcodes.py b/src/documents/tests/test_barcodes.py index 70c69571d..c54911d64 100644 --- a/src/documents/tests/test_barcodes.py +++ b/src/documents/tests/test_barcodes.py @@ -858,6 +858,35 @@ class TestTagBarcode(DirectoriesMixin, SampleDirMixin, GetReaderPluginMixin, Tes yield reader reader.cleanup() + @override_settings( + CONSUMER_ENABLE_TAG_BARCODE=True, + CONSUMER_TAG_BARCODE_MAPPING={"TAG:(.*)": "\\g<1>"}, + ) + def test_barcode_without_tag_match(self): + """ + GIVEN: + - Barcode that does not match any TAG mapping pattern + - TAG mapping configured for "TAG:" prefix only + WHEN: + - is_tag property is checked on an ASN barcode + THEN: + - Returns False + """ + test_file = self.BARCODE_SAMPLE_DIR / "barcode-39-asn-123.pdf" + with self.get_reader(test_file) as reader: + reader.detect() + + self.assertGreater( + len(reader.barcodes), + 0, + "Should have detected at least one barcode", + ) + asn_barcode = reader.barcodes[0] + self.assertFalse( + asn_barcode.is_tag, + f"ASN barcode '{asn_barcode.value}' should not match TAG: pattern", + ) + @override_settings(CONSUMER_ENABLE_TAG_BARCODE=True) def test_scan_file_without_matching_barcodes(self): """ @@ -964,3 +993,163 @@ class TestTagBarcode(DirectoriesMixin, SampleDirMixin, GetReaderPluginMixin, Tes # expect error to be caught and logged only tags = reader.metadata.tag_ids self.assertEqual(tags, None) + + @override_settings( + CONSUMER_ENABLE_TAG_BARCODE=True, + CONSUMER_TAG_BARCODE_SPLIT=True, + CONSUMER_TAG_BARCODE_MAPPING={"TAG:(.*)": "\\g<1>"}, + ) + def test_split_on_tag_barcodes(self): + """ + GIVEN: + - PDF containing barcodes with TAG: prefix + - Tag barcode splitting is enabled with TAG: mapping + WHEN: + - File is processed + THEN: + - Splits should occur at pages with TAG barcodes + - Tags should NOT be assigned when tag splitting is enabled (they're assigned during re-consumption) + """ + test_file = self.BARCODE_SAMPLE_DIR / "split-by-tag-basic.pdf" + with self.get_reader(test_file) as reader: + reader.detect() + separator_page_numbers = reader.get_separation_pages() + + self.assertDictEqual(separator_page_numbers, {1: True, 3: True}) + + tags = reader.metadata.tag_ids + self.assertIsNone(tags) + + @override_settings( + CONSUMER_ENABLE_TAG_BARCODE=True, + CONSUMER_TAG_BARCODE_SPLIT=False, + CONSUMER_TAG_BARCODE_MAPPING={"TAG:(.*)": "\\g<1>"}, + ) + def test_no_split_when_tag_split_disabled(self): + """ + GIVEN: + - PDF containing TAG barcodes (TAG:invoice, TAG:receipt) + - Tag barcode splitting is disabled + WHEN: + - File is processed + THEN: + - No separation pages are identified + - Tags are still extracted and assigned + """ + test_file = self.BARCODE_SAMPLE_DIR / "split-by-tag-basic.pdf" + with self.get_reader(test_file) as reader: + reader.run() + separator_page_numbers = reader.get_separation_pages() + + self.assertDictEqual(separator_page_numbers, {}) + + tags = reader.metadata.tag_ids + self.assertEqual(len(tags), 2) + + @override_settings( + CONSUMER_ENABLE_BARCODES=True, + CONSUMER_ENABLE_TAG_BARCODE=True, + CONSUMER_TAG_BARCODE_SPLIT=True, + CONSUMER_TAG_BARCODE_MAPPING={"TAG:(.*)": "\\g<1>"}, + CELERY_TASK_ALWAYS_EAGER=True, + OCR_MODE="skip", + ) + def test_consume_barcode_file_tag_split_and_assignment(self): + """ + GIVEN: + - PDF containing TAG barcodes on pages 2 and 4 (TAG:invoice, TAG:receipt) + - Tag barcode splitting is enabled + WHEN: + - File is consumed + THEN: + - PDF is split into 3 documents at barcode pages + - Each split document has the appropriate TAG barcodes extracted and assigned + - Document 1: page 1 (no tags) + - Document 2: pages 2-3 with TAG:invoice + - Document 3: pages 4-5 with TAG:receipt + """ + test_file = self.BARCODE_SAMPLE_DIR / "split-by-tag-basic.pdf" + dst = settings.SCRATCH_DIR / "split-by-tag-basic.pdf" + shutil.copy(test_file, dst) + + with mock.patch("documents.tasks.ProgressManager", DummyProgressManager): + result = tasks.consume_file( + ConsumableDocument( + source=DocumentSource.ConsumeFolder, + original_file=dst, + ), + None, + ) + + self.assertEqual(result, "Barcode splitting complete!") + + documents = Document.objects.all().order_by("id") + self.assertEqual(documents.count(), 3) + + doc1 = documents[0] + self.assertEqual(doc1.tags.count(), 0) + + doc2 = documents[1] + self.assertEqual(doc2.tags.count(), 1) + self.assertEqual(doc2.tags.first().name, "invoice") + + doc3 = documents[2] + self.assertEqual(doc3.tags.count(), 1) + self.assertEqual(doc3.tags.first().name, "receipt") + + @override_settings( + CONSUMER_ENABLE_TAG_BARCODE=True, + CONSUMER_TAG_BARCODE_SPLIT=True, + CONSUMER_TAG_BARCODE_MAPPING={"ASN(.*)": "ASN_\\g<1>", "TAG:(.*)": "\\g<1>"}, + ) + def test_split_by_mixed_asn_tag_backwards_compat(self): + """ + GIVEN: + - PDF with mixed ASN and TAG barcodes + - Mapping that treats ASN barcodes as tags (backwards compatibility) + - ASN12345 on page 1, TAG:personal on page 3, ASN13456 on page 5, TAG:business on page 7 + WHEN: + - File is consumed + THEN: + - Both ASN and TAG barcodes trigger splits + - Split points are at pages 3, 5, and 7 (page 1 never splits) + - 4 separate documents are produced + """ + test_file = self.BARCODE_SAMPLE_DIR / "split-by-tag-mixed-asn.pdf" + + with self.get_reader(test_file) as reader: + reader.detect() + separator_pages = reader.get_separation_pages() + + self.assertDictEqual(separator_pages, {2: True, 4: True, 6: True}) + + document_list = reader.separate_pages(separator_pages) + self.assertEqual(len(document_list), 4) + + @override_settings( + CONSUMER_ENABLE_TAG_BARCODE=True, + CONSUMER_TAG_BARCODE_SPLIT=True, + CONSUMER_TAG_BARCODE_MAPPING={"TAG:(.*)": "\\g<1>"}, + ) + def test_split_by_tag_multiple_per_page(self): + """ + GIVEN: + - PDF with multiple TAG barcodes on same page + - TAG:invoice and TAG:expense on page 2, TAG:receipt on page 4 + WHEN: + - File is processed + THEN: + - Pages with barcodes trigger splits + - Split points at pages 2 and 4 + - 3 separate documents are produced + """ + test_file = self.BARCODE_SAMPLE_DIR / "split-by-tag-multiple-per-page.pdf" + + with self.get_reader(test_file) as reader: + reader.detect() + separator_pages = reader.get_separation_pages() + + self.assertDictEqual(separator_pages, {1: True, 3: True}) + + document_list = reader.separate_pages(separator_pages) + self.assertEqual(len(document_list), 3) diff --git a/src/documents/tests/test_checks.py b/src/documents/tests/test_checks.py index 4af05746f..304074e37 100644 --- a/src/documents/tests/test_checks.py +++ b/src/documents/tests/test_checks.py @@ -1,4 +1,3 @@ -import textwrap from unittest import mock from django.core.checks import Error @@ -6,60 +5,11 @@ from django.core.checks import Warning from django.test import TestCase from django.test import override_settings -from documents.checks import changed_password_check from documents.checks import filename_format_check from documents.checks import parser_check -from documents.models import Document -from documents.tests.factories import DocumentFactory class TestDocumentChecks(TestCase): - def test_changed_password_check_empty_db(self): - self.assertListEqual(changed_password_check(None), []) - - def test_changed_password_check_no_encryption(self): - DocumentFactory.create(storage_type=Document.STORAGE_TYPE_UNENCRYPTED) - self.assertListEqual(changed_password_check(None), []) - - def test_encrypted_missing_passphrase(self): - DocumentFactory.create(storage_type=Document.STORAGE_TYPE_GPG) - msgs = changed_password_check(None) - self.assertEqual(len(msgs), 1) - msg_text = msgs[0].msg - self.assertEqual( - msg_text, - "The database contains encrypted documents but no password is set.", - ) - - @override_settings( - PASSPHRASE="test", - ) - @mock.patch("paperless.db.GnuPG.decrypted") - @mock.patch("documents.models.Document.source_file") - def test_encrypted_decrypt_fails(self, mock_decrypted, mock_source_file): - mock_decrypted.return_value = None - mock_source_file.return_value = b"" - - DocumentFactory.create(storage_type=Document.STORAGE_TYPE_GPG) - - msgs = changed_password_check(None) - - self.assertEqual(len(msgs), 1) - msg_text = msgs[0].msg - self.assertEqual( - msg_text, - textwrap.dedent( - """ - The current password doesn't match the password of the - existing documents. - - If you intend to change your password, you must first export - all of the old documents, start fresh with the new password - and then re-import them." - """, - ), - ) - def test_parser_check(self): self.assertEqual(parser_check(None), []) diff --git a/src/documents/tests/test_consumer.py b/src/documents/tests/test_consumer.py index 63d6f8f5b..16fa2bf70 100644 --- a/src/documents/tests/test_consumer.py +++ b/src/documents/tests/test_consumer.py @@ -485,21 +485,21 @@ class TestConsumer( with self.get_consumer(self.get_test_file()) as consumer: consumer.run() - with self.assertRaisesMessage(ConsumerError, "It is a duplicate"): - with self.get_consumer(self.get_test_file()) as consumer: - consumer.run() + with self.get_consumer(self.get_test_file()) as consumer: + consumer.run() - self._assert_first_last_send_progress(last_status="FAILED") + self.assertEqual(Document.objects.count(), 2) + self._assert_first_last_send_progress() def testDuplicates2(self): with self.get_consumer(self.get_test_file()) as consumer: consumer.run() - with self.assertRaisesMessage(ConsumerError, "It is a duplicate"): - with self.get_consumer(self.get_test_archive_file()) as consumer: - consumer.run() + with self.get_consumer(self.get_test_archive_file()) as consumer: + consumer.run() - self._assert_first_last_send_progress(last_status="FAILED") + self.assertEqual(Document.objects.count(), 2) + self._assert_first_last_send_progress() def testDuplicates3(self): with self.get_consumer(self.get_test_archive_file()) as consumer: @@ -513,9 +513,10 @@ class TestConsumer( Document.objects.all().delete() - with self.assertRaisesMessage(ConsumerError, "document is in the trash"): - with self.get_consumer(self.get_test_file()) as consumer: - consumer.run() + with self.get_consumer(self.get_test_file()) as consumer: + consumer.run() + + self.assertEqual(Document.objects.count(), 1) def testAsnExists(self): with self.get_consumer( @@ -718,12 +719,45 @@ class TestConsumer( dst = self.get_test_file() self.assertIsFile(dst) - with self.assertRaises(ConsumerError): + expected_message = ( + f"{dst.name}: Not consuming {dst.name}: " + f"It is a duplicate of {document.title} (#{document.pk})" + ) + + with self.assertRaisesMessage(ConsumerError, expected_message): with self.get_consumer(dst) as consumer: consumer.run() self.assertIsNotFile(dst) - self._assert_first_last_send_progress(last_status="FAILED") + self.assertEqual(Document.objects.count(), 1) + self._assert_first_last_send_progress(last_status=ProgressStatusOptions.FAILED) + + @override_settings(CONSUMER_DELETE_DUPLICATES=True) + def test_delete_duplicate_in_trash(self): + dst = self.get_test_file() + with self.get_consumer(dst) as consumer: + consumer.run() + + # Move the existing document to trash + document = Document.objects.first() + document.delete() + + dst = self.get_test_file() + self.assertIsFile(dst) + + expected_message = ( + f"{dst.name}: Not consuming {dst.name}: " + f"It is a duplicate of {document.title} (#{document.pk})" + f" Note: existing document is in the trash." + ) + + with self.assertRaisesMessage(ConsumerError, expected_message): + with self.get_consumer(dst) as consumer: + consumer.run() + + self.assertIsNotFile(dst) + self.assertEqual(Document.global_objects.count(), 1) + self.assertEqual(Document.objects.count(), 0) @override_settings(CONSUMER_DELETE_DUPLICATES=False) def test_no_delete_duplicate(self): @@ -743,15 +777,12 @@ class TestConsumer( dst = self.get_test_file() self.assertIsFile(dst) - with self.assertRaisesRegex( - ConsumerError, - r"sample\.pdf: Not consuming sample\.pdf: It is a duplicate of sample \(#\d+\)", - ): - with self.get_consumer(dst) as consumer: - consumer.run() + with self.get_consumer(dst) as consumer: + consumer.run() - self.assertIsFile(dst) - self._assert_first_last_send_progress(last_status="FAILED") + self.assertIsNotFile(dst) + self.assertEqual(Document.objects.count(), 2) + self._assert_first_last_send_progress() @override_settings(FILENAME_FORMAT="{title}") @mock.patch("documents.parsers.document_consumer_declaration.send") diff --git a/src/documents/tests/test_double_sided.py b/src/documents/tests/test_double_sided.py index 5d068b735..32ca5ceab 100644 --- a/src/documents/tests/test_double_sided.py +++ b/src/documents/tests/test_double_sided.py @@ -224,17 +224,18 @@ class TestDoubleSided(DirectoriesMixin, FileSystemAssertsMixin, TestCase): THEN: - The collated file gets put into foo/bar """ + # TODO: parameterize this instead for path in [ Path("foo") / "bar" / "double-sided", Path("double-sided") / "foo" / "bar", ]: - with self.subTest(path=path): + with self.subTest(path=str(path)): # Ensure we get fresh directories for each run self.tearDown() self.setUp() self.create_staging_file() - self.consume_file("double-sided-odd.pdf", path / "foo.pdf") + self.consume_file("double-sided-odd.pdf", Path(path) / "foo.pdf") self.assertIsFile( self.dirs.consumption_dir / "foo" / "bar" / "foo-collated.pdf", ) diff --git a/src/documents/tests/test_file_handling.py b/src/documents/tests/test_file_handling.py index befc7050f..f6764d3f8 100644 --- a/src/documents/tests/test_file_handling.py +++ b/src/documents/tests/test_file_handling.py @@ -34,22 +34,14 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): def test_generate_source_filename(self): document = Document() document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED document.save() self.assertEqual(generate_filename(document), Path(f"{document.pk:07d}.pdf")) - document.storage_type = Document.STORAGE_TYPE_GPG - self.assertEqual( - generate_filename(document), - Path(f"{document.pk:07d}.pdf.gpg"), - ) - @override_settings(FILENAME_FORMAT="{correspondent}/{correspondent}") def test_file_renaming(self): document = Document() document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED document.save() # Test default source_path @@ -63,11 +55,6 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): # Ensure that filename is properly generated self.assertEqual(document.filename, Path("none/none.pdf")) - # Enable encryption and check again - document.storage_type = Document.STORAGE_TYPE_GPG - document.filename = generate_filename(document) - self.assertEqual(document.filename, Path("none/none.pdf.gpg")) - document.save() # test that creating dirs for the source_path creates the correct directory @@ -87,14 +74,14 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): settings.ORIGINALS_DIR / "none", ) self.assertIsFile( - settings.ORIGINALS_DIR / "test" / "test.pdf.gpg", + settings.ORIGINALS_DIR / "test" / "test.pdf", ) @override_settings(FILENAME_FORMAT="{correspondent}/{correspondent}") def test_file_renaming_missing_permissions(self): document = Document() document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED + document.save() # Ensure that filename is properly generated @@ -128,14 +115,13 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): def test_file_renaming_database_error(self): Document.objects.create( mime_type="application/pdf", - storage_type=Document.STORAGE_TYPE_UNENCRYPTED, checksum="AAAAA", ) document = Document() document.mime_type = "application/pdf" document.checksum = "BBBBB" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED + document.save() # Ensure that filename is properly generated @@ -170,7 +156,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): def test_document_delete(self): document = Document() document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED + document.save() # Ensure that filename is properly generated @@ -196,7 +182,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): def test_document_delete_trash_dir(self): document = Document() document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED + document.save() # Ensure that filename is properly generated @@ -221,7 +207,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): # Create an identical document and ensure it is trashed under a new name document = Document() document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED + document.save() document.filename = generate_filename(document) document.save() @@ -235,7 +221,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): def test_document_delete_nofile(self): document = Document() document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED + document.save() document.delete() @@ -245,7 +231,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): def test_directory_not_empty(self): document = Document() document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED + document.save() # Ensure that filename is properly generated @@ -362,7 +348,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): def test_nested_directory_cleanup(self): document = Document() document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED + document.save() # Ensure that filename is properly generated @@ -390,7 +376,6 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): document = Document() document.pk = 1 document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED self.assertEqual(generate_filename(document), Path("0000001.pdf")) @@ -403,7 +388,6 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): document = Document() document.pk = 1 document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED self.assertEqual(generate_filename(document), Path("0000001.pdf")) @@ -429,7 +413,6 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): document = Document() document.pk = 1 document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED self.assertEqual(generate_filename(document), Path("0000001.pdf")) @@ -438,7 +421,6 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): document = Document() document.pk = 1 document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED self.assertEqual(generate_filename(document), Path("0000001.pdf")) @@ -1258,7 +1240,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase): title="doc1", mime_type="application/pdf", ) - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED + document.save() # Ensure that filename is properly generated @@ -1732,7 +1714,6 @@ class TestPathDateLocalization: document = DocumentFactory.create( title="My Document", mime_type="application/pdf", - storage_type=Document.STORAGE_TYPE_UNENCRYPTED, created=self.TEST_DATE, # 2023-10-26 (which is a Thursday) ) with override_settings(FILENAME_FORMAT=filename_format): diff --git a/src/documents/tests/test_index.py b/src/documents/tests/test_index.py index 3167bb762..ef6b535f7 100644 --- a/src/documents/tests/test_index.py +++ b/src/documents/tests/test_index.py @@ -180,7 +180,7 @@ class TestRewriteNaturalDateKeywords(SimpleTestCase): ( "added:this year", datetime(2025, 7, 15, 12, 0, 0, tzinfo=timezone.utc), - ("added:[20250101", "TO 20250715"), + ("added:[20250101", "TO 20251231"), ), ( "added:previous year", diff --git a/src/documents/tests/test_management.py b/src/documents/tests/test_management.py index 014f5d673..e1b88633c 100644 --- a/src/documents/tests/test_management.py +++ b/src/documents/tests/test_management.py @@ -1,7 +1,5 @@ import filecmp -import hashlib import shutil -import tempfile from io import StringIO from pathlib import Path from unittest import mock @@ -96,66 +94,6 @@ class TestArchiver(DirectoriesMixin, FileSystemAssertsMixin, TestCase): self.assertEqual(doc2.archive_filename, "document_01.pdf") -class TestDecryptDocuments(FileSystemAssertsMixin, TestCase): - @mock.patch("documents.management.commands.decrypt_documents.input") - def test_decrypt(self, m): - media_dir = tempfile.mkdtemp() - originals_dir = Path(media_dir) / "documents" / "originals" - thumb_dir = Path(media_dir) / "documents" / "thumbnails" - originals_dir.mkdir(parents=True, exist_ok=True) - thumb_dir.mkdir(parents=True, exist_ok=True) - - with override_settings( - ORIGINALS_DIR=originals_dir, - THUMBNAIL_DIR=thumb_dir, - PASSPHRASE="test", - FILENAME_FORMAT=None, - ): - doc = Document.objects.create( - checksum="82186aaa94f0b98697d704b90fd1c072", - title="wow", - filename="0000004.pdf.gpg", - mime_type="application/pdf", - storage_type=Document.STORAGE_TYPE_GPG, - ) - - shutil.copy( - ( - Path(__file__).parent - / "samples" - / "documents" - / "originals" - / "0000004.pdf.gpg" - ), - originals_dir / "0000004.pdf.gpg", - ) - shutil.copy( - ( - Path(__file__).parent - / "samples" - / "documents" - / "thumbnails" - / "0000004.webp.gpg" - ), - thumb_dir / f"{doc.id:07}.webp.gpg", - ) - - call_command("decrypt_documents") - - doc.refresh_from_db() - - self.assertEqual(doc.storage_type, Document.STORAGE_TYPE_UNENCRYPTED) - self.assertEqual(doc.filename, "0000004.pdf") - self.assertIsFile(Path(originals_dir) / "0000004.pdf") - self.assertIsFile(doc.source_path) - self.assertIsFile(Path(thumb_dir) / f"{doc.id:07}.webp") - self.assertIsFile(doc.thumbnail_path) - - with doc.source_file as f: - checksum: str = hashlib.md5(f.read()).hexdigest() - self.assertEqual(checksum, doc.checksum) - - class TestMakeIndex(TestCase): @mock.patch("documents.management.commands.document_index.index_reindex") def test_reindex(self, m): diff --git a/src/documents/tests/test_management_consumer.py b/src/documents/tests/test_management_consumer.py index 38b9eadda..810ae63e2 100644 --- a/src/documents/tests/test_management_consumer.py +++ b/src/documents/tests/test_management_consumer.py @@ -1,438 +1,1048 @@ -import filecmp +""" +Tests for the document consumer management command. + +Tests are organized into classes by component: +- TestFileStabilityTracker: Unit tests for FileStabilityTracker +- TestConsumerFilter: Unit tests for ConsumerFilter +- TestConsumeFile: Unit tests for the _consume_file function +- TestTagsFromPath: Unit tests for _tags_from_path +- TestCommandValidation: Tests for command argument validation +- TestCommandOneshot: Tests for oneshot mode +- TestCommandWatch: Integration tests for the watch loop +""" + +from __future__ import annotations + +import re import shutil from pathlib import Path from threading import Thread +from time import monotonic from time import sleep -from unittest import mock +from typing import TYPE_CHECKING -from django.conf import settings +import pytest +from django import db from django.core.management import CommandError -from django.core.management import call_command -from django.test import TransactionTestCase +from django.db import DatabaseError from django.test import override_settings +from watchfiles import Change -from documents.consumer import ConsumerError from documents.data_models import ConsumableDocument -from documents.management.commands import document_consumer +from documents.data_models import DocumentSource +from documents.management.commands.document_consumer import Command +from documents.management.commands.document_consumer import ConsumerFilter +from documents.management.commands.document_consumer import FileStabilityTracker +from documents.management.commands.document_consumer import TrackedFile +from documents.management.commands.document_consumer import _consume_file +from documents.management.commands.document_consumer import _tags_from_path from documents.models import Tag -from documents.tests.utils import DirectoriesMixin -from documents.tests.utils import DocumentConsumeDelayMixin + +if TYPE_CHECKING: + from collections.abc import Callable + from collections.abc import Generator + from unittest.mock import MagicMock + + from pytest_django.fixtures import SettingsWrapper + from pytest_mock import MockerFixture + + +@pytest.fixture +def stability_tracker() -> FileStabilityTracker: + """Create a FileStabilityTracker with a short delay for testing.""" + return FileStabilityTracker(stability_delay=0.1) + + +@pytest.fixture +def temp_file(tmp_path: Path) -> Path: + """Create a temporary file for testing.""" + file_path = tmp_path / "test_file.pdf" + file_path.write_bytes(b"test content") + return file_path + + +@pytest.fixture +def consumption_dir(tmp_path: Path) -> Path: + """Create a temporary consumption directory for testing.""" + consume_dir = tmp_path / "consume" + consume_dir.mkdir() + return consume_dir + + +@pytest.fixture +def scratch_dir(tmp_path: Path) -> Path: + """Create a temporary scratch directory for testing.""" + scratch = tmp_path / "scratch" + scratch.mkdir() + return scratch + + +@pytest.fixture +def sample_pdf(tmp_path: Path) -> Path: + """Create a sample PDF file.""" + pdf_content = b"%PDF-1.4\n%test\n1 0 obj\n<<>>\nendobj\ntrailer\n<<>>\n%%EOF" + pdf_path = tmp_path / "sample.pdf" + pdf_path.write_bytes(pdf_content) + return pdf_path + + +@pytest.fixture +def consumer_filter() -> ConsumerFilter: + """Create a ConsumerFilter for testing.""" + return ConsumerFilter( + supported_extensions=frozenset({".pdf", ".png", ".jpg"}), + ignore_patterns=[r"^custom_ignore"], + ) + + +@pytest.fixture +def mock_consume_file_delay(mocker: MockerFixture) -> MagicMock: + """Mock the consume_file.delay celery task.""" + mock_task = mocker.patch( + "documents.management.commands.document_consumer.consume_file", + ) + mock_task.delay = mocker.MagicMock() + return mock_task + + +@pytest.fixture +def mock_supported_extensions(mocker: MockerFixture) -> MagicMock: + """Mock get_supported_file_extensions to return only .pdf.""" + return mocker.patch( + "documents.management.commands.document_consumer.get_supported_file_extensions", + return_value={".pdf"}, + ) + + +def wait_for_mock_call( + mock_obj: MagicMock, + timeout_s: float = 5.0, + poll_interval_s: float = 0.1, +) -> bool: + """ + Actively wait for a mock to be called. + + Args: + mock_obj: The mock object to check (e.g., mock.delay) + timeout_s: Maximum time to wait in seconds + poll_interval_s: How often to check in seconds + + Returns: + True if mock was called within timeout, False otherwise + """ + start_time = monotonic() + while monotonic() - start_time < timeout_s: + if mock_obj.called: + return True + sleep(poll_interval_s) + return False + + +class TestTrackedFile: + """Tests for the TrackedFile dataclass.""" + + def test_update_stats_existing_file(self, temp_file: Path) -> None: + """Test update_stats succeeds for existing file.""" + tracked = TrackedFile(path=temp_file, last_event_time=monotonic()) + assert tracked.update_stats() is True + assert tracked.last_mtime is not None + assert tracked.last_size is not None + assert tracked.last_size == len(b"test content") + + def test_update_stats_nonexistent_file(self, tmp_path: Path) -> None: + """Test update_stats fails for nonexistent file.""" + tracked = TrackedFile( + path=tmp_path / "nonexistent.pdf", + last_event_time=monotonic(), + ) + assert tracked.update_stats() is False + assert tracked.last_mtime is None + assert tracked.last_size is None + + def test_is_unchanged_same_stats(self, temp_file: Path) -> None: + """Test is_unchanged returns True when stats haven't changed.""" + tracked = TrackedFile(path=temp_file, last_event_time=monotonic()) + tracked.update_stats() + assert tracked.is_unchanged() is True + + def test_is_unchanged_modified_file(self, temp_file: Path) -> None: + """Test is_unchanged returns False when file is modified.""" + tracked = TrackedFile(path=temp_file, last_event_time=monotonic()) + tracked.update_stats() + temp_file.write_bytes(b"modified content that is longer") + assert tracked.is_unchanged() is False + + def test_is_unchanged_deleted_file(self, temp_file: Path) -> None: + """Test is_unchanged returns False when file is deleted.""" + tracked = TrackedFile(path=temp_file, last_event_time=monotonic()) + tracked.update_stats() + temp_file.unlink() + assert tracked.is_unchanged() is False + + +class TestFileStabilityTracker: + """Tests for the FileStabilityTracker class.""" + + def test_track_new_file( + self, + stability_tracker: FileStabilityTracker, + temp_file: Path, + ) -> None: + """Test tracking a new file adds it to pending.""" + stability_tracker.track(temp_file, Change.added) + assert stability_tracker.pending_count == 1 + assert stability_tracker.has_pending_files() is True + + def test_track_modified_file( + self, + stability_tracker: FileStabilityTracker, + temp_file: Path, + ) -> None: + """Test tracking a modified file updates its event time.""" + stability_tracker.track(temp_file, Change.added) + sleep(0.05) + stability_tracker.track(temp_file, Change.modified) + assert stability_tracker.pending_count == 1 + + def test_track_deleted_file( + self, + stability_tracker: FileStabilityTracker, + temp_file: Path, + ) -> None: + """Test tracking a deleted file removes it from pending.""" + stability_tracker.track(temp_file, Change.added) + assert stability_tracker.pending_count == 1 + stability_tracker.track(temp_file, Change.deleted) + assert stability_tracker.pending_count == 0 + assert stability_tracker.has_pending_files() is False + + def test_track_nonexistent_file( + self, + stability_tracker: FileStabilityTracker, + tmp_path: Path, + ) -> None: + """Test tracking a nonexistent file doesn't add it.""" + nonexistent = tmp_path / "nonexistent.pdf" + stability_tracker.track(nonexistent, Change.added) + assert stability_tracker.pending_count == 0 + + def test_get_stable_files_before_delay( + self, + stability_tracker: FileStabilityTracker, + temp_file: Path, + ) -> None: + """Test get_stable_files returns nothing before delay expires.""" + stability_tracker.track(temp_file, Change.added) + stable = list(stability_tracker.get_stable_files()) + assert len(stable) == 0 + assert stability_tracker.pending_count == 1 + + def test_get_stable_files_after_delay( + self, + stability_tracker: FileStabilityTracker, + temp_file: Path, + ) -> None: + """Test get_stable_files returns file after delay expires.""" + stability_tracker.track(temp_file, Change.added) + sleep(0.15) + stable = list(stability_tracker.get_stable_files()) + assert len(stable) == 1 + assert stable[0] == temp_file + assert stability_tracker.pending_count == 0 + + def test_get_stable_files_modified_during_check( + self, + stability_tracker: FileStabilityTracker, + temp_file: Path, + ) -> None: + """Test file is not returned if modified during stability check.""" + stability_tracker.track(temp_file, Change.added) + sleep(0.12) + temp_file.write_bytes(b"modified content") + stable = list(stability_tracker.get_stable_files()) + assert len(stable) == 0 + assert stability_tracker.pending_count == 1 + + def test_get_stable_files_deleted_during_check(self, temp_file: Path) -> None: + """Test deleted file is not returned during stability check.""" + tracker = FileStabilityTracker(stability_delay=0.1) + tracker.track(temp_file, Change.added) + sleep(0.12) + temp_file.unlink() + stable = list(tracker.get_stable_files()) + assert len(stable) == 0 + assert tracker.pending_count == 0 + + def test_get_stable_files_error_during_check( + self, + temp_file: Path, + mocker: MockerFixture, + ) -> None: + """Test a file which has become inaccessible is removed from tracking""" + + mocker.patch.object(Path, "stat", side_effect=PermissionError("denied")) + + tracker = FileStabilityTracker(stability_delay=0.1) + tracker.track(temp_file, Change.added) + stable = list(tracker.get_stable_files()) + assert len(stable) == 0 + assert tracker.pending_count == 0 + + def test_multiple_files_tracking( + self, + stability_tracker: FileStabilityTracker, + tmp_path: Path, + ) -> None: + """Test tracking multiple files independently.""" + file1 = tmp_path / "file1.pdf" + file2 = tmp_path / "file2.pdf" + file1.write_bytes(b"content1") + file2.write_bytes(b"content2") + + stability_tracker.track(file1, Change.added) + sleep(0.05) + stability_tracker.track(file2, Change.added) + + assert stability_tracker.pending_count == 2 + + sleep(0.06) + stable = list(stability_tracker.get_stable_files()) + assert len(stable) == 1 + assert stable[0] == file1 + + sleep(0.06) + stable = list(stability_tracker.get_stable_files()) + assert len(stable) == 1 + assert stable[0] == file2 + + def test_track_resolves_path( + self, + stability_tracker: FileStabilityTracker, + temp_file: Path, + ) -> None: + """Test that tracking resolves paths consistently.""" + stability_tracker.track(temp_file, Change.added) + stability_tracker.track(temp_file.resolve(), Change.modified) + assert stability_tracker.pending_count == 1 + + +class TestConsumerFilter: + """Tests for the ConsumerFilter class.""" + + @pytest.mark.parametrize( + ("filename", "should_accept"), + [ + pytest.param("document.pdf", True, id="supported_pdf"), + pytest.param("image.png", True, id="supported_png"), + pytest.param("photo.jpg", True, id="supported_jpg"), + pytest.param("document.PDF", True, id="case_insensitive"), + pytest.param("document.xyz", False, id="unsupported_ext"), + pytest.param("document", False, id="no_extension"), + pytest.param(".DS_Store", False, id="ds_store"), + pytest.param(".DS_STORE", False, id="ds_store_upper"), + pytest.param("._document.pdf", False, id="macos_resource_fork"), + pytest.param("._hidden", False, id="macos_resource_no_ext"), + pytest.param("Thumbs.db", False, id="thumbs_db"), + pytest.param("desktop.ini", False, id="desktop_ini"), + pytest.param("custom_ignore_this.pdf", False, id="custom_pattern"), + pytest.param("stfolder.pdf", True, id="similar_to_ignored"), + pytest.param("my_document.pdf", True, id="normal_with_underscore"), + ], + ) + def test_file_filtering( + self, + consumer_filter: ConsumerFilter, + tmp_path: Path, + filename: str, + should_accept: bool, # noqa: FBT001 + ) -> None: + """Test filter correctly accepts or rejects files.""" + test_file = tmp_path / filename + test_file.touch() + assert consumer_filter(Change.added, str(test_file)) is should_accept + + @pytest.mark.parametrize( + ("dirname", "should_accept"), + [ + pytest.param(".stfolder", False, id="syncthing_stfolder"), + pytest.param(".stversions", False, id="syncthing_stversions"), + pytest.param("@eaDir", False, id="synology_eadir"), + pytest.param(".Spotlight-V100", False, id="macos_spotlight"), + pytest.param(".Trashes", False, id="macos_trashes"), + pytest.param("__MACOSX", False, id="macos_archive"), + pytest.param(".localized", False, id="macos_localized"), + pytest.param("documents", True, id="normal_dir"), + pytest.param("invoices", True, id="normal_dir_2"), + ], + ) + def test_directory_filtering( + self, + consumer_filter: ConsumerFilter, + tmp_path: Path, + dirname: str, + should_accept: bool, # noqa: FBT001 + ) -> None: + """Test filter correctly accepts or rejects directories.""" + test_dir = tmp_path / dirname + test_dir.mkdir() + assert consumer_filter(Change.added, str(test_dir)) is should_accept + + def test_default_patterns_are_valid_regex(self) -> None: + """Test that default patterns are valid regex.""" + for pattern in ConsumerFilter.DEFAULT_IGNORE_PATTERNS: + re.compile(pattern) + + def test_custom_ignore_dirs(self, tmp_path: Path) -> None: + """Test filter respects custom ignore_dirs.""" + filter_obj = ConsumerFilter( + supported_extensions=frozenset({".pdf"}), + ignore_dirs=["custom_ignored_dir"], + ) + + # Custom ignored directory should be rejected + custom_dir = tmp_path / "custom_ignored_dir" + custom_dir.mkdir() + assert filter_obj(Change.added, str(custom_dir)) is False + + # Normal directory should be accepted + normal_dir = tmp_path / "normal_dir" + normal_dir.mkdir() + assert filter_obj(Change.added, str(normal_dir)) is True + + # Default ignored directories should still be ignored + stfolder = tmp_path / ".stfolder" + stfolder.mkdir() + assert filter_obj(Change.added, str(stfolder)) is False + + +class TestConsumerFilterDefaults: + """Tests for ConsumerFilter with default settings.""" + + def test_filter_with_mocked_extensions( + self, + tmp_path: Path, + mocker: MockerFixture, + ) -> None: + """Test filter works when using mocked extensions from parser.""" + mocker.patch( + "documents.management.commands.document_consumer.get_supported_file_extensions", + return_value={".pdf", ".png"}, + ) + filter_obj = ConsumerFilter() + test_file = tmp_path / "document.pdf" + test_file.touch() + assert filter_obj(Change.added, str(test_file)) is True + + +class TestConsumeFile: + """Tests for the _consume_file function.""" + + def test_consume_queues_file( + self, + consumption_dir: Path, + sample_pdf: Path, + mock_consume_file_delay: MagicMock, + ) -> None: + """Test _consume_file queues a valid file.""" + target = consumption_dir / "document.pdf" + shutil.copy(sample_pdf, target) + + _consume_file( + filepath=target, + consumption_dir=consumption_dir, + subdirs_as_tags=False, + ) + + mock_consume_file_delay.delay.assert_called_once() + call_args = mock_consume_file_delay.delay.call_args + consumable_doc = call_args[0][0] + assert isinstance(consumable_doc, ConsumableDocument) + assert consumable_doc.original_file == target + assert consumable_doc.source == DocumentSource.ConsumeFolder + + def test_consume_nonexistent_file( + self, + consumption_dir: Path, + mock_consume_file_delay: MagicMock, + ) -> None: + """Test _consume_file handles nonexistent files gracefully.""" + _consume_file( + filepath=consumption_dir / "nonexistent.pdf", + consumption_dir=consumption_dir, + subdirs_as_tags=False, + ) + mock_consume_file_delay.delay.assert_not_called() + + def test_consume_directory( + self, + consumption_dir: Path, + mock_consume_file_delay: MagicMock, + ) -> None: + """Test _consume_file ignores directories.""" + subdir = consumption_dir / "subdir" + subdir.mkdir() + + _consume_file( + filepath=subdir, + consumption_dir=consumption_dir, + subdirs_as_tags=False, + ) + mock_consume_file_delay.delay.assert_not_called() + + def test_consume_with_permission_error( + self, + consumption_dir: Path, + sample_pdf: Path, + mock_consume_file_delay: MagicMock, + mocker: MockerFixture, + ) -> None: + """Test _consume_file handles permission errors.""" + target = consumption_dir / "document.pdf" + shutil.copy(sample_pdf, target) + + mocker.patch.object(Path, "is_file", side_effect=PermissionError("denied")) + _consume_file( + filepath=target, + consumption_dir=consumption_dir, + subdirs_as_tags=False, + ) + mock_consume_file_delay.delay.assert_not_called() + + def test_consume_with_tags_error( + self, + consumption_dir: Path, + sample_pdf: Path, + mock_consume_file_delay: MagicMock, + mocker: MockerFixture, + ) -> None: + """Test _consume_file handles errors during tag creation""" + target = consumption_dir / "document.pdf" + shutil.copy(sample_pdf, target) + + mocker.patch( + "documents.management.commands.document_consumer._tags_from_path", + side_effect=DatabaseError("Something happened"), + ) + + _consume_file( + filepath=target, + consumption_dir=consumption_dir, + subdirs_as_tags=True, + ) + mock_consume_file_delay.delay.assert_called_once() + call_args = mock_consume_file_delay.delay.call_args + overrides = call_args[0][1] + assert overrides.tag_ids is None + + +@pytest.mark.django_db +class TestTagsFromPath: + """Tests for the _tags_from_path function.""" + + def test_creates_tags_from_subdirectories(self, consumption_dir: Path) -> None: + """Test tags are created for each subdirectory.""" + subdir = consumption_dir / "Invoice" / "2024" + subdir.mkdir(parents=True) + target = subdir / "document.pdf" + target.touch() + + tag_ids = _tags_from_path(target, consumption_dir) + + assert len(tag_ids) == 2 + assert Tag.objects.filter(name="Invoice").exists() + assert Tag.objects.filter(name="2024").exists() + + def test_reuses_existing_tags(self, consumption_dir: Path) -> None: + """Test existing tags are reused (case-insensitive).""" + existing_tag = Tag.objects.create(name="existing") + + subdir = consumption_dir / "EXISTING" + subdir.mkdir(parents=True) + target = subdir / "document.pdf" + target.touch() + + tag_ids = _tags_from_path(target, consumption_dir) + + assert len(tag_ids) == 1 + assert existing_tag.pk in tag_ids + assert Tag.objects.filter(name__iexact="existing").count() == 1 + + def test_no_tags_for_root_file(self, consumption_dir: Path) -> None: + """Test no tags created for files directly in consumption dir.""" + target = consumption_dir / "document.pdf" + target.touch() + + tag_ids = _tags_from_path(target, consumption_dir) + + assert len(tag_ids) == 0 + + +class TestCommandValidation: + """Tests for command argument validation.""" + + def test_raises_for_missing_consumption_dir( + self, + settings: SettingsWrapper, + ) -> None: + """Test command raises error when directory is not provided.""" + settings.CONSUMPTION_DIR = None + with pytest.raises(CommandError, match="not configured"): + cmd = Command() + cmd.handle(directory=None, oneshot=True, testing=False) + + def test_raises_for_nonexistent_directory(self, tmp_path: Path) -> None: + """Test command raises error for nonexistent directory.""" + nonexistent = tmp_path / "nonexistent" + + with pytest.raises(CommandError, match="does not exist"): + cmd = Command() + cmd.handle(directory=str(nonexistent), oneshot=True, testing=False) + + def test_raises_for_file_instead_of_directory(self, sample_pdf: Path) -> None: + """Test command raises error when path is a file, not directory.""" + with pytest.raises(CommandError, match="not a directory"): + cmd = Command() + cmd.handle(directory=str(sample_pdf), oneshot=True, testing=False) + + +@pytest.mark.usefixtures("mock_supported_extensions") +class TestCommandOneshot: + """Tests for oneshot mode.""" + + def test_processes_existing_files( + self, + consumption_dir: Path, + scratch_dir: Path, + sample_pdf: Path, + mock_consume_file_delay: MagicMock, + settings: SettingsWrapper, + ) -> None: + """Test oneshot mode processes existing files.""" + target = consumption_dir / "document.pdf" + shutil.copy(sample_pdf, target) + + settings.SCRATCH_DIR = scratch_dir + settings.CONSUMER_IGNORE_PATTERNS = [] + + cmd = Command() + cmd.handle(directory=str(consumption_dir), oneshot=True, testing=False) + + mock_consume_file_delay.delay.assert_called_once() + + def test_processes_recursive( + self, + consumption_dir: Path, + scratch_dir: Path, + sample_pdf: Path, + mock_consume_file_delay: MagicMock, + settings: SettingsWrapper, + ) -> None: + """Test oneshot mode processes files recursively.""" + subdir = consumption_dir / "subdir" + subdir.mkdir() + target = subdir / "document.pdf" + shutil.copy(sample_pdf, target) + + settings.SCRATCH_DIR = scratch_dir + settings.CONSUMER_RECURSIVE = True + settings.CONSUMER_IGNORE_PATTERNS = [] + + cmd = Command() + cmd.handle(directory=str(consumption_dir), oneshot=True, testing=False) + + mock_consume_file_delay.delay.assert_called_once() + + def test_ignores_unsupported_extensions( + self, + consumption_dir: Path, + scratch_dir: Path, + mock_consume_file_delay: MagicMock, + settings: SettingsWrapper, + ) -> None: + """Test oneshot mode ignores unsupported file extensions.""" + target = consumption_dir / "document.xyz" + target.write_bytes(b"content") + + settings.SCRATCH_DIR = scratch_dir + settings.CONSUMER_IGNORE_PATTERNS = [] + + cmd = Command() + cmd.handle(directory=str(consumption_dir), oneshot=True, testing=False) + + mock_consume_file_delay.delay.assert_not_called() class ConsumerThread(Thread): - def __init__(self): + """Thread wrapper for running the consumer command with proper cleanup.""" + + def __init__( + self, + consumption_dir: Path, + scratch_dir: Path, + *, + recursive: bool = False, + subdirs_as_tags: bool = False, + polling_interval: float = 0, + stability_delay: float = 0.1, + ) -> None: super().__init__() - self.cmd = document_consumer.Command() + self.consumption_dir = consumption_dir + self.scratch_dir = scratch_dir + self.recursive = recursive + self.subdirs_as_tags = subdirs_as_tags + self.polling_interval = polling_interval + self.stability_delay = stability_delay + self.cmd = Command() self.cmd.stop_flag.clear() + # Non-daemon ensures finally block runs and connections are closed + self.daemon = False + self.exception: Exception | None = None def run(self) -> None: - self.cmd.handle(directory=settings.CONSUMPTION_DIR, oneshot=False, testing=True) + try: + # Use override_settings to avoid polluting global settings + # which would affect other tests running on the same worker + with override_settings( + SCRATCH_DIR=self.scratch_dir, + CONSUMER_RECURSIVE=self.recursive, + CONSUMER_SUBDIRS_AS_TAGS=self.subdirs_as_tags, + CONSUMER_POLLING_INTERVAL=self.polling_interval, + CONSUMER_STABILITY_DELAY=self.stability_delay, + CONSUMER_IGNORE_PATTERNS=[], + ): + self.cmd.handle( + directory=str(self.consumption_dir), + oneshot=False, + testing=True, + ) + except Exception as e: + self.exception = e + finally: + # Close database connections created in this thread + db.connections.close_all() - def stop(self): - # Consumer checks this every second. + def stop(self) -> None: self.cmd.stop_flag.set() - -def chunked(size, source): - for i in range(0, len(source), size): - yield source[i : i + size] - - -class ConsumerThreadMixin(DocumentConsumeDelayMixin): - """ - Provides a thread which runs the consumer management command at setUp - and stops it at tearDown - """ - - sample_file: Path = ( - Path(__file__).parent / Path("samples") / Path("simple.pdf") - ).resolve() - - def setUp(self) -> None: - super().setUp() - self.t = None - - def t_start(self): - self.t = ConsumerThread() - self.t.start() - # give the consumer some time to do initial work - sleep(1) - - def tearDown(self) -> None: - if self.t: - # set the stop flag - self.t.stop() - # wait for the consumer to exit. - self.t.join() - self.t = None - - super().tearDown() - - def wait_for_task_mock_call(self, expected_call_count=1): - n = 0 - while n < 50: - if self.consume_file_mock.call_count >= expected_call_count: - # give task_mock some time to finish and raise errors - sleep(1) - return - n += 1 - sleep(0.1) - - # A bogus async_task that will simply check the file for - # completeness and raise an exception otherwise. - def bogus_task( - self, - input_doc: ConsumableDocument, - overrides=None, - ): - eq = filecmp.cmp(input_doc.original_file, self.sample_file, shallow=False) - if not eq: - print("Consumed an INVALID file.") # noqa: T201 - raise ConsumerError("Incomplete File READ FAILED") - else: - print("Consumed a perfectly valid file.") # noqa: T201 - - def slow_write_file(self, target, *, incomplete=False): - with Path(self.sample_file).open("rb") as f: - pdf_bytes = f.read() - - if incomplete: - pdf_bytes = pdf_bytes[: len(pdf_bytes) - 100] - - with Path(target).open("wb") as f: - # this will take 2 seconds, since the file is about 20k. - print("Start writing file.") # noqa: T201 - for b in chunked(1000, pdf_bytes): - f.write(b) - sleep(0.1) - print("file completed.") # noqa: T201 - - -@override_settings( - CONSUMER_INOTIFY_DELAY=0.01, -) -class TestConsumer(DirectoriesMixin, ConsumerThreadMixin, TransactionTestCase): - def test_consume_file(self): - self.t_start() - - f = Path(self.dirs.consumption_dir) / "my_file.pdf" - shutil.copy(self.sample_file, f) - - self.wait_for_task_mock_call() - - self.consume_file_mock.assert_called_once() - - input_doc, _ = self.get_last_consume_delay_call_args() - - self.assertEqual(input_doc.original_file, f) - - def test_consume_file_invalid_ext(self): - self.t_start() - - f = Path(self.dirs.consumption_dir) / "my_file.wow" - shutil.copy(self.sample_file, f) - - self.wait_for_task_mock_call() - - self.consume_file_mock.assert_not_called() - - def test_consume_existing_file(self): - f = Path(self.dirs.consumption_dir) / "my_file.pdf" - shutil.copy(self.sample_file, f) - - self.t_start() - self.consume_file_mock.assert_called_once() - - input_doc, _ = self.get_last_consume_delay_call_args() - - self.assertEqual(input_doc.original_file, f) - - @mock.patch("documents.management.commands.document_consumer.logger.error") - def test_slow_write_pdf(self, error_logger): - self.consume_file_mock.side_effect = self.bogus_task - - self.t_start() - - fname = Path(self.dirs.consumption_dir) / "my_file.pdf" - - self.slow_write_file(fname) - - self.wait_for_task_mock_call() - - error_logger.assert_not_called() - - self.consume_file_mock.assert_called_once() - - input_doc, _ = self.get_last_consume_delay_call_args() - - self.assertEqual(input_doc.original_file, fname) - - @mock.patch("documents.management.commands.document_consumer.logger.error") - def test_slow_write_and_move(self, error_logger): - self.consume_file_mock.side_effect = self.bogus_task - - self.t_start() - - fname = Path(self.dirs.consumption_dir) / "my_file.~df" - fname2 = Path(self.dirs.consumption_dir) / "my_file.pdf" - - self.slow_write_file(fname) - shutil.move(fname, fname2) - - self.wait_for_task_mock_call() - - self.consume_file_mock.assert_called_once() - - input_doc, _ = self.get_last_consume_delay_call_args() - - self.assertEqual(input_doc.original_file, fname2) - - error_logger.assert_not_called() - - @mock.patch("documents.management.commands.document_consumer.logger.error") - def test_slow_write_incomplete(self, error_logger): - self.consume_file_mock.side_effect = self.bogus_task - - self.t_start() - - fname = Path(self.dirs.consumption_dir) / "my_file.pdf" - self.slow_write_file(fname, incomplete=True) - - self.wait_for_task_mock_call() - - self.consume_file_mock.assert_called_once() - - input_doc, _ = self.get_last_consume_delay_call_args() - - self.assertEqual(input_doc.original_file, fname) - - # assert that we have an error logged with this invalid file. - error_logger.assert_called_once() - - @mock.patch("documents.management.commands.document_consumer.logger.warning") - def test_permission_error_on_prechecks(self, warning_logger): - filepath = Path(self.dirs.consumption_dir) / "selinux.txt" - filepath.touch() - - original_stat = Path.stat - - def raising_stat(self, *args, **kwargs): - if self == filepath: - raise PermissionError("Permission denied") - return original_stat(self, *args, **kwargs) - - with mock.patch("pathlib.Path.stat", new=raising_stat): - document_consumer._consume(filepath) - - warning_logger.assert_called_once() - (args, _) = warning_logger.call_args - self.assertIn("Permission denied", args[0]) - self.consume_file_mock.assert_not_called() - - @override_settings(CONSUMPTION_DIR="does_not_exist") - def test_consumption_directory_invalid(self): - self.assertRaises(CommandError, call_command, "document_consumer", "--oneshot") - - @override_settings(CONSUMPTION_DIR="") - def test_consumption_directory_unset(self): - self.assertRaises(CommandError, call_command, "document_consumer", "--oneshot") - - def test_mac_write(self): - self.consume_file_mock.side_effect = self.bogus_task - - self.t_start() - - shutil.copy( - self.sample_file, - Path(self.dirs.consumption_dir) / ".DS_STORE", - ) - shutil.copy( - self.sample_file, - Path(self.dirs.consumption_dir) / "my_file.pdf", - ) - shutil.copy( - self.sample_file, - Path(self.dirs.consumption_dir) / "._my_file.pdf", - ) - shutil.copy( - self.sample_file, - Path(self.dirs.consumption_dir) / "my_second_file.pdf", - ) - shutil.copy( - self.sample_file, - Path(self.dirs.consumption_dir) / "._my_second_file.pdf", - ) - - sleep(5) - - self.wait_for_task_mock_call(expected_call_count=2) - - self.assertEqual(2, self.consume_file_mock.call_count) - - consumed_files = [] - for input_doc, _ in self.get_all_consume_delay_call_args(): - consumed_files.append(input_doc.original_file.name) - - self.assertCountEqual(consumed_files, ["my_file.pdf", "my_second_file.pdf"]) - - def test_is_ignored(self): - test_paths = [ - { - "path": str(Path(self.dirs.consumption_dir) / "foo.pdf"), - "ignore": False, - }, - { - "path": str( - Path(self.dirs.consumption_dir) / "foo" / "bar.pdf", - ), - "ignore": False, - }, - { - "path": str(Path(self.dirs.consumption_dir) / ".DS_STORE"), - "ignore": True, - }, - { - "path": str(Path(self.dirs.consumption_dir) / ".DS_Store"), - "ignore": True, - }, - { - "path": str( - Path(self.dirs.consumption_dir) / ".stfolder" / "foo.pdf", - ), - "ignore": True, - }, - { - "path": str(Path(self.dirs.consumption_dir) / ".stfolder.pdf"), - "ignore": False, - }, - { - "path": str( - Path(self.dirs.consumption_dir) / ".stversions" / "foo.pdf", - ), - "ignore": True, - }, - { - "path": str( - Path(self.dirs.consumption_dir) / ".stversions.pdf", - ), - "ignore": False, - }, - { - "path": str(Path(self.dirs.consumption_dir) / "._foo.pdf"), - "ignore": True, - }, - { - "path": str(Path(self.dirs.consumption_dir) / "my_foo.pdf"), - "ignore": False, - }, - { - "path": str( - Path(self.dirs.consumption_dir) / "._foo" / "bar.pdf", - ), - "ignore": True, - }, - { - "path": str( - Path(self.dirs.consumption_dir) - / "@eaDir" - / "SYNO@.fileindexdb" - / "_1jk.fnm", - ), - "ignore": True, - }, - ] - for test_setup in test_paths: - filepath = test_setup["path"] - expected_ignored_result = test_setup["ignore"] - self.assertEqual( - expected_ignored_result, - document_consumer._is_ignored(filepath), - f'_is_ignored("{filepath}") != {expected_ignored_result}', + def stop_and_wait(self, timeout: float = 5.0) -> None: + """Stop the thread and wait for it to finish, with cleanup.""" + self.stop() + self.join(timeout=timeout) + if self.is_alive(): + # Thread didn't stop in time - this is a test failure + raise RuntimeError( + f"Consumer thread did not stop within {timeout}s timeout", ) - @mock.patch("documents.management.commands.document_consumer.Path.open") - def test_consume_file_busy(self, open_mock): - # Calling this mock always raises this - open_mock.side_effect = OSError - self.t_start() +@pytest.fixture +def start_consumer( + consumption_dir: Path, + scratch_dir: Path, + mock_supported_extensions: MagicMock, +) -> Generator[Callable[..., ConsumerThread], None, None]: + """Start a consumer thread and ensure cleanup.""" + threads: list[ConsumerThread] = [] - f = Path(self.dirs.consumption_dir) / "my_file.pdf" - shutil.copy(self.sample_file, f) + def _start(**kwargs) -> ConsumerThread: + thread = ConsumerThread(consumption_dir, scratch_dir, **kwargs) + threads.append(thread) + thread.start() + sleep(2.0) # Give thread time to start + return thread - self.wait_for_task_mock_call() + try: + yield _start + finally: + # Cleanup all threads that were started + for thread in threads: + thread.stop_and_wait() - self.consume_file_mock.assert_not_called() + failed_threads = [] + for thread in threads: + thread.join(timeout=5.0) + if thread.is_alive(): + failed_threads.append(thread) + + # Clean up any Tags created by threads (they bypass test transaction isolation) + Tag.objects.all().delete() + + db.connections.close_all() + + if failed_threads: + pytest.fail( + f"{len(failed_threads)} consumer thread(s) did not stop within timeout", + ) -@override_settings( - CONSUMER_POLLING=1, - # please leave the delay here and down below - # see https://github.com/paperless-ngx/paperless-ngx/pull/66 - CONSUMER_POLLING_DELAY=3, - CONSUMER_POLLING_RETRY_COUNT=20, -) -class TestConsumerPolling(TestConsumer): - # just do all the tests with polling - pass +@pytest.mark.django_db +class TestCommandWatch: + """Integration tests for the watch loop.""" + + def test_detects_new_file( + self, + consumption_dir: Path, + sample_pdf: Path, + mock_consume_file_delay: MagicMock, + start_consumer: Callable[..., ConsumerThread], + ) -> None: + """Test watch mode detects and consumes new files.""" + thread = start_consumer() + + target = consumption_dir / "document.pdf" + shutil.copy(sample_pdf, target) + + wait_for_mock_call(mock_consume_file_delay.delay, timeout_s=2.0) + + if thread.exception: + raise thread.exception + + mock_consume_file_delay.delay.assert_called() + + def test_detects_moved_file( + self, + consumption_dir: Path, + scratch_dir: Path, + sample_pdf: Path, + mock_consume_file_delay: MagicMock, + start_consumer: Callable[..., ConsumerThread], + ) -> None: + """Test watch mode detects moved/renamed files.""" + temp_location = scratch_dir / "temp.pdf" + shutil.copy(sample_pdf, temp_location) + + thread = start_consumer() + + sleep(0.5) + + target = consumption_dir / "document.pdf" + shutil.move(temp_location, target) + + wait_for_mock_call(mock_consume_file_delay.delay, timeout_s=2.0) + + if thread.exception: + raise thread.exception + + mock_consume_file_delay.delay.assert_called() + + def test_handles_slow_write( + self, + consumption_dir: Path, + sample_pdf: Path, + mock_consume_file_delay: MagicMock, + start_consumer: Callable[..., ConsumerThread], + ) -> None: + """Test watch mode waits for slow writes to complete.""" + pdf_bytes = sample_pdf.read_bytes() + + thread = start_consumer(stability_delay=0.2) + + target = consumption_dir / "document.pdf" + with target.open("wb") as f: + for i in range(0, len(pdf_bytes), 100): + f.write(pdf_bytes[i : i + 100]) + f.flush() + sleep(0.05) + + wait_for_mock_call(mock_consume_file_delay.delay, timeout_s=2.0) + + if thread.exception: + raise thread.exception + + mock_consume_file_delay.delay.assert_called() + + def test_ignores_macos_files( + self, + consumption_dir: Path, + sample_pdf: Path, + mock_consume_file_delay: MagicMock, + start_consumer: Callable[..., ConsumerThread], + ) -> None: + """Test watch mode ignores macOS system files.""" + thread = start_consumer() + + (consumption_dir / ".DS_Store").write_bytes(b"test") + (consumption_dir / "._document.pdf").write_bytes(b"test") + shutil.copy(sample_pdf, consumption_dir / "valid.pdf") + + wait_for_mock_call(mock_consume_file_delay.delay, timeout_s=2.0) + + if thread.exception: + raise thread.exception + + assert mock_consume_file_delay.delay.call_count == 1 + call_args = mock_consume_file_delay.delay.call_args[0][0] + assert call_args.original_file.name == "valid.pdf" + + @pytest.mark.django_db + @pytest.mark.usefixtures("mock_supported_extensions") + def test_stop_flag_stops_consumer( + self, + consumption_dir: Path, + scratch_dir: Path, + mock_consume_file_delay: MagicMock, + ) -> None: + """Test stop flag properly stops the consumer.""" + thread = ConsumerThread(consumption_dir, scratch_dir) + try: + thread.start() + sleep(0.3) + assert thread.is_alive() + finally: + thread.stop_and_wait(timeout=5.0) + # Clean up any Tags created by the thread + Tag.objects.all().delete() + + assert not thread.is_alive() -@override_settings(CONSUMER_INOTIFY_DELAY=0.01, CONSUMER_RECURSIVE=True) -class TestConsumerRecursive(TestConsumer): - # just do all the tests with recursive - pass +@pytest.mark.django_db +class TestCommandWatchPolling: + """Tests for polling mode.""" + + def test_polling_mode_works( + self, + consumption_dir: Path, + sample_pdf: Path, + mock_consume_file_delay: MagicMock, + start_consumer: Callable[..., ConsumerThread], + ) -> None: + """ + Test polling mode detects files. + + Uses active waiting with timeout to handle CI delays and polling timing. + """ + # Use shorter polling interval for faster test + thread = start_consumer(polling_interval=0.5, stability_delay=0.1) + + target = consumption_dir / "document.pdf" + shutil.copy(sample_pdf, target) + + # Actively wait for consumption + # Polling needs: interval (0.5s) + stability (0.1s) + next poll (0.5s) + margin + wait_for_mock_call(mock_consume_file_delay.delay, timeout_s=5.0) + + if thread.exception: + raise thread.exception + + mock_consume_file_delay.delay.assert_called() -@override_settings( - CONSUMER_RECURSIVE=True, - CONSUMER_POLLING=1, - CONSUMER_POLLING_DELAY=3, - CONSUMER_POLLING_RETRY_COUNT=20, -) -class TestConsumerRecursivePolling(TestConsumer): - # just do all the tests with polling and recursive - pass +@pytest.mark.django_db +class TestCommandWatchRecursive: + """Tests for recursive watching.""" + + def test_recursive_detects_nested_files( + self, + consumption_dir: Path, + sample_pdf: Path, + mock_consume_file_delay: MagicMock, + start_consumer: Callable[..., ConsumerThread], + ) -> None: + """Test recursive mode detects files in subdirectories.""" + subdir = consumption_dir / "level1" / "level2" + subdir.mkdir(parents=True) + + thread = start_consumer(recursive=True) + + target = subdir / "document.pdf" + shutil.copy(sample_pdf, target) + + wait_for_mock_call(mock_consume_file_delay.delay, timeout_s=2.0) + + if thread.exception: + raise thread.exception + + mock_consume_file_delay.delay.assert_called() + + def test_subdirs_as_tags( + self, + consumption_dir: Path, + sample_pdf: Path, + mock_consume_file_delay: MagicMock, + start_consumer: Callable[..., ConsumerThread], + mocker: MockerFixture, + ) -> None: + """Test subdirs_as_tags creates tags from directory names.""" + # Mock _tags_from_path to avoid database operations in the consumer thread + mock_tags = mocker.patch( + "documents.management.commands.document_consumer._tags_from_path", + return_value=[1, 2], + ) + + subdir = consumption_dir / "Invoices" / "2024" + subdir.mkdir(parents=True) + + thread = start_consumer(recursive=True, subdirs_as_tags=True) + + target = subdir / "document.pdf" + shutil.copy(sample_pdf, target) + + wait_for_mock_call(mock_consume_file_delay.delay, timeout_s=2.0) + + if thread.exception: + raise thread.exception + + mock_consume_file_delay.delay.assert_called() + mock_tags.assert_called() + call_args = mock_consume_file_delay.delay.call_args + overrides = call_args[0][1] + assert overrides.tag_ids is not None + assert len(overrides.tag_ids) == 2 -class TestConsumerTags(DirectoriesMixin, ConsumerThreadMixin, TransactionTestCase): - @override_settings(CONSUMER_RECURSIVE=True, CONSUMER_SUBDIRS_AS_TAGS=True) - def test_consume_file_with_path_tags(self): - tag_names = ("existingTag", "Space Tag") - # Create a Tag prior to consuming a file using it in path - tag_ids = [ - Tag.objects.create(name="existingtag").pk, - ] +@pytest.mark.django_db +class TestCommandWatchEdgeCases: + """Tests for edge cases and error handling.""" - self.t_start() + def test_handles_deleted_before_stable( + self, + consumption_dir: Path, + sample_pdf: Path, + mock_consume_file_delay: MagicMock, + start_consumer: Callable[..., ConsumerThread], + ) -> None: + """Test handles files deleted before becoming stable.""" + thread = start_consumer(stability_delay=0.3) - path = Path(self.dirs.consumption_dir) / "/".join(tag_names) - path.mkdir(parents=True, exist_ok=True) - f = path / "my_file.pdf" - # Wait at least inotify read_delay for recursive watchers - # to be created for the new directories - sleep(1) - shutil.copy(self.sample_file, f) + target = consumption_dir / "document.pdf" + shutil.copy(sample_pdf, target) + sleep(0.1) + target.unlink() - self.wait_for_task_mock_call() + sleep(0.5) - self.consume_file_mock.assert_called_once() + if thread.exception: + raise thread.exception - # Add the pk of the Tag created by _consume() - tag_ids.append(Tag.objects.get(name=tag_names[1]).pk) + mock_consume_file_delay.delay.assert_not_called() - input_doc, overrides = self.get_last_consume_delay_call_args() + @pytest.mark.usefixtures("mock_supported_extensions") + def test_handles_task_exception( + self, + consumption_dir: Path, + scratch_dir: Path, + sample_pdf: Path, + mocker: MockerFixture, + ) -> None: + """Test handles exceptions from consume task gracefully.""" + mock_task = mocker.patch( + "documents.management.commands.document_consumer.consume_file", + ) + mock_task.delay.side_effect = Exception("Task error") - self.assertEqual(input_doc.original_file, f) + thread = ConsumerThread(consumption_dir, scratch_dir) + try: + thread.start() + sleep(0.3) - # assertCountEqual has a bad name, but test that the first - # sequence contains the same elements as second, regardless of - # their order. - self.assertCountEqual(overrides.tag_ids, tag_ids) + target = consumption_dir / "document.pdf" + shutil.copy(sample_pdf, target) + sleep(0.5) - @override_settings( - CONSUMER_POLLING=1, - CONSUMER_POLLING_DELAY=3, - CONSUMER_POLLING_RETRY_COUNT=20, - ) - def test_consume_file_with_path_tags_polling(self): - self.test_consume_file_with_path_tags() + # Consumer should still be running despite the exception + assert thread.is_alive() + finally: + thread.stop_and_wait(timeout=5.0) + # Clean up any Tags created by the thread + Tag.objects.all().delete() diff --git a/src/documents/tests/test_management_exporter.py b/src/documents/tests/test_management_exporter.py index b01b8d47e..c2a1360ca 100644 --- a/src/documents/tests/test_management_exporter.py +++ b/src/documents/tests/test_management_exporter.py @@ -86,9 +86,8 @@ class TestExportImport( content="Content", checksum="82186aaa94f0b98697d704b90fd1c072", title="wow_dec", - filename="0000004.pdf.gpg", + filename="0000004.pdf", mime_type="application/pdf", - storage_type=Document.STORAGE_TYPE_GPG, ) self.note = Note.objects.create( @@ -242,10 +241,9 @@ class TestExportImport( checksum = hashlib.md5(f.read()).hexdigest() self.assertEqual(checksum, element["fields"]["checksum"]) - self.assertEqual( - element["fields"]["storage_type"], - Document.STORAGE_TYPE_UNENCRYPTED, - ) + # Generated field "content_length" should not be exported, + # it is automatically computed during import. + self.assertNotIn("content_length", element["fields"]) if document_exporter.EXPORTER_ARCHIVE_NAME in element: fname = ( @@ -436,7 +434,7 @@ class TestExportImport( Document.objects.create( checksum="AAAAAAAAAAAAAAAAA", title="wow", - filename="0000004.pdf", + filename="0000010.pdf", mime_type="application/pdf", ) self.assertRaises(FileNotFoundError, call_command, "document_exporter", target) diff --git a/src/documents/tests/test_management_superuser.py b/src/documents/tests/test_management_superuser.py index 01f03c8e1..343d5f568 100644 --- a/src/documents/tests/test_management_superuser.py +++ b/src/documents/tests/test_management_superuser.py @@ -33,8 +33,7 @@ class TestManageSuperUser(DirectoriesMixin, TestCase): # just the consumer user which is created # during migration, and AnonymousUser - self.assertEqual(User.objects.count(), 2) - self.assertTrue(User.objects.filter(username="consumer").exists()) + self.assertEqual(User.objects.count(), 1) self.assertEqual(User.objects.filter(is_superuser=True).count(), 0) self.assertEqual( out, @@ -54,7 +53,7 @@ class TestManageSuperUser(DirectoriesMixin, TestCase): # count is 3 as there's the consumer # user already created during migration, and AnonymousUser user: User = User.objects.get_by_natural_key("admin") - self.assertEqual(User.objects.count(), 3) + self.assertEqual(User.objects.count(), 2) self.assertTrue(user.is_superuser) self.assertEqual(user.email, "root@localhost") self.assertEqual(out, 'Created superuser "admin" with provided password.\n') @@ -71,7 +70,7 @@ class TestManageSuperUser(DirectoriesMixin, TestCase): out = self.call_command(environ={"PAPERLESS_ADMIN_PASSWORD": "123456"}) - self.assertEqual(User.objects.count(), 3) + self.assertEqual(User.objects.count(), 2) with self.assertRaises(User.DoesNotExist): User.objects.get_by_natural_key("admin") self.assertEqual( @@ -92,7 +91,7 @@ class TestManageSuperUser(DirectoriesMixin, TestCase): out = self.call_command(environ={"PAPERLESS_ADMIN_PASSWORD": "123456"}) - self.assertEqual(User.objects.count(), 3) + self.assertEqual(User.objects.count(), 2) user: User = User.objects.get_by_natural_key("admin") self.assertTrue(user.check_password("password")) self.assertEqual(out, "Did not create superuser, a user admin already exists\n") @@ -111,7 +110,7 @@ class TestManageSuperUser(DirectoriesMixin, TestCase): out = self.call_command(environ={"PAPERLESS_ADMIN_PASSWORD": "123456"}) - self.assertEqual(User.objects.count(), 3) + self.assertEqual(User.objects.count(), 2) user: User = User.objects.get_by_natural_key("admin") self.assertTrue(user.check_password("password")) self.assertFalse(user.is_superuser) @@ -150,7 +149,7 @@ class TestManageSuperUser(DirectoriesMixin, TestCase): ) user: User = User.objects.get_by_natural_key("admin") - self.assertEqual(User.objects.count(), 3) + self.assertEqual(User.objects.count(), 2) self.assertTrue(user.is_superuser) self.assertEqual(user.email, "hello@world.com") self.assertEqual(user.username, "admin") @@ -174,7 +173,7 @@ class TestManageSuperUser(DirectoriesMixin, TestCase): ) user: User = User.objects.get_by_natural_key("super") - self.assertEqual(User.objects.count(), 3) + self.assertEqual(User.objects.count(), 2) self.assertTrue(user.is_superuser) self.assertEqual(user.email, "hello@world.com") self.assertEqual(user.username, "super") diff --git a/src/documents/tests/test_migration_archive_files.py b/src/documents/tests/test_migration_archive_files.py deleted file mode 100644 index a2e8a5f8f..000000000 --- a/src/documents/tests/test_migration_archive_files.py +++ /dev/null @@ -1,574 +0,0 @@ -import hashlib -import importlib -import shutil -from pathlib import Path -from unittest import mock - -import pytest -from django.conf import settings -from django.test import override_settings - -from documents.parsers import ParseError -from documents.tests.utils import DirectoriesMixin -from documents.tests.utils import FileSystemAssertsMixin -from documents.tests.utils import TestMigrations - -STORAGE_TYPE_GPG = "gpg" - -migration_1012_obj = importlib.import_module( - "documents.migrations.1012_fix_archive_files", -) - - -def archive_name_from_filename(filename: Path) -> Path: - return Path(filename.stem + ".pdf") - - -def archive_path_old(self) -> Path: - if self.filename: - fname = archive_name_from_filename(Path(self.filename)) - else: - fname = Path(f"{self.pk:07}.pdf") - - return Path(settings.ARCHIVE_DIR) / fname - - -def archive_path_new(doc): - if doc.archive_filename is not None: - return Path(settings.ARCHIVE_DIR) / str(doc.archive_filename) - else: - return None - - -def source_path(doc): - if doc.filename: - fname = str(doc.filename) - else: - fname = f"{doc.pk:07}{doc.file_type}" - if doc.storage_type == STORAGE_TYPE_GPG: - fname += ".gpg" # pragma: no cover - - return Path(settings.ORIGINALS_DIR) / fname - - -def thumbnail_path(doc): - file_name = f"{doc.pk:07}.png" - if doc.storage_type == STORAGE_TYPE_GPG: - file_name += ".gpg" - - return Path(settings.THUMBNAIL_DIR) / file_name - - -def make_test_document( - document_class, - title: str, - mime_type: str, - original: str, - original_filename: str, - archive: str | None = None, - archive_filename: str | None = None, -): - doc = document_class() - doc.filename = original_filename - doc.title = title - doc.mime_type = mime_type - doc.content = "the content, does not matter for this test" - doc.save() - - shutil.copy2(original, source_path(doc)) - with Path(original).open("rb") as f: - doc.checksum = hashlib.md5(f.read()).hexdigest() - - if archive: - if archive_filename: - doc.archive_filename = archive_filename - shutil.copy2(archive, archive_path_new(doc)) - else: - shutil.copy2(archive, archive_path_old(doc)) - - with Path(archive).open("rb") as f: - doc.archive_checksum = hashlib.md5(f.read()).hexdigest() - - doc.save() - - Path(thumbnail_path(doc)).touch() - - return doc - - -simple_jpg = Path(__file__).parent / "samples" / "simple.jpg" -simple_pdf = Path(__file__).parent / "samples" / "simple.pdf" -simple_pdf2 = ( - Path(__file__).parent / "samples" / "documents" / "originals" / "0000002.pdf" -) -simple_pdf3 = ( - Path(__file__).parent / "samples" / "documents" / "originals" / "0000003.pdf" -) -simple_txt = Path(__file__).parent / "samples" / "simple.txt" -simple_png = Path(__file__).parent / "samples" / "simple-noalpha.png" -simple_png2 = Path(__file__).parent / "examples" / "no-text.png" - - -@override_settings(FILENAME_FORMAT="") -class TestMigrateArchiveFiles(DirectoriesMixin, FileSystemAssertsMixin, TestMigrations): - migrate_from = "1006_auto_20201208_2209_squashed_1011_auto_20210101_2340" - migrate_to = "1012_fix_archive_files" - - def setUpBeforeMigration(self, apps): - Document = apps.get_model("documents", "Document") - - self.unrelated = make_test_document( - Document, - "unrelated", - "application/pdf", - simple_pdf3, - "unrelated.pdf", - simple_pdf, - ) - self.no_text = make_test_document( - Document, - "no-text", - "image/png", - simple_png2, - "no-text.png", - simple_pdf, - ) - self.doc_no_archive = make_test_document( - Document, - "no_archive", - "text/plain", - simple_txt, - "no_archive.txt", - ) - self.clash1 = make_test_document( - Document, - "clash", - "application/pdf", - simple_pdf, - "clash.pdf", - simple_pdf, - ) - self.clash2 = make_test_document( - Document, - "clash", - "image/jpeg", - simple_jpg, - "clash.jpg", - simple_pdf, - ) - self.clash3 = make_test_document( - Document, - "clash", - "image/png", - simple_png, - "clash.png", - simple_pdf, - ) - self.clash4 = make_test_document( - Document, - "clash.png", - "application/pdf", - simple_pdf2, - "clash.png.pdf", - simple_pdf2, - ) - - self.assertEqual(archive_path_old(self.clash1), archive_path_old(self.clash2)) - self.assertEqual(archive_path_old(self.clash1), archive_path_old(self.clash3)) - self.assertNotEqual( - archive_path_old(self.clash1), - archive_path_old(self.clash4), - ) - - def testArchiveFilesMigrated(self): - Document = self.apps.get_model("documents", "Document") - - for doc in Document.objects.all(): - if doc.archive_checksum: - self.assertIsNotNone(doc.archive_filename) - self.assertIsFile(archive_path_new(doc)) - else: - self.assertIsNone(doc.archive_filename) - - with Path(source_path(doc)).open("rb") as f: - original_checksum = hashlib.md5(f.read()).hexdigest() - self.assertEqual(original_checksum, doc.checksum) - - if doc.archive_checksum: - self.assertIsFile(archive_path_new(doc)) - with archive_path_new(doc).open("rb") as f: - archive_checksum = hashlib.md5(f.read()).hexdigest() - self.assertEqual(archive_checksum, doc.archive_checksum) - - self.assertEqual( - Document.objects.filter(archive_checksum__isnull=False).count(), - 6, - ) - - def test_filenames(self): - Document = self.apps.get_model("documents", "Document") - self.assertEqual( - Document.objects.get(id=self.unrelated.id).archive_filename, - "unrelated.pdf", - ) - self.assertEqual( - Document.objects.get(id=self.no_text.id).archive_filename, - "no-text.pdf", - ) - self.assertEqual( - Document.objects.get(id=self.doc_no_archive.id).archive_filename, - None, - ) - self.assertEqual( - Document.objects.get(id=self.clash1.id).archive_filename, - f"{self.clash1.id:07}.pdf", - ) - self.assertEqual( - Document.objects.get(id=self.clash2.id).archive_filename, - f"{self.clash2.id:07}.pdf", - ) - self.assertEqual( - Document.objects.get(id=self.clash3.id).archive_filename, - f"{self.clash3.id:07}.pdf", - ) - self.assertEqual( - Document.objects.get(id=self.clash4.id).archive_filename, - "clash.png.pdf", - ) - - -@override_settings(FILENAME_FORMAT="{correspondent}/{title}") -class TestMigrateArchiveFilesWithFilenameFormat(TestMigrateArchiveFiles): - def test_filenames(self): - Document = self.apps.get_model("documents", "Document") - self.assertEqual( - Document.objects.get(id=self.unrelated.id).archive_filename, - "unrelated.pdf", - ) - self.assertEqual( - Document.objects.get(id=self.no_text.id).archive_filename, - "no-text.pdf", - ) - self.assertEqual( - Document.objects.get(id=self.doc_no_archive.id).archive_filename, - None, - ) - self.assertEqual( - Document.objects.get(id=self.clash1.id).archive_filename, - "none/clash.pdf", - ) - self.assertEqual( - Document.objects.get(id=self.clash2.id).archive_filename, - "none/clash_01.pdf", - ) - self.assertEqual( - Document.objects.get(id=self.clash3.id).archive_filename, - "none/clash_02.pdf", - ) - self.assertEqual( - Document.objects.get(id=self.clash4.id).archive_filename, - "clash.png.pdf", - ) - - -def fake_parse_wrapper(parser, path, mime_type, file_name): - parser.archive_path = None - parser.text = "the text" - - -@override_settings(FILENAME_FORMAT="") -class TestMigrateArchiveFilesErrors(DirectoriesMixin, TestMigrations): - migrate_from = "1006_auto_20201208_2209_squashed_1011_auto_20210101_2340" - migrate_to = "1012_fix_archive_files" - auto_migrate = False - - @pytest.mark.skip(reason="Fails with migration tearDown util. Needs investigation.") - def test_archive_missing(self): - Document = self.apps.get_model("documents", "Document") - - doc = make_test_document( - Document, - "clash", - "application/pdf", - simple_pdf, - "clash.pdf", - simple_pdf, - ) - archive_path_old(doc).unlink() - - self.assertRaisesMessage( - ValueError, - "does not exist at: ", - self.performMigration, - ) - - @pytest.mark.skip(reason="Fails with migration tearDown util. Needs investigation.") - def test_parser_missing(self): - Document = self.apps.get_model("documents", "Document") - - make_test_document( - Document, - "document", - "invalid/typesss768", - simple_png, - "document.png", - simple_pdf, - ) - make_test_document( - Document, - "document", - "invalid/typesss768", - simple_jpg, - "document.jpg", - simple_pdf, - ) - - self.assertRaisesMessage( - ValueError, - "no parsers are available", - self.performMigration, - ) - - @mock.patch(f"{__name__}.migration_1012_obj.parse_wrapper") - def test_parser_error(self, m): - m.side_effect = ParseError() - Document = self.apps.get_model("documents", "Document") - - doc1 = make_test_document( - Document, - "document", - "image/png", - simple_png, - "document.png", - simple_pdf, - ) - doc2 = make_test_document( - Document, - "document", - "application/pdf", - simple_jpg, - "document.jpg", - simple_pdf, - ) - - self.assertIsNotNone(doc1.archive_checksum) - self.assertIsNotNone(doc2.archive_checksum) - - with self.assertLogs() as capture: - self.performMigration() - - self.assertEqual(m.call_count, 6) - - self.assertEqual( - len( - list( - filter( - lambda log: "Parse error, will try again in 5 seconds" in log, - capture.output, - ), - ), - ), - 4, - ) - - self.assertEqual( - len( - list( - filter( - lambda log: "Unable to regenerate archive document for ID:" - in log, - capture.output, - ), - ), - ), - 2, - ) - - Document = self.apps.get_model("documents", "Document") - - doc1 = Document.objects.get(id=doc1.id) - doc2 = Document.objects.get(id=doc2.id) - - self.assertIsNone(doc1.archive_checksum) - self.assertIsNone(doc2.archive_checksum) - self.assertIsNone(doc1.archive_filename) - self.assertIsNone(doc2.archive_filename) - - @mock.patch(f"{__name__}.migration_1012_obj.parse_wrapper") - def test_parser_no_archive(self, m): - m.side_effect = fake_parse_wrapper - - Document = self.apps.get_model("documents", "Document") - - doc1 = make_test_document( - Document, - "document", - "image/png", - simple_png, - "document.png", - simple_pdf, - ) - doc2 = make_test_document( - Document, - "document", - "application/pdf", - simple_jpg, - "document.jpg", - simple_pdf, - ) - - with self.assertLogs() as capture: - self.performMigration() - - self.assertEqual( - len( - list( - filter( - lambda log: "Parser did not return an archive document for document" - in log, - capture.output, - ), - ), - ), - 2, - ) - - Document = self.apps.get_model("documents", "Document") - - doc1 = Document.objects.get(id=doc1.id) - doc2 = Document.objects.get(id=doc2.id) - - self.assertIsNone(doc1.archive_checksum) - self.assertIsNone(doc2.archive_checksum) - self.assertIsNone(doc1.archive_filename) - self.assertIsNone(doc2.archive_filename) - - -@override_settings(FILENAME_FORMAT="") -class TestMigrateArchiveFilesBackwards( - DirectoriesMixin, - FileSystemAssertsMixin, - TestMigrations, -): - migrate_from = "1012_fix_archive_files" - migrate_to = "1006_auto_20201208_2209_squashed_1011_auto_20210101_2340" - - def setUpBeforeMigration(self, apps): - Document = apps.get_model("documents", "Document") - - make_test_document( - Document, - "unrelated", - "application/pdf", - simple_pdf2, - "unrelated.txt", - simple_pdf2, - "unrelated.pdf", - ) - make_test_document( - Document, - "no_archive", - "text/plain", - simple_txt, - "no_archive.txt", - ) - make_test_document( - Document, - "clash", - "image/jpeg", - simple_jpg, - "clash.jpg", - simple_pdf, - "clash_02.pdf", - ) - - def testArchiveFilesReverted(self): - Document = self.apps.get_model("documents", "Document") - - for doc in Document.objects.all(): - if doc.archive_checksum: - self.assertIsFile(archive_path_old(doc)) - with Path(source_path(doc)).open("rb") as f: - original_checksum = hashlib.md5(f.read()).hexdigest() - self.assertEqual(original_checksum, doc.checksum) - - if doc.archive_checksum: - self.assertIsFile(archive_path_old(doc)) - with archive_path_old(doc).open("rb") as f: - archive_checksum = hashlib.md5(f.read()).hexdigest() - self.assertEqual(archive_checksum, doc.archive_checksum) - - self.assertEqual( - Document.objects.filter(archive_checksum__isnull=False).count(), - 2, - ) - - -@override_settings(FILENAME_FORMAT="{correspondent}/{title}") -class TestMigrateArchiveFilesBackwardsWithFilenameFormat( - TestMigrateArchiveFilesBackwards, -): - pass - - -@override_settings(FILENAME_FORMAT="") -class TestMigrateArchiveFilesBackwardsErrors(DirectoriesMixin, TestMigrations): - migrate_from = "1012_fix_archive_files" - migrate_to = "1006_auto_20201208_2209_squashed_1011_auto_20210101_2340" - auto_migrate = False - - def test_filename_clash(self): - Document = self.apps.get_model("documents", "Document") - - self.clashA = make_test_document( - Document, - "clash", - "application/pdf", - simple_pdf, - "clash.pdf", - simple_pdf, - "clash_02.pdf", - ) - self.clashB = make_test_document( - Document, - "clash", - "image/jpeg", - simple_jpg, - "clash.jpg", - simple_pdf, - "clash_01.pdf", - ) - - self.assertRaisesMessage( - ValueError, - "would clash with another archive filename", - self.performMigration, - ) - - def test_filename_exists(self): - Document = self.apps.get_model("documents", "Document") - - self.clashA = make_test_document( - Document, - "clash", - "application/pdf", - simple_pdf, - "clash.pdf", - simple_pdf, - "clash.pdf", - ) - self.clashB = make_test_document( - Document, - "clash", - "image/jpeg", - simple_jpg, - "clash.jpg", - simple_pdf, - "clash_01.pdf", - ) - - self.assertRaisesMessage( - ValueError, - "file already exists.", - self.performMigration, - ) diff --git a/src/documents/tests/test_migration_consumption_templates.py b/src/documents/tests/test_migration_consumption_templates.py deleted file mode 100644 index 917007116..000000000 --- a/src/documents/tests/test_migration_consumption_templates.py +++ /dev/null @@ -1,50 +0,0 @@ -from django.contrib.auth import get_user_model - -from documents.tests.utils import TestMigrations - - -class TestMigrateConsumptionTemplate(TestMigrations): - migrate_from = "1038_sharelink" - migrate_to = "1039_consumptiontemplate" - - def setUpBeforeMigration(self, apps): - User = get_user_model() - Group = apps.get_model("auth.Group") - self.Permission = apps.get_model("auth", "Permission") - self.user = User.objects.create(username="user1") - self.group = Group.objects.create(name="group1") - permission = self.Permission.objects.get(codename="add_document") - self.user.user_permissions.add(permission.id) - self.group.permissions.add(permission.id) - - def test_users_with_add_documents_get_add_consumptiontemplate(self): - permission = self.Permission.objects.get(codename="add_consumptiontemplate") - self.assertTrue(self.user.has_perm(f"documents.{permission.codename}")) - self.assertTrue(permission in self.group.permissions.all()) - - -class TestReverseMigrateConsumptionTemplate(TestMigrations): - migrate_from = "1039_consumptiontemplate" - migrate_to = "1038_sharelink" - - def setUpBeforeMigration(self, apps): - User = get_user_model() - Group = apps.get_model("auth.Group") - self.Permission = apps.get_model("auth", "Permission") - self.user = User.objects.create(username="user1") - self.group = Group.objects.create(name="group1") - permission = self.Permission.objects.filter( - codename="add_consumptiontemplate", - ).first() - if permission is not None: - self.user.user_permissions.add(permission.id) - self.group.permissions.add(permission.id) - - def test_remove_consumptiontemplate_permissions(self): - permission = self.Permission.objects.filter( - codename="add_consumptiontemplate", - ).first() - # can be None ? now that CTs removed - if permission is not None: - self.assertFalse(self.user.has_perm(f"documents.{permission.codename}")) - self.assertFalse(permission in self.group.permissions.all()) diff --git a/src/documents/tests/test_migration_created.py b/src/documents/tests/test_migration_created.py deleted file mode 100644 index 89e97cbe1..000000000 --- a/src/documents/tests/test_migration_created.py +++ /dev/null @@ -1,33 +0,0 @@ -from datetime import date -from datetime import datetime -from datetime import timedelta - -from django.utils.timezone import make_aware -from pytz import UTC - -from documents.tests.utils import DirectoriesMixin -from documents.tests.utils import TestMigrations - - -class TestMigrateDocumentCreated(DirectoriesMixin, TestMigrations): - migrate_from = "1066_alter_workflowtrigger_schedule_offset_days" - migrate_to = "1067_alter_document_created" - - def setUpBeforeMigration(self, apps): - # create 600 documents - for i in range(600): - Document = apps.get_model("documents", "Document") - naive = datetime(2023, 10, 1, 12, 0, 0) + timedelta(days=i) - Document.objects.create( - title=f"test{i}", - mime_type="application/pdf", - filename=f"file{i}.pdf", - created=make_aware(naive, timezone=UTC), - checksum=i, - ) - - def testDocumentCreatedMigrated(self): - Document = self.apps.get_model("documents", "Document") - - doc = Document.objects.get(id=1) - self.assertEqual(doc.created, date(2023, 10, 1)) diff --git a/src/documents/tests/test_migration_custom_field_selects.py b/src/documents/tests/test_migration_custom_field_selects.py deleted file mode 100644 index 59004bf21..000000000 --- a/src/documents/tests/test_migration_custom_field_selects.py +++ /dev/null @@ -1,87 +0,0 @@ -from unittest.mock import ANY - -from documents.tests.utils import TestMigrations - - -class TestMigrateCustomFieldSelects(TestMigrations): - migrate_from = "1059_workflowactionemail_workflowactionwebhook_and_more" - migrate_to = "1060_alter_customfieldinstance_value_select" - - def setUpBeforeMigration(self, apps): - CustomField = apps.get_model("documents.CustomField") - self.old_format = CustomField.objects.create( - name="cf1", - data_type="select", - extra_data={"select_options": ["Option 1", "Option 2", "Option 3"]}, - ) - Document = apps.get_model("documents.Document") - doc = Document.objects.create(title="doc1") - CustomFieldInstance = apps.get_model("documents.CustomFieldInstance") - self.old_instance = CustomFieldInstance.objects.create( - field=self.old_format, - value_select=0, - document=doc, - ) - - def test_migrate_old_to_new_select_fields(self): - self.old_format.refresh_from_db() - self.old_instance.refresh_from_db() - - self.assertEqual( - self.old_format.extra_data["select_options"], - [ - {"label": "Option 1", "id": ANY}, - {"label": "Option 2", "id": ANY}, - {"label": "Option 3", "id": ANY}, - ], - ) - - self.assertEqual( - self.old_instance.value_select, - self.old_format.extra_data["select_options"][0]["id"], - ) - - -class TestMigrationCustomFieldSelectsReverse(TestMigrations): - migrate_from = "1060_alter_customfieldinstance_value_select" - migrate_to = "1059_workflowactionemail_workflowactionwebhook_and_more" - - def setUpBeforeMigration(self, apps): - CustomField = apps.get_model("documents.CustomField") - self.new_format = CustomField.objects.create( - name="cf1", - data_type="select", - extra_data={ - "select_options": [ - {"label": "Option 1", "id": "id1"}, - {"label": "Option 2", "id": "id2"}, - {"label": "Option 3", "id": "id3"}, - ], - }, - ) - Document = apps.get_model("documents.Document") - doc = Document.objects.create(title="doc1") - CustomFieldInstance = apps.get_model("documents.CustomFieldInstance") - self.new_instance = CustomFieldInstance.objects.create( - field=self.new_format, - value_select="id1", - document=doc, - ) - - def test_migrate_new_to_old_select_fields(self): - self.new_format.refresh_from_db() - self.new_instance.refresh_from_db() - - self.assertEqual( - self.new_format.extra_data["select_options"], - [ - "Option 1", - "Option 2", - "Option 3", - ], - ) - - self.assertEqual( - self.new_instance.value_select, - 0, - ) diff --git a/src/documents/tests/test_migration_customfields.py b/src/documents/tests/test_migration_customfields.py deleted file mode 100644 index 79308bceb..000000000 --- a/src/documents/tests/test_migration_customfields.py +++ /dev/null @@ -1,43 +0,0 @@ -from django.contrib.auth import get_user_model - -from documents.tests.utils import TestMigrations - - -class TestMigrateCustomFields(TestMigrations): - migrate_from = "1039_consumptiontemplate" - migrate_to = "1040_customfield_customfieldinstance_and_more" - - def setUpBeforeMigration(self, apps): - User = get_user_model() - Group = apps.get_model("auth.Group") - self.Permission = apps.get_model("auth", "Permission") - self.user = User.objects.create(username="user1") - self.group = Group.objects.create(name="group1") - permission = self.Permission.objects.get(codename="add_document") - self.user.user_permissions.add(permission.id) - self.group.permissions.add(permission.id) - - def test_users_with_add_documents_get_add_customfields(self): - permission = self.Permission.objects.get(codename="add_customfield") - self.assertTrue(self.user.has_perm(f"documents.{permission.codename}")) - self.assertTrue(permission in self.group.permissions.all()) - - -class TestReverseMigrateCustomFields(TestMigrations): - migrate_from = "1040_customfield_customfieldinstance_and_more" - migrate_to = "1039_consumptiontemplate" - - def setUpBeforeMigration(self, apps): - User = get_user_model() - Group = apps.get_model("auth.Group") - self.Permission = apps.get_model("auth", "Permission") - self.user = User.objects.create(username="user1") - self.group = Group.objects.create(name="group1") - permission = self.Permission.objects.get(codename="add_customfield") - self.user.user_permissions.add(permission.id) - self.group.permissions.add(permission.id) - - def test_remove_consumptiontemplate_permissions(self): - permission = self.Permission.objects.get(codename="add_customfield") - self.assertFalse(self.user.has_perm(f"documents.{permission.codename}")) - self.assertFalse(permission in self.group.permissions.all()) diff --git a/src/documents/tests/test_migration_document_pages_count.py b/src/documents/tests/test_migration_document_pages_count.py deleted file mode 100644 index e8f297acb..000000000 --- a/src/documents/tests/test_migration_document_pages_count.py +++ /dev/null @@ -1,59 +0,0 @@ -import shutil -from pathlib import Path - -from django.conf import settings - -from documents.tests.utils import DirectoriesMixin -from documents.tests.utils import TestMigrations - - -def source_path_before(self) -> Path: - if self.filename: - fname = str(self.filename) - - return Path(settings.ORIGINALS_DIR) / fname - - -class TestMigrateDocumentPageCount(DirectoriesMixin, TestMigrations): - migrate_from = "1052_document_transaction_id" - migrate_to = "1053_document_page_count" - - def setUpBeforeMigration(self, apps): - Document = apps.get_model("documents", "Document") - doc = Document.objects.create( - title="test1", - mime_type="application/pdf", - filename="file1.pdf", - ) - self.doc_id = doc.id - shutil.copy( - Path(__file__).parent / "samples" / "simple.pdf", - source_path_before(doc), - ) - - def testDocumentPageCountMigrated(self): - Document = self.apps.get_model("documents", "Document") - - doc = Document.objects.get(id=self.doc_id) - self.assertEqual(doc.page_count, 1) - - -class TestMigrateDocumentPageCountBackwards(TestMigrations): - migrate_from = "1053_document_page_count" - migrate_to = "1052_document_transaction_id" - - def setUpBeforeMigration(self, apps): - Document = apps.get_model("documents", "Document") - doc = Document.objects.create( - title="test1", - mime_type="application/pdf", - filename="file1.pdf", - page_count=8, - ) - self.doc_id = doc.id - - def test_remove_number_of_pages_to_page_count(self): - Document = self.apps.get_model("documents", "Document") - self.assertFalse( - "page_count" in [field.name for field in Document._meta.get_fields()], - ) diff --git a/src/documents/tests/test_migration_encrypted_webp_conversion.py b/src/documents/tests/test_migration_encrypted_webp_conversion.py deleted file mode 100644 index 0660df368..000000000 --- a/src/documents/tests/test_migration_encrypted_webp_conversion.py +++ /dev/null @@ -1,283 +0,0 @@ -import importlib -import shutil -import tempfile -from collections.abc import Callable -from collections.abc import Iterable -from pathlib import Path -from unittest import mock - -from django.test import override_settings - -from documents.tests.utils import TestMigrations - -# https://github.com/python/cpython/issues/100950 -migration_1037_obj = importlib.import_module( - "documents.migrations.1037_webp_encrypted_thumbnail_conversion", -) - - -@override_settings(PASSPHRASE="test") -@mock.patch( - f"{__name__}.migration_1037_obj.multiprocessing.pool.Pool.map", -) -@mock.patch(f"{__name__}.migration_1037_obj.run_convert") -class TestMigrateToEncrytpedWebPThumbnails(TestMigrations): - migrate_from = ( - "1022_paperlesstask_squashed_1036_alter_savedviewfilterrule_rule_type" - ) - migrate_to = "1037_webp_encrypted_thumbnail_conversion" - auto_migrate = False - - def pretend_convert_output(self, *args, **kwargs): - """ - Pretends to do the conversion, by copying the input file - to the output file - """ - shutil.copy2( - Path(kwargs["input_file"].rstrip("[0]")), - Path(kwargs["output_file"]), - ) - - def pretend_map(self, func: Callable, iterable: Iterable): - """ - Pretends to be the map of a multiprocessing.Pool, but secretly does - everything in series - """ - for item in iterable: - func(item) - - def create_dummy_thumbnails( - self, - thumb_dir: Path, - ext: str, - count: int, - start_count: int = 0, - ): - """ - Helper to create a certain count of files of given extension in a given directory - """ - for idx in range(count): - (Path(thumb_dir) / Path(f"{start_count + idx:07}.{ext}")).touch() - # Triple check expected files exist - self.assert_file_count_by_extension(ext, thumb_dir, count) - - def create_webp_thumbnail_files( - self, - thumb_dir: Path, - count: int, - start_count: int = 0, - ): - """ - Creates a dummy WebP thumbnail file in the given directory, based on - the database Document - """ - self.create_dummy_thumbnails(thumb_dir, "webp", count, start_count) - - def create_encrypted_webp_thumbnail_files( - self, - thumb_dir: Path, - count: int, - start_count: int = 0, - ): - """ - Creates a dummy encrypted WebP thumbnail file in the given directory, based on - the database Document - """ - self.create_dummy_thumbnails(thumb_dir, "webp.gpg", count, start_count) - - def create_png_thumbnail_files( - self, - thumb_dir: Path, - count: int, - start_count: int = 0, - ): - """ - Creates a dummy PNG thumbnail file in the given directory, based on - the database Document - """ - - self.create_dummy_thumbnails(thumb_dir, "png", count, start_count) - - def create_encrypted_png_thumbnail_files( - self, - thumb_dir: Path, - count: int, - start_count: int = 0, - ): - """ - Creates a dummy encrypted PNG thumbnail file in the given directory, based on - the database Document - """ - - self.create_dummy_thumbnails(thumb_dir, "png.gpg", count, start_count) - - def assert_file_count_by_extension( - self, - ext: str, - dir: str | Path, - expected_count: int, - ): - """ - Helper to assert a certain count of given extension files in given directory - """ - if not isinstance(dir, Path): - dir = Path(dir) - matching_files = list(dir.glob(f"*.{ext}")) - self.assertEqual(len(matching_files), expected_count) - - def assert_encrypted_png_file_count(self, dir: Path, expected_count: int): - """ - Helper to assert a certain count of excrypted PNG extension files in given directory - """ - self.assert_file_count_by_extension("png.gpg", dir, expected_count) - - def assert_encrypted_webp_file_count(self, dir: Path, expected_count: int): - """ - Helper to assert a certain count of encrypted WebP extension files in given directory - """ - self.assert_file_count_by_extension("webp.gpg", dir, expected_count) - - def assert_webp_file_count(self, dir: Path, expected_count: int): - """ - Helper to assert a certain count of WebP extension files in given directory - """ - self.assert_file_count_by_extension("webp", dir, expected_count) - - def assert_png_file_count(self, dir: Path, expected_count: int): - """ - Helper to assert a certain count of PNG extension files in given directory - """ - self.assert_file_count_by_extension("png", dir, expected_count) - - def setUp(self): - self.thumbnail_dir = Path(tempfile.mkdtemp()).resolve() - - return super().setUp() - - def tearDown(self) -> None: - shutil.rmtree(self.thumbnail_dir) - - return super().tearDown() - - def test_do_nothing_if_converted( - self, - run_convert_mock: mock.MagicMock, - map_mock: mock.MagicMock, - ): - """ - GIVEN: - - Encrypted document exists with existing encrypted WebP thumbnail path - WHEN: - - Migration is attempted - THEN: - - Nothing is converted - """ - map_mock.side_effect = self.pretend_map - - with override_settings( - THUMBNAIL_DIR=self.thumbnail_dir, - ): - self.create_encrypted_webp_thumbnail_files(self.thumbnail_dir, 3) - - self.performMigration() - run_convert_mock.assert_not_called() - - self.assert_encrypted_webp_file_count(self.thumbnail_dir, 3) - - def test_convert_thumbnails( - self, - run_convert_mock: mock.MagicMock, - map_mock: mock.MagicMock, - ): - """ - GIVEN: - - Encrypted documents exist with PNG thumbnail - WHEN: - - Migration is attempted - THEN: - - Thumbnails are converted to webp & re-encrypted - """ - map_mock.side_effect = self.pretend_map - run_convert_mock.side_effect = self.pretend_convert_output - - with override_settings( - THUMBNAIL_DIR=self.thumbnail_dir, - ): - self.create_encrypted_png_thumbnail_files(self.thumbnail_dir, 3) - - self.performMigration() - - run_convert_mock.assert_called() - self.assertEqual(run_convert_mock.call_count, 3) - - self.assert_encrypted_webp_file_count(self.thumbnail_dir, 3) - - def test_convert_errors_out( - self, - run_convert_mock: mock.MagicMock, - map_mock: mock.MagicMock, - ): - """ - GIVEN: - - Encrypted document exists with PNG thumbnail - WHEN: - - Migration is attempted, but raises an exception - THEN: - - Single thumbnail is converted - """ - map_mock.side_effect = self.pretend_map - run_convert_mock.side_effect = OSError - - with override_settings( - THUMBNAIL_DIR=self.thumbnail_dir, - ): - self.create_encrypted_png_thumbnail_files(self.thumbnail_dir, 3) - - self.performMigration() - - run_convert_mock.assert_called() - self.assertEqual(run_convert_mock.call_count, 3) - - self.assert_encrypted_png_file_count(self.thumbnail_dir, 3) - - def test_convert_mixed( - self, - run_convert_mock: mock.MagicMock, - map_mock: mock.MagicMock, - ): - """ - GIVEN: - - Documents exist with PNG, encrypted PNG and WebP thumbnails - WHEN: - - Migration is attempted - THEN: - - Only encrypted PNG thumbnails are converted - """ - map_mock.side_effect = self.pretend_map - run_convert_mock.side_effect = self.pretend_convert_output - - with override_settings( - THUMBNAIL_DIR=self.thumbnail_dir, - ): - self.create_png_thumbnail_files(self.thumbnail_dir, 3) - self.create_encrypted_png_thumbnail_files( - self.thumbnail_dir, - 3, - start_count=3, - ) - self.create_webp_thumbnail_files(self.thumbnail_dir, 2, start_count=6) - self.create_encrypted_webp_thumbnail_files( - self.thumbnail_dir, - 3, - start_count=8, - ) - - self.performMigration() - - run_convert_mock.assert_called() - self.assertEqual(run_convert_mock.call_count, 3) - - self.assert_png_file_count(self.thumbnail_dir, 3) - self.assert_encrypted_webp_file_count(self.thumbnail_dir, 6) - self.assert_webp_file_count(self.thumbnail_dir, 2) - self.assert_encrypted_png_file_count(self.thumbnail_dir, 0) diff --git a/src/documents/tests/test_migration_mime_type.py b/src/documents/tests/test_migration_mime_type.py deleted file mode 100644 index 7805799fe..000000000 --- a/src/documents/tests/test_migration_mime_type.py +++ /dev/null @@ -1,108 +0,0 @@ -import shutil -from pathlib import Path - -from django.conf import settings -from django.test import override_settings - -from documents.parsers import get_default_file_extension -from documents.tests.utils import DirectoriesMixin -from documents.tests.utils import TestMigrations - -STORAGE_TYPE_UNENCRYPTED = "unencrypted" -STORAGE_TYPE_GPG = "gpg" - - -def source_path_before(self): - if self.filename: - fname = str(self.filename) - else: - fname = f"{self.pk:07}.{self.file_type}" - if self.storage_type == STORAGE_TYPE_GPG: - fname += ".gpg" - - return Path(settings.ORIGINALS_DIR) / fname - - -def file_type_after(self): - return get_default_file_extension(self.mime_type) - - -def source_path_after(doc): - if doc.filename: - fname = str(doc.filename) - else: - fname = f"{doc.pk:07}{file_type_after(doc)}" - if doc.storage_type == STORAGE_TYPE_GPG: - fname += ".gpg" # pragma: no cover - - return Path(settings.ORIGINALS_DIR) / fname - - -@override_settings(PASSPHRASE="test") -class TestMigrateMimeType(DirectoriesMixin, TestMigrations): - migrate_from = "1002_auto_20201111_1105" - migrate_to = "1003_mime_types" - - def setUpBeforeMigration(self, apps): - Document = apps.get_model("documents", "Document") - doc = Document.objects.create( - title="test", - file_type="pdf", - filename="file1.pdf", - ) - self.doc_id = doc.id - shutil.copy( - Path(__file__).parent / "samples" / "simple.pdf", - source_path_before(doc), - ) - - doc2 = Document.objects.create( - checksum="B", - file_type="pdf", - storage_type=STORAGE_TYPE_GPG, - ) - self.doc2_id = doc2.id - shutil.copy( - ( - Path(__file__).parent - / "samples" - / "documents" - / "originals" - / "0000004.pdf.gpg" - ), - source_path_before(doc2), - ) - - def testMimeTypesMigrated(self): - Document = self.apps.get_model("documents", "Document") - - doc = Document.objects.get(id=self.doc_id) - self.assertEqual(doc.mime_type, "application/pdf") - - doc2 = Document.objects.get(id=self.doc2_id) - self.assertEqual(doc2.mime_type, "application/pdf") - - -@override_settings(PASSPHRASE="test") -class TestMigrateMimeTypeBackwards(DirectoriesMixin, TestMigrations): - migrate_from = "1003_mime_types" - migrate_to = "1002_auto_20201111_1105" - - def setUpBeforeMigration(self, apps): - Document = apps.get_model("documents", "Document") - doc = Document.objects.create( - title="test", - mime_type="application/pdf", - filename="file1.pdf", - ) - self.doc_id = doc.id - shutil.copy( - Path(__file__).parent / "samples" / "simple.pdf", - source_path_after(doc), - ) - - def testMimeTypesReverted(self): - Document = self.apps.get_model("documents", "Document") - - doc = Document.objects.get(id=self.doc_id) - self.assertEqual(doc.file_type, "pdf") diff --git a/src/documents/tests/test_migration_remove_null_characters.py b/src/documents/tests/test_migration_remove_null_characters.py deleted file mode 100644 index c47bc80ca..000000000 --- a/src/documents/tests/test_migration_remove_null_characters.py +++ /dev/null @@ -1,15 +0,0 @@ -from documents.tests.utils import DirectoriesMixin -from documents.tests.utils import TestMigrations - - -class TestMigrateNullCharacters(DirectoriesMixin, TestMigrations): - migrate_from = "1014_auto_20210228_1614" - migrate_to = "1015_remove_null_characters" - - def setUpBeforeMigration(self, apps): - Document = apps.get_model("documents", "Document") - self.doc = Document.objects.create(content="aaa\0bbb") - - def testMimeTypesMigrated(self): - Document = self.apps.get_model("documents", "Document") - self.assertNotIn("\0", Document.objects.get(id=self.doc.id).content) diff --git a/src/documents/tests/test_migration_share_link_bundle.py b/src/documents/tests/test_migration_share_link_bundle.py new file mode 100644 index 000000000..74aeca14c --- /dev/null +++ b/src/documents/tests/test_migration_share_link_bundle.py @@ -0,0 +1,51 @@ +from documents.tests.utils import TestMigrations + + +class TestMigrateShareLinkBundlePermissions(TestMigrations): + migrate_from = "0007_document_content_length" + migrate_to = "0008_sharelinkbundle" + + def setUpBeforeMigration(self, apps): + User = apps.get_model("auth", "User") + Group = apps.get_model("auth", "Group") + self.Permission = apps.get_model("auth", "Permission") + self.user = User.objects.create(username="user1") + self.group = Group.objects.create(name="group1") + add_document = self.Permission.objects.get(codename="add_document") + self.user.user_permissions.add(add_document.id) + self.group.permissions.add(add_document.id) + + def test_share_link_permissions_granted_to_add_document_holders(self): + share_perms = self.Permission.objects.filter( + codename__contains="sharelinkbundle", + ) + self.assertTrue(self.user.user_permissions.filter(pk__in=share_perms).exists()) + self.assertTrue(self.group.permissions.filter(pk__in=share_perms).exists()) + + +class TestReverseMigrateShareLinkBundlePermissions(TestMigrations): + migrate_from = "0008_sharelinkbundle" + migrate_to = "0007_document_content_length" + + def setUpBeforeMigration(self, apps): + User = apps.get_model("auth", "User") + Group = apps.get_model("auth", "Group") + self.Permission = apps.get_model("auth", "Permission") + self.user = User.objects.create(username="user1") + self.group = Group.objects.create(name="group1") + add_document = self.Permission.objects.get(codename="add_document") + share_perms = self.Permission.objects.filter( + codename__contains="sharelinkbundle", + ) + self.share_perm_ids = list(share_perms.values_list("id", flat=True)) + + self.user.user_permissions.add(add_document.id, *self.share_perm_ids) + self.group.permissions.add(add_document.id, *self.share_perm_ids) + + def test_share_link_permissions_revoked_on_reverse(self): + self.assertFalse( + self.user.user_permissions.filter(pk__in=self.share_perm_ids).exists(), + ) + self.assertFalse( + self.group.permissions.filter(pk__in=self.share_perm_ids).exists(), + ) diff --git a/src/documents/tests/test_migration_storage_path_template.py b/src/documents/tests/test_migration_storage_path_template.py deleted file mode 100644 index 37b87a115..000000000 --- a/src/documents/tests/test_migration_storage_path_template.py +++ /dev/null @@ -1,30 +0,0 @@ -from documents.models import StoragePath -from documents.tests.utils import TestMigrations - - -class TestMigrateStoragePathToTemplate(TestMigrations): - migrate_from = "1054_customfieldinstance_value_monetary_amount_and_more" - migrate_to = "1055_alter_storagepath_path" - - def setUpBeforeMigration(self, apps): - self.old_format = StoragePath.objects.create( - name="sp1", - path="Something/{title}", - ) - self.new_format = StoragePath.objects.create( - name="sp2", - path="{{asn}}/{{title}}", - ) - self.no_formatting = StoragePath.objects.create( - name="sp3", - path="Some/Fixed/Path", - ) - - def test_migrate_old_to_new_storage_path(self): - self.old_format.refresh_from_db() - self.new_format.refresh_from_db() - self.no_formatting.refresh_from_db() - - self.assertEqual(self.old_format.path, "Something/{{ title }}") - self.assertEqual(self.new_format.path, "{{asn}}/{{title}}") - self.assertEqual(self.no_formatting.path, "Some/Fixed/Path") diff --git a/src/documents/tests/test_migration_tag_colors.py b/src/documents/tests/test_migration_tag_colors.py deleted file mode 100644 index 0643fe883..000000000 --- a/src/documents/tests/test_migration_tag_colors.py +++ /dev/null @@ -1,36 +0,0 @@ -from documents.tests.utils import DirectoriesMixin -from documents.tests.utils import TestMigrations - - -class TestMigrateTagColor(DirectoriesMixin, TestMigrations): - migrate_from = "1012_fix_archive_files" - migrate_to = "1013_migrate_tag_colour" - - def setUpBeforeMigration(self, apps): - Tag = apps.get_model("documents", "Tag") - self.t1_id = Tag.objects.create(name="tag1").id - self.t2_id = Tag.objects.create(name="tag2", colour=1).id - self.t3_id = Tag.objects.create(name="tag3", colour=5).id - - def testMimeTypesMigrated(self): - Tag = self.apps.get_model("documents", "Tag") - self.assertEqual(Tag.objects.get(id=self.t1_id).color, "#a6cee3") - self.assertEqual(Tag.objects.get(id=self.t2_id).color, "#a6cee3") - self.assertEqual(Tag.objects.get(id=self.t3_id).color, "#fb9a99") - - -class TestMigrateTagColorBackwards(DirectoriesMixin, TestMigrations): - migrate_from = "1013_migrate_tag_colour" - migrate_to = "1012_fix_archive_files" - - def setUpBeforeMigration(self, apps): - Tag = apps.get_model("documents", "Tag") - self.t1_id = Tag.objects.create(name="tag1").id - self.t2_id = Tag.objects.create(name="tag2", color="#cab2d6").id - self.t3_id = Tag.objects.create(name="tag3", color="#123456").id - - def testMimeTypesReverted(self): - Tag = self.apps.get_model("documents", "Tag") - self.assertEqual(Tag.objects.get(id=self.t1_id).colour, 1) - self.assertEqual(Tag.objects.get(id=self.t2_id).colour, 9) - self.assertEqual(Tag.objects.get(id=self.t3_id).colour, 1) diff --git a/src/documents/tests/test_migration_webp_conversion.py b/src/documents/tests/test_migration_webp_conversion.py deleted file mode 100644 index cd148ed6f..000000000 --- a/src/documents/tests/test_migration_webp_conversion.py +++ /dev/null @@ -1,230 +0,0 @@ -import importlib -import shutil -import tempfile -from collections.abc import Callable -from collections.abc import Iterable -from pathlib import Path -from unittest import mock - -from django.test import override_settings - -from documents.tests.utils import TestMigrations - -# https://github.com/python/cpython/issues/100950 -migration_1021_obj = importlib.import_module( - "documents.migrations.1021_webp_thumbnail_conversion", -) - - -@mock.patch( - f"{__name__}.migration_1021_obj.multiprocessing.pool.Pool.map", -) -@mock.patch(f"{__name__}.migration_1021_obj.run_convert") -class TestMigrateWebPThumbnails(TestMigrations): - migrate_from = "1016_auto_20210317_1351_squashed_1020_merge_20220518_1839" - migrate_to = "1021_webp_thumbnail_conversion" - auto_migrate = False - - def pretend_convert_output(self, *args, **kwargs): - """ - Pretends to do the conversion, by copying the input file - to the output file - """ - shutil.copy2( - Path(kwargs["input_file"].rstrip("[0]")), - Path(kwargs["output_file"]), - ) - - def pretend_map(self, func: Callable, iterable: Iterable): - """ - Pretends to be the map of a multiprocessing.Pool, but secretly does - everything in series - """ - for item in iterable: - func(item) - - def create_dummy_thumbnails( - self, - thumb_dir: Path, - ext: str, - count: int, - start_count: int = 0, - ): - """ - Helper to create a certain count of files of given extension in a given directory - """ - for idx in range(count): - (Path(thumb_dir) / Path(f"{start_count + idx:07}.{ext}")).touch() - # Triple check expected files exist - self.assert_file_count_by_extension(ext, thumb_dir, count) - - def create_webp_thumbnail_files( - self, - thumb_dir: Path, - count: int, - start_count: int = 0, - ): - """ - Creates a dummy WebP thumbnail file in the given directory, based on - the database Document - """ - self.create_dummy_thumbnails(thumb_dir, "webp", count, start_count) - - def create_png_thumbnail_file( - self, - thumb_dir: Path, - count: int, - start_count: int = 0, - ): - """ - Creates a dummy PNG thumbnail file in the given directory, based on - the database Document - """ - self.create_dummy_thumbnails(thumb_dir, "png", count, start_count) - - def assert_file_count_by_extension( - self, - ext: str, - dir: str | Path, - expected_count: int, - ): - """ - Helper to assert a certain count of given extension files in given directory - """ - if not isinstance(dir, Path): - dir = Path(dir) - matching_files = list(dir.glob(f"*.{ext}")) - self.assertEqual(len(matching_files), expected_count) - - def assert_png_file_count(self, dir: Path, expected_count: int): - """ - Helper to assert a certain count of PNG extension files in given directory - """ - self.assert_file_count_by_extension("png", dir, expected_count) - - def assert_webp_file_count(self, dir: Path, expected_count: int): - """ - Helper to assert a certain count of WebP extension files in given directory - """ - self.assert_file_count_by_extension("webp", dir, expected_count) - - def setUp(self): - self.thumbnail_dir = Path(tempfile.mkdtemp()).resolve() - - return super().setUp() - - def tearDown(self) -> None: - shutil.rmtree(self.thumbnail_dir) - - return super().tearDown() - - def test_do_nothing_if_converted( - self, - run_convert_mock: mock.MagicMock, - map_mock: mock.MagicMock, - ): - """ - GIVEN: - - Document exists with default WebP thumbnail path - WHEN: - - Thumbnail conversion is attempted - THEN: - - Nothing is converted - """ - map_mock.side_effect = self.pretend_map - - with override_settings( - THUMBNAIL_DIR=self.thumbnail_dir, - ): - self.create_webp_thumbnail_files(self.thumbnail_dir, 3) - - self.performMigration() - run_convert_mock.assert_not_called() - - self.assert_webp_file_count(self.thumbnail_dir, 3) - - def test_convert_single_thumbnail( - self, - run_convert_mock: mock.MagicMock, - map_mock: mock.MagicMock, - ): - """ - GIVEN: - - Document exists with PNG thumbnail - WHEN: - - Thumbnail conversion is attempted - THEN: - - Single thumbnail is converted - """ - map_mock.side_effect = self.pretend_map - run_convert_mock.side_effect = self.pretend_convert_output - - with override_settings( - THUMBNAIL_DIR=self.thumbnail_dir, - ): - self.create_png_thumbnail_file(self.thumbnail_dir, 3) - - self.performMigration() - - run_convert_mock.assert_called() - self.assertEqual(run_convert_mock.call_count, 3) - - self.assert_webp_file_count(self.thumbnail_dir, 3) - - def test_convert_errors_out( - self, - run_convert_mock: mock.MagicMock, - map_mock: mock.MagicMock, - ): - """ - GIVEN: - - Document exists with PNG thumbnail - WHEN: - - Thumbnail conversion is attempted, but raises an exception - THEN: - - Single thumbnail is converted - """ - map_mock.side_effect = self.pretend_map - run_convert_mock.side_effect = OSError - - with override_settings( - THUMBNAIL_DIR=self.thumbnail_dir, - ): - self.create_png_thumbnail_file(self.thumbnail_dir, 3) - - self.performMigration() - - run_convert_mock.assert_called() - self.assertEqual(run_convert_mock.call_count, 3) - - self.assert_png_file_count(self.thumbnail_dir, 3) - - def test_convert_mixed( - self, - run_convert_mock: mock.MagicMock, - map_mock: mock.MagicMock, - ): - """ - GIVEN: - - Document exists with PNG thumbnail - WHEN: - - Thumbnail conversion is attempted, but raises an exception - THEN: - - Single thumbnail is converted - """ - map_mock.side_effect = self.pretend_map - run_convert_mock.side_effect = self.pretend_convert_output - - with override_settings( - THUMBNAIL_DIR=self.thumbnail_dir, - ): - self.create_png_thumbnail_file(self.thumbnail_dir, 3) - self.create_webp_thumbnail_files(self.thumbnail_dir, 2, start_count=3) - - self.performMigration() - - run_convert_mock.assert_called() - self.assertEqual(run_convert_mock.call_count, 3) - - self.assert_png_file_count(self.thumbnail_dir, 0) - self.assert_webp_file_count(self.thumbnail_dir, 5) diff --git a/src/documents/tests/test_migration_workflows.py b/src/documents/tests/test_migration_workflows.py deleted file mode 100644 index 60e429d68..000000000 --- a/src/documents/tests/test_migration_workflows.py +++ /dev/null @@ -1,134 +0,0 @@ -from documents.data_models import DocumentSource -from documents.tests.utils import TestMigrations - - -class TestMigrateWorkflow(TestMigrations): - migrate_from = "1043_alter_savedviewfilterrule_rule_type" - migrate_to = "1044_workflow_workflowaction_workflowtrigger_and_more" - dependencies = ( - ( - "paperless_mail", - "0029_mailrule_pdf_layout", - ), - ) - - def setUpBeforeMigration(self, apps): - User = apps.get_model("auth", "User") - Group = apps.get_model("auth", "Group") - self.Permission = apps.get_model("auth", "Permission") - self.user = User.objects.create(username="user1") - self.group = Group.objects.create(name="group1") - permission = self.Permission.objects.get(codename="add_document") - self.user.user_permissions.add(permission.id) - self.group.permissions.add(permission.id) - - # create a CT to migrate - c = apps.get_model("documents", "Correspondent").objects.create( - name="Correspondent Name", - ) - dt = apps.get_model("documents", "DocumentType").objects.create( - name="DocType Name", - ) - t1 = apps.get_model("documents", "Tag").objects.create(name="t1") - sp = apps.get_model("documents", "StoragePath").objects.create(path="/test/") - cf1 = apps.get_model("documents", "CustomField").objects.create( - name="Custom Field 1", - data_type="string", - ) - ma = apps.get_model("paperless_mail", "MailAccount").objects.create( - name="MailAccount 1", - ) - mr = apps.get_model("paperless_mail", "MailRule").objects.create( - name="MailRule 1", - order=0, - account=ma, - ) - - user2 = User.objects.create(username="user2") - user3 = User.objects.create(username="user3") - group2 = Group.objects.create(name="group2") - - ConsumptionTemplate = apps.get_model("documents", "ConsumptionTemplate") - - ct = ConsumptionTemplate.objects.create( - name="Template 1", - order=0, - sources=f"{DocumentSource.ApiUpload},{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}", - filter_filename="*simple*", - filter_path="*/samples/*", - filter_mailrule=mr, - assign_title="Doc from {correspondent}", - assign_correspondent=c, - assign_document_type=dt, - assign_storage_path=sp, - assign_owner=user2, - ) - - ct.assign_tags.add(t1) - ct.assign_view_users.add(user3) - ct.assign_view_groups.add(group2) - ct.assign_change_users.add(user3) - ct.assign_change_groups.add(group2) - ct.assign_custom_fields.add(cf1) - ct.save() - - def test_users_with_add_documents_get_add_and_workflow_templates_get_migrated(self): - permission = self.Permission.objects.get(codename="add_workflow") - self.assertTrue(permission in self.user.user_permissions.all()) - self.assertTrue(permission in self.group.permissions.all()) - - Workflow = self.apps.get_model("documents", "Workflow") - self.assertEqual(Workflow.objects.all().count(), 1) - - -class TestReverseMigrateWorkflow(TestMigrations): - migrate_from = "1044_workflow_workflowaction_workflowtrigger_and_more" - migrate_to = "1043_alter_savedviewfilterrule_rule_type" - - def setUpBeforeMigration(self, apps): - User = apps.get_model("auth", "User") - Group = apps.get_model("auth", "Group") - self.Permission = apps.get_model("auth", "Permission") - self.user = User.objects.create(username="user1") - self.group = Group.objects.create(name="group1") - permission = self.Permission.objects.filter( - codename="add_workflow", - ).first() - if permission is not None: - self.user.user_permissions.add(permission.id) - self.group.permissions.add(permission.id) - - Workflow = apps.get_model("documents", "Workflow") - WorkflowTrigger = apps.get_model("documents", "WorkflowTrigger") - WorkflowAction = apps.get_model("documents", "WorkflowAction") - - trigger = WorkflowTrigger.objects.create( - type=0, - sources=[str(DocumentSource.ConsumeFolder)], - filter_path="*/path/*", - filter_filename="*file*", - ) - - action = WorkflowAction.objects.create( - assign_title="assign title", - ) - workflow = Workflow.objects.create( - name="workflow 1", - order=0, - ) - workflow.triggers.set([trigger]) - workflow.actions.set([action]) - workflow.save() - - def test_remove_workflow_permissions_and_migrate_workflows_to_consumption_templates( - self, - ): - permission = self.Permission.objects.filter( - codename="add_workflow", - ).first() - if permission is not None: - self.assertFalse(permission in self.user.user_permissions.all()) - self.assertFalse(permission in self.group.permissions.all()) - - ConsumptionTemplate = self.apps.get_model("documents", "ConsumptionTemplate") - self.assertEqual(ConsumptionTemplate.objects.all().count(), 1) diff --git a/src/documents/tests/test_share_link_bundles.py b/src/documents/tests/test_share_link_bundles.py new file mode 100644 index 000000000..f7deed5c9 --- /dev/null +++ b/src/documents/tests/test_share_link_bundles.py @@ -0,0 +1,536 @@ +from __future__ import annotations + +import zipfile +from datetime import timedelta +from pathlib import Path +from unittest import mock + +from django.conf import settings +from django.contrib.auth.models import User +from django.utils import timezone +from rest_framework import serializers +from rest_framework import status +from rest_framework.test import APITestCase + +from documents.filters import ShareLinkBundleFilterSet +from documents.models import ShareLink +from documents.models import ShareLinkBundle +from documents.serialisers import ShareLinkBundleSerializer +from documents.tasks import build_share_link_bundle +from documents.tasks import cleanup_expired_share_link_bundles +from documents.tests.factories import DocumentFactory +from documents.tests.utils import DirectoriesMixin + + +class ShareLinkBundleAPITests(DirectoriesMixin, APITestCase): + ENDPOINT = "/api/share_link_bundles/" + + def setUp(self): + super().setUp() + self.user = User.objects.create_superuser(username="bundle_admin") + self.client.force_authenticate(self.user) + self.document = DocumentFactory.create() + + @mock.patch("documents.views.build_share_link_bundle.delay") + def test_create_bundle_triggers_build_job(self, delay_mock): + payload = { + "document_ids": [self.document.pk], + "file_version": ShareLink.FileVersion.ARCHIVE, + "expiration_days": 7, + } + + response = self.client.post(self.ENDPOINT, payload, format="json") + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + bundle = ShareLinkBundle.objects.get(pk=response.data["id"]) + self.assertEqual(bundle.documents.count(), 1) + self.assertEqual(bundle.status, ShareLinkBundle.Status.PENDING) + delay_mock.assert_called_once_with(bundle.pk) + + def test_create_bundle_rejects_missing_documents(self): + payload = { + "document_ids": [9999], + "file_version": ShareLink.FileVersion.ARCHIVE, + "expiration_days": 7, + } + + response = self.client.post(self.ENDPOINT, payload, format="json") + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("document_ids", response.data) + + @mock.patch("documents.views.has_perms_owner_aware", return_value=False) + def test_create_bundle_rejects_insufficient_permissions(self, perms_mock): + payload = { + "document_ids": [self.document.pk], + "file_version": ShareLink.FileVersion.ARCHIVE, + "expiration_days": 7, + } + + response = self.client.post(self.ENDPOINT, payload, format="json") + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("document_ids", response.data) + perms_mock.assert_called() + + @mock.patch("documents.views.build_share_link_bundle.delay") + def test_rebuild_bundle_resets_state(self, delay_mock): + bundle = ShareLinkBundle.objects.create( + slug="rebuild-slug", + file_version=ShareLink.FileVersion.ARCHIVE, + status=ShareLinkBundle.Status.FAILED, + ) + bundle.documents.set([self.document]) + bundle.last_error = {"message": "Something went wrong"} + bundle.size_bytes = 100 + bundle.file_path = "path/to/file.zip" + bundle.save() + + response = self.client.post(f"{self.ENDPOINT}{bundle.pk}/rebuild/") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + bundle.refresh_from_db() + self.assertEqual(bundle.status, ShareLinkBundle.Status.PENDING) + self.assertIsNone(bundle.last_error) + self.assertIsNone(bundle.size_bytes) + self.assertEqual(bundle.file_path, "") + delay_mock.assert_called_once_with(bundle.pk) + + def test_rebuild_bundle_rejects_processing_status(self): + bundle = ShareLinkBundle.objects.create( + slug="processing-slug", + file_version=ShareLink.FileVersion.ARCHIVE, + status=ShareLinkBundle.Status.PROCESSING, + ) + bundle.documents.set([self.document]) + + response = self.client.post(f"{self.ENDPOINT}{bundle.pk}/rebuild/") + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("detail", response.data) + + def test_create_bundle_rejects_duplicate_documents(self): + payload = { + "document_ids": [self.document.pk, self.document.pk], + "file_version": ShareLink.FileVersion.ARCHIVE, + "expiration_days": 7, + } + + response = self.client.post(self.ENDPOINT, payload, format="json") + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("document_ids", response.data) + + def test_download_ready_bundle_streams_file(self): + bundle_file = Path(self.dirs.media_dir) / "bundles" / "ready.zip" + bundle_file.parent.mkdir(parents=True, exist_ok=True) + bundle_file.write_bytes(b"binary-zip-content") + + bundle = ShareLinkBundle.objects.create( + slug="readyslug", + file_version=ShareLink.FileVersion.ARCHIVE, + status=ShareLinkBundle.Status.READY, + file_path=str(bundle_file), + ) + bundle.documents.set([self.document]) + + self.client.logout() + response = self.client.get(f"/share/{bundle.slug}/") + content = b"".join(response.streaming_content) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response["Content-Type"], "application/zip") + self.assertEqual(content, b"binary-zip-content") + self.assertIn("attachment;", response["Content-Disposition"]) + + def test_download_pending_bundle_returns_202(self): + bundle = ShareLinkBundle.objects.create( + slug="pendingslug", + file_version=ShareLink.FileVersion.ARCHIVE, + status=ShareLinkBundle.Status.PENDING, + ) + bundle.documents.set([self.document]) + + self.client.logout() + response = self.client.get(f"/share/{bundle.slug}/") + + self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED) + + def test_download_failed_bundle_returns_503(self): + bundle = ShareLinkBundle.objects.create( + slug="failedslug", + file_version=ShareLink.FileVersion.ARCHIVE, + status=ShareLinkBundle.Status.FAILED, + ) + bundle.documents.set([self.document]) + + self.client.logout() + response = self.client.get(f"/share/{bundle.slug}/") + + self.assertEqual(response.status_code, status.HTTP_503_SERVICE_UNAVAILABLE) + + def test_expired_share_link_redirects(self): + share_link = ShareLink.objects.create( + slug="expiredlink", + document=self.document, + file_version=ShareLink.FileVersion.ORIGINAL, + expiration=timezone.now() - timedelta(hours=1), + ) + + self.client.logout() + response = self.client.get(f"/share/{share_link.slug}/") + + self.assertEqual(response.status_code, status.HTTP_302_FOUND) + self.assertIn("sharelink_expired=1", response["Location"]) + + def test_unknown_share_link_redirects(self): + self.client.logout() + response = self.client.get("/share/unknownsharelink/") + + self.assertEqual(response.status_code, status.HTTP_302_FOUND) + self.assertIn("sharelink_notfound=1", response["Location"]) + + +class ShareLinkBundleTaskTests(DirectoriesMixin, APITestCase): + def setUp(self): + super().setUp() + self.document = DocumentFactory.create() + + def test_cleanup_expired_share_link_bundles(self): + expired_path = Path(self.dirs.media_dir) / "expired.zip" + expired_path.parent.mkdir(parents=True, exist_ok=True) + expired_path.write_bytes(b"expired") + + active_path = Path(self.dirs.media_dir) / "active.zip" + active_path.write_bytes(b"active") + + expired_bundle = ShareLinkBundle.objects.create( + slug="expired-bundle", + file_version=ShareLink.FileVersion.ARCHIVE, + status=ShareLinkBundle.Status.READY, + expiration=timezone.now() - timedelta(days=1), + file_path=str(expired_path), + ) + expired_bundle.documents.set([self.document]) + + active_bundle = ShareLinkBundle.objects.create( + slug="active-bundle", + file_version=ShareLink.FileVersion.ARCHIVE, + status=ShareLinkBundle.Status.READY, + expiration=timezone.now() + timedelta(days=1), + file_path=str(active_path), + ) + active_bundle.documents.set([self.document]) + + cleanup_expired_share_link_bundles() + + self.assertFalse(ShareLinkBundle.objects.filter(pk=expired_bundle.pk).exists()) + self.assertTrue(ShareLinkBundle.objects.filter(pk=active_bundle.pk).exists()) + self.assertFalse(expired_path.exists()) + self.assertTrue(active_path.exists()) + + def test_cleanup_expired_share_link_bundles_logs_on_failure(self): + expired_bundle = ShareLinkBundle.objects.create( + slug="expired-bundle", + file_version=ShareLink.FileVersion.ARCHIVE, + status=ShareLinkBundle.Status.READY, + expiration=timezone.now() - timedelta(days=1), + ) + expired_bundle.documents.set([self.document]) + + with mock.patch.object( + ShareLinkBundle, + "delete", + side_effect=RuntimeError("fail"), + ): + with self.assertLogs("paperless.tasks", level="WARNING") as logs: + cleanup_expired_share_link_bundles() + + self.assertTrue( + any( + "Failed to delete expired share link bundle" in msg + for msg in logs.output + ), + ) + + +class ShareLinkBundleBuildTaskTests(DirectoriesMixin, APITestCase): + def setUp(self): + super().setUp() + self.document = DocumentFactory.create( + mime_type="application/pdf", + checksum="123", + ) + self.document.archive_checksum = "" + self.document.save() + self.addCleanup( + setattr, + settings, + "SHARE_LINK_BUNDLE_DIR", + settings.SHARE_LINK_BUNDLE_DIR, + ) + settings.SHARE_LINK_BUNDLE_DIR = ( + Path(settings.MEDIA_ROOT) / "documents" / "share_link_bundles" + ) + + def _write_document_file(self, *, archive: bool, content: bytes) -> Path: + if archive: + self.document.archive_filename = f"{self.document.pk:07}.pdf" + self.document.save() + path = self.document.archive_path + else: + path = self.document.source_path + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(content) + return path + + def test_build_share_link_bundle_creates_zip_and_sets_metadata(self): + self._write_document_file(archive=False, content=b"source") + archive_path = self._write_document_file(archive=True, content=b"archive") + bundle = ShareLinkBundle.objects.create( + slug="build-archive", + file_version=ShareLink.FileVersion.ARCHIVE, + ) + bundle.documents.set([self.document]) + + build_share_link_bundle(bundle.pk) + + bundle.refresh_from_db() + self.assertEqual(bundle.status, ShareLinkBundle.Status.READY) + self.assertIsNone(bundle.last_error) + self.assertIsNotNone(bundle.built_at) + self.assertGreater(bundle.size_bytes or 0, 0) + final_path = bundle.absolute_file_path + self.assertIsNotNone(final_path) + self.assertTrue(final_path.exists()) + with zipfile.ZipFile(final_path) as zipf: + names = zipf.namelist() + self.assertEqual(len(names), 1) + self.assertEqual(zipf.read(names[0]), archive_path.read_bytes()) + + def test_build_share_link_bundle_overwrites_existing_file(self): + self._write_document_file(archive=False, content=b"source") + bundle = ShareLinkBundle.objects.create( + slug="overwrite", + file_version=ShareLink.FileVersion.ORIGINAL, + ) + bundle.documents.set([self.document]) + + existing = settings.SHARE_LINK_BUNDLE_DIR / "overwrite.zip" + existing.parent.mkdir(parents=True, exist_ok=True) + existing.write_bytes(b"old") + + build_share_link_bundle(bundle.pk) + + bundle.refresh_from_db() + final_path = bundle.absolute_file_path + self.assertIsNotNone(final_path) + self.assertTrue(final_path.exists()) + self.assertNotEqual(final_path.read_bytes(), b"old") + + def test_build_share_link_bundle_failure_marks_failed(self): + self._write_document_file(archive=False, content=b"source") + bundle = ShareLinkBundle.objects.create( + slug="fail-bundle", + file_version=ShareLink.FileVersion.ORIGINAL, + ) + bundle.documents.set([self.document]) + + with ( + mock.patch( + "documents.tasks.OriginalsOnlyStrategy.add_document", + side_effect=RuntimeError("zip failure"), + ), + mock.patch("pathlib.Path.unlink") as unlink_mock, + ): + unlink_mock.side_effect = [OSError("unlink"), OSError("unlink-finally")] + [ + None, + ] * 5 + with self.assertRaises(RuntimeError): + build_share_link_bundle(bundle.pk) + + bundle.refresh_from_db() + self.assertEqual(bundle.status, ShareLinkBundle.Status.FAILED) + self.assertIsInstance(bundle.last_error, dict) + self.assertEqual(bundle.last_error.get("message"), "zip failure") + self.assertEqual(bundle.last_error.get("exception_type"), "RuntimeError") + scratch_zips = list(Path(settings.SCRATCH_DIR).glob("*.zip")) + self.assertTrue(scratch_zips) + for path in scratch_zips: + path.unlink(missing_ok=True) + + def test_build_share_link_bundle_missing_bundle_noop(self): + # Should not raise when bundle does not exist + build_share_link_bundle(99999) + + +class ShareLinkBundleFilterSetTests(DirectoriesMixin, APITestCase): + def setUp(self): + super().setUp() + self.document = DocumentFactory.create() + self.document.checksum = "doc1checksum" + self.document.save() + self.other_document = DocumentFactory.create() + self.other_document.checksum = "doc2checksum" + self.other_document.save() + self.bundle_one = ShareLinkBundle.objects.create( + slug="bundle-one", + file_version=ShareLink.FileVersion.ORIGINAL, + ) + self.bundle_one.documents.set([self.document]) + self.bundle_two = ShareLinkBundle.objects.create( + slug="bundle-two", + file_version=ShareLink.FileVersion.ORIGINAL, + ) + self.bundle_two.documents.set([self.other_document]) + + def test_filter_documents_returns_all_for_empty_value(self): + filterset = ShareLinkBundleFilterSet( + data={"documents": ""}, + queryset=ShareLinkBundle.objects.all(), + ) + + self.assertCountEqual(filterset.qs, [self.bundle_one, self.bundle_two]) + + def test_filter_documents_handles_invalid_input(self): + filterset = ShareLinkBundleFilterSet( + data={"documents": "invalid"}, + queryset=ShareLinkBundle.objects.all(), + ) + + self.assertFalse(filterset.qs.exists()) + + def test_filter_documents_filters_by_multiple_ids(self): + filterset = ShareLinkBundleFilterSet( + data={"documents": f"{self.document.pk},{self.other_document.pk}"}, + queryset=ShareLinkBundle.objects.all(), + ) + + self.assertCountEqual(filterset.qs, [self.bundle_one, self.bundle_two]) + + def test_filter_documents_returns_queryset_for_empty_ids(self): + filterset = ShareLinkBundleFilterSet( + data={"documents": ","}, + queryset=ShareLinkBundle.objects.all(), + ) + + self.assertCountEqual(filterset.qs, [self.bundle_one, self.bundle_two]) + + +class ShareLinkBundleModelTests(DirectoriesMixin, APITestCase): + def test_absolute_file_path_handles_relative_and_absolute(self): + relative_path = Path("relative.zip") + bundle = ShareLinkBundle.objects.create( + slug="relative-bundle", + file_version=ShareLink.FileVersion.ORIGINAL, + file_path=str(relative_path), + ) + + self.assertEqual( + bundle.absolute_file_path, + (settings.SHARE_LINK_BUNDLE_DIR / relative_path).resolve(), + ) + + absolute_path = Path(self.dirs.media_dir) / "absolute.zip" + bundle.file_path = str(absolute_path) + + self.assertEqual(bundle.absolute_file_path.resolve(), absolute_path.resolve()) + + def test_str_returns_translated_slug(self): + bundle = ShareLinkBundle.objects.create( + slug="string-slug", + file_version=ShareLink.FileVersion.ORIGINAL, + ) + + self.assertIn("string-slug", str(bundle)) + + def test_remove_file_deletes_existing_file(self): + bundle_path = settings.SHARE_LINK_BUNDLE_DIR / "remove.zip" + bundle_path.parent.mkdir(parents=True, exist_ok=True) + bundle_path.write_bytes(b"remove-me") + bundle = ShareLinkBundle.objects.create( + slug="remove-bundle", + file_version=ShareLink.FileVersion.ORIGINAL, + file_path=str(bundle_path.relative_to(settings.SHARE_LINK_BUNDLE_DIR)), + ) + + bundle.remove_file() + + self.assertFalse(bundle_path.exists()) + + def test_remove_file_handles_oserror(self): + bundle_path = settings.SHARE_LINK_BUNDLE_DIR / "remove-error.zip" + bundle_path.parent.mkdir(parents=True, exist_ok=True) + bundle_path.write_bytes(b"remove-me") + bundle = ShareLinkBundle.objects.create( + slug="remove-error", + file_version=ShareLink.FileVersion.ORIGINAL, + file_path=str(bundle_path.relative_to(settings.SHARE_LINK_BUNDLE_DIR)), + ) + + with mock.patch("pathlib.Path.unlink", side_effect=OSError("fail")): + bundle.remove_file() + + self.assertTrue(bundle_path.exists()) + + def test_delete_calls_remove_file(self): + bundle_path = settings.SHARE_LINK_BUNDLE_DIR / "delete.zip" + bundle_path.parent.mkdir(parents=True, exist_ok=True) + bundle_path.write_bytes(b"remove-me") + bundle = ShareLinkBundle.objects.create( + slug="delete-bundle", + file_version=ShareLink.FileVersion.ORIGINAL, + file_path=str(bundle_path.relative_to(settings.SHARE_LINK_BUNDLE_DIR)), + ) + + bundle.delete() + self.assertFalse(bundle_path.exists()) + + +class ShareLinkBundleSerializerTests(DirectoriesMixin, APITestCase): + def setUp(self): + super().setUp() + self.document = DocumentFactory.create() + + def test_validate_document_ids_rejects_duplicates(self): + serializer = ShareLinkBundleSerializer( + data={ + "document_ids": [self.document.pk, self.document.pk], + "file_version": ShareLink.FileVersion.ORIGINAL, + }, + ) + + self.assertFalse(serializer.is_valid()) + self.assertIn("document_ids", serializer.errors) + + def test_create_assigns_documents_and_expiration(self): + serializer = ShareLinkBundleSerializer( + data={ + "document_ids": [self.document.pk], + "file_version": ShareLink.FileVersion.ORIGINAL, + "expiration_days": 3, + }, + ) + + self.assertTrue(serializer.is_valid(), serializer.errors) + bundle = serializer.save() + + self.assertEqual(list(bundle.documents.all()), [self.document]) + expected_expiration = timezone.now() + timedelta(days=3) + self.assertAlmostEqual( + bundle.expiration, + expected_expiration, + delta=timedelta(seconds=10), + ) + + def test_create_raises_when_missing_documents(self): + serializer = ShareLinkBundleSerializer( + data={ + "document_ids": [self.document.pk, 9999], + "file_version": ShareLink.FileVersion.ORIGINAL, + }, + ) + + self.assertTrue(serializer.is_valid(), serializer.errors) + with self.assertRaises(serializers.ValidationError): + serializer.save(documents=[self.document]) diff --git a/src/documents/tests/test_tasks.py b/src/documents/tests/test_tasks.py index 11712549a..475709dd0 100644 --- a/src/documents/tests/test_tasks.py +++ b/src/documents/tests/test_tasks.py @@ -3,14 +3,17 @@ from datetime import timedelta from pathlib import Path from unittest import mock +from celery import states from django.conf import settings from django.test import TestCase +from django.test import override_settings from django.utils import timezone from documents import tasks from documents.models import Correspondent from documents.models import Document from documents.models import DocumentType +from documents.models import PaperlessTask from documents.models import Tag from documents.sanity_checker import SanityCheckFailedException from documents.sanity_checker import SanityCheckMessages @@ -270,3 +273,103 @@ class TestUpdateContent(DirectoriesMixin, TestCase): tasks.update_document_content_maybe_archive_file(doc.pk) self.assertNotEqual(Document.objects.get(pk=doc.pk).content, "test") + + +class TestAIIndex(DirectoriesMixin, TestCase): + @override_settings( + AI_ENABLED=True, + LLM_EMBEDDING_BACKEND="huggingface", + ) + def test_ai_index_success(self): + """ + GIVEN: + - Document exists, AI is enabled, llm index backend is set + WHEN: + - llmindex_index task is called + THEN: + - update_llm_index is called, and the task is marked as success + """ + Document.objects.create( + title="test", + content="my document", + checksum="wow", + ) + # lazy-loaded so mock the actual function + with mock.patch("paperless_ai.indexing.update_llm_index") as update_llm_index: + update_llm_index.return_value = "LLM index updated successfully." + tasks.llmindex_index() + update_llm_index.assert_called_once() + task = PaperlessTask.objects.get( + task_name=PaperlessTask.TaskName.LLMINDEX_UPDATE, + ) + self.assertEqual(task.status, states.SUCCESS) + self.assertEqual(task.result, "LLM index updated successfully.") + + @override_settings( + AI_ENABLED=True, + LLM_EMBEDDING_BACKEND="huggingface", + ) + def test_ai_index_failure(self): + """ + GIVEN: + - Document exists, AI is enabled, llm index backend is set + WHEN: + - llmindex_index task is called + THEN: + - update_llm_index raises an exception, and the task is marked as failure + """ + Document.objects.create( + title="test", + content="my document", + checksum="wow", + ) + # lazy-loaded so mock the actual function + with mock.patch("paperless_ai.indexing.update_llm_index") as update_llm_index: + update_llm_index.side_effect = Exception("LLM index update failed.") + tasks.llmindex_index() + update_llm_index.assert_called_once() + task = PaperlessTask.objects.get( + task_name=PaperlessTask.TaskName.LLMINDEX_UPDATE, + ) + self.assertEqual(task.status, states.FAILURE) + self.assertIn("LLM index update failed.", task.result) + + def test_update_document_in_llm_index(self): + """ + GIVEN: + - Nothing + WHEN: + - update_document_in_llm_index task is called + THEN: + - llm_index_add_or_update_document is called + """ + doc = Document.objects.create( + title="test", + content="my document", + checksum="wow", + ) + with mock.patch( + "documents.tasks.llm_index_add_or_update_document", + ) as llm_index_add_or_update_document: + tasks.update_document_in_llm_index(doc) + llm_index_add_or_update_document.assert_called_once_with(doc) + + def test_remove_document_from_llm_index(self): + """ + GIVEN: + - Nothing + WHEN: + - remove_document_from_llm_index task is called + THEN: + - llm_index_remove_document is called + """ + doc = Document.objects.create( + title="test", + content="my document", + checksum="wow", + ) + with mock.patch( + "documents.tasks.llm_index_remove_document", + ) as llm_index_remove_document: + tasks.remove_document_from_llm_index(doc) + llm_index_remove_document.assert_called_once_with(doc) diff --git a/src/documents/tests/test_views.py b/src/documents/tests/test_views.py index 4fa8fa833..a73016c26 100644 --- a/src/documents/tests/test_views.py +++ b/src/documents/tests/test_views.py @@ -2,6 +2,8 @@ import json import tempfile from datetime import timedelta from pathlib import Path +from unittest.mock import MagicMock +from unittest.mock import patch from django.conf import settings from django.contrib.auth.models import Group @@ -15,9 +17,15 @@ from django.utils import timezone from guardian.shortcuts import assign_perm from rest_framework import status +from documents.caching import get_llm_suggestion_cache +from documents.caching import set_llm_suggestions_cache +from documents.models import Correspondent from documents.models import Document +from documents.models import DocumentType from documents.models import ShareLink +from documents.models import StoragePath from documents.models import Tag +from documents.signals.handlers import update_llm_suggestions_cache from documents.tests.utils import DirectoriesMixin from paperless.models import ApplicationConfiguration @@ -270,3 +278,176 @@ class TestViews(DirectoriesMixin, TestCase): f"Possible N+1 queries detected: {num_queries_small} queries for 2 tags, " f"but {num_queries_large} queries for 50 tags" ) + + +class TestAISuggestions(DirectoriesMixin, TestCase): + def setUp(self): + self.user = User.objects.create_superuser(username="testuser") + self.document = Document.objects.create( + title="Test Document", + filename="test.pdf", + mime_type="application/pdf", + ) + self.tag1 = Tag.objects.create(name="tag1") + self.correspondent1 = Correspondent.objects.create(name="correspondent1") + self.document_type1 = DocumentType.objects.create(name="type1") + self.path1 = StoragePath.objects.create(name="path1") + super().setUp() + + @patch("documents.views.get_llm_suggestion_cache") + @patch("documents.views.refresh_suggestions_cache") + @override_settings( + AI_ENABLED=True, + LLM_BACKEND="mock_backend", + ) + def test_suggestions_with_cached_llm(self, mock_refresh_cache, mock_get_cache): + mock_get_cache.return_value = MagicMock(suggestions={"tags": ["tag1", "tag2"]}) + + self.client.force_login(user=self.user) + response = self.client.get(f"/api/documents/{self.document.pk}/suggestions/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json(), {"tags": ["tag1", "tag2"]}) + mock_refresh_cache.assert_called_once_with(self.document.pk) + + @patch("documents.views.get_ai_document_classification") + @override_settings( + AI_ENABLED=True, + LLM_BACKEND="mock_backend", + ) + def test_suggestions_with_ai_enabled( + self, + mock_get_ai_classification, + ): + mock_get_ai_classification.return_value = { + "title": "AI Title", + "tags": ["tag1", "tag2"], + "correspondents": ["correspondent1"], + "document_types": ["type1"], + "storage_paths": ["path1"], + "dates": ["2023-01-01"], + } + + self.client.force_login(user=self.user) + response = self.client.get(f"/api/documents/{self.document.pk}/suggestions/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.json(), + { + "title": "AI Title", + "tags": [self.tag1.pk], + "suggested_tags": ["tag2"], + "correspondents": [self.correspondent1.pk], + "suggested_correspondents": [], + "document_types": [self.document_type1.pk], + "suggested_document_types": [], + "storage_paths": [self.path1.pk], + "suggested_storage_paths": [], + "dates": ["2023-01-01"], + }, + ) + + def test_invalidate_suggestions_cache(self): + self.client.force_login(user=self.user) + suggestions = { + "title": "AI Title", + "tags": ["tag1", "tag2"], + "correspondents": ["correspondent1"], + "document_types": ["type1"], + "storage_paths": ["path1"], + "dates": ["2023-01-01"], + } + set_llm_suggestions_cache( + self.document.pk, + suggestions, + backend="mock_backend", + ) + self.assertEqual( + get_llm_suggestion_cache( + self.document.pk, + backend="mock_backend", + ).suggestions, + suggestions, + ) + # post_save signal triggered + update_llm_suggestions_cache( + sender=None, + instance=self.document, + ) + self.assertIsNone( + get_llm_suggestion_cache( + self.document.pk, + backend="mock_backend", + ), + ) + + +class TestAIChatStreamingView(DirectoriesMixin, TestCase): + ENDPOINT = "/api/documents/chat/" + + def setUp(self): + self.user = User.objects.create_user(username="testuser", password="pass") + self.client.force_login(user=self.user) + self.document = Document.objects.create( + title="Test Document", + filename="test.pdf", + mime_type="application/pdf", + ) + super().setUp() + + @override_settings(AI_ENABLED=False) + def test_post_ai_disabled(self): + response = self.client.post( + self.ENDPOINT, + data='{"q": "question"}', + content_type="application/json", + ) + self.assertEqual(response.status_code, 400) + self.assertIn(b"AI is required for this feature", response.content) + + @patch("documents.views.stream_chat_with_documents") + @patch("documents.views.get_objects_for_user_owner_aware") + @override_settings(AI_ENABLED=True) + def test_post_no_document_id(self, mock_get_objects, mock_stream_chat): + mock_get_objects.return_value = [self.document] + mock_stream_chat.return_value = iter([b"data"]) + response = self.client.post( + self.ENDPOINT, + data='{"q": "question"}', + content_type="application/json", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response["Content-Type"], "text/event-stream") + + @patch("documents.views.stream_chat_with_documents") + @override_settings(AI_ENABLED=True) + def test_post_with_document_id(self, mock_stream_chat): + mock_stream_chat.return_value = iter([b"data"]) + response = self.client.post( + self.ENDPOINT, + data=f'{{"q": "question", "document_id": {self.document.pk}}}', + content_type="application/json", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response["Content-Type"], "text/event-stream") + + @override_settings(AI_ENABLED=True) + def test_post_with_invalid_document_id(self): + response = self.client.post( + self.ENDPOINT, + data='{"q": "question", "document_id": 999999}', + content_type="application/json", + ) + self.assertEqual(response.status_code, 400) + self.assertIn(b"Document not found", response.content) + + @patch("documents.views.has_perms_owner_aware") + @override_settings(AI_ENABLED=True) + def test_post_with_document_id_no_permission(self, mock_has_perms): + mock_has_perms.return_value = False + response = self.client.post( + self.ENDPOINT, + data=f'{{"q": "question", "document_id": {self.document.pk}}}', + content_type="application/json", + ) + self.assertEqual(response.status_code, 403) + self.assertIn(b"Insufficient permissions", response.content) diff --git a/src/documents/tests/test_workflows.py b/src/documents/tests/test_workflows.py index deb40a165..75f9d5fe6 100644 --- a/src/documents/tests/test_workflows.py +++ b/src/documents/tests/test_workflows.py @@ -1276,6 +1276,76 @@ class TestWorkflows( ) self.assertIn(expected_str, cm.output[1]) + def test_document_added_any_filters(self): + trigger = WorkflowTrigger.objects.create( + type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED, + ) + trigger.filter_has_any_correspondents.set([self.c]) + trigger.filter_has_any_document_types.set([self.dt]) + trigger.filter_has_any_storage_paths.set([self.sp]) + + matching_doc = Document.objects.create( + title="sample test", + correspondent=self.c, + document_type=self.dt, + storage_path=self.sp, + original_filename="sample.pdf", + checksum="checksum-any-match", + ) + + matched, reason = existing_document_matches_workflow(matching_doc, trigger) + self.assertTrue(matched) + self.assertIsNone(reason) + + wrong_correspondent = Document.objects.create( + title="wrong correspondent", + correspondent=self.c2, + document_type=self.dt, + storage_path=self.sp, + original_filename="sample2.pdf", + ) + matched, reason = existing_document_matches_workflow( + wrong_correspondent, + trigger, + ) + self.assertFalse(matched) + self.assertIn("correspondent", reason) + + other_document_type = DocumentType.objects.create(name="Other") + wrong_document_type = Document.objects.create( + title="wrong doc type", + correspondent=self.c, + document_type=other_document_type, + storage_path=self.sp, + original_filename="sample3.pdf", + checksum="checksum-wrong-doc-type", + ) + matched, reason = existing_document_matches_workflow( + wrong_document_type, + trigger, + ) + self.assertFalse(matched) + self.assertIn("doc type", reason) + + other_storage_path = StoragePath.objects.create( + name="Other path", + path="/other/", + ) + wrong_storage_path = Document.objects.create( + title="wrong storage", + correspondent=self.c, + document_type=self.dt, + storage_path=other_storage_path, + original_filename="sample4.pdf", + checksum="checksum-wrong-storage-path", + ) + matched, reason = existing_document_matches_workflow( + wrong_storage_path, + trigger, + ) + self.assertFalse(matched) + self.assertIn("storage path", reason) + def test_document_added_custom_field_query_no_match(self): trigger = WorkflowTrigger.objects.create( type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED, @@ -1384,6 +1454,39 @@ class TestWorkflows( self.assertIn(doc1, filtered) self.assertNotIn(doc2, filtered) + def test_prefilter_documents_any_filters(self): + trigger = WorkflowTrigger.objects.create( + type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED, + ) + trigger.filter_has_any_correspondents.set([self.c]) + trigger.filter_has_any_document_types.set([self.dt]) + trigger.filter_has_any_storage_paths.set([self.sp]) + + allowed_document = Document.objects.create( + title="allowed", + correspondent=self.c, + document_type=self.dt, + storage_path=self.sp, + original_filename="doc-allowed.pdf", + checksum="checksum-any-allowed", + ) + blocked_document = Document.objects.create( + title="blocked", + correspondent=self.c2, + document_type=self.dt, + storage_path=self.sp, + original_filename="doc-blocked.pdf", + checksum="checksum-any-blocked", + ) + + filtered = prefilter_documents_by_workflowtrigger( + Document.objects.all(), + trigger, + ) + + self.assertIn(allowed_document, filtered) + self.assertNotIn(blocked_document, filtered) + def test_consumption_trigger_requires_filter_configuration(self): serializer = WorkflowTriggerSerializer( data={ @@ -3298,7 +3401,7 @@ class TestWorkflows( ) webhook_action = WorkflowActionWebhook.objects.create( use_params=False, - body="Test message: {{doc_url}}", + body="Test message: {{doc_url}} with id {{doc_id}}", url="http://paperless-ngx.com", include_document=False, ) @@ -3328,7 +3431,10 @@ class TestWorkflows( mock_post.assert_called_once_with( url="http://paperless-ngx.com", - data=f"Test message: http://localhost:8000/paperless/documents/{doc.id}/", + data=( + f"Test message: http://localhost:8000/paperless/documents/{doc.id}/" + f" with id {doc.id}" + ), headers={}, files=None, as_json=False, diff --git a/src/documents/views.py b/src/documents/views.py index 680600c4b..c0f3b5db4 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -35,7 +35,6 @@ from django.db.models import Model from django.db.models import Q from django.db.models import Sum from django.db.models import When -from django.db.models.functions import Length from django.db.models.functions import Lower from django.db.models.manager import Manager from django.http import FileResponse @@ -45,13 +44,16 @@ from django.http import HttpResponseBadRequest from django.http import HttpResponseForbidden from django.http import HttpResponseRedirect from django.http import HttpResponseServerError +from django.http import StreamingHttpResponse from django.shortcuts import get_object_or_404 from django.utils import timezone from django.utils.decorators import method_decorator from django.utils.timezone import make_aware from django.utils.translation import get_language +from django.utils.translation import gettext_lazy as _ from django.views import View from django.views.decorators.cache import cache_control +from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.http import condition from django.views.decorators.http import last_modified from django.views.generic import TemplateView @@ -69,6 +71,7 @@ from packaging import version as packaging_version from redis import Redis from rest_framework import parsers from rest_framework import serializers +from rest_framework import status from rest_framework.decorators import action from rest_framework.exceptions import NotFound from rest_framework.exceptions import ValidationError @@ -91,10 +94,12 @@ from documents import index from documents.bulk_download import ArchiveOnlyStrategy from documents.bulk_download import OriginalAndArchiveStrategy from documents.bulk_download import OriginalsOnlyStrategy +from documents.caching import get_llm_suggestion_cache from documents.caching import get_metadata_cache from documents.caching import get_suggestion_cache from documents.caching import refresh_metadata_cache from documents.caching import refresh_suggestions_cache +from documents.caching import set_llm_suggestions_cache from documents.caching import set_metadata_cache from documents.caching import set_suggestions_cache from documents.classifier import load_classifier @@ -117,6 +122,7 @@ from documents.filters import DocumentTypeFilterSet from documents.filters import ObjectOwnedOrGrantedPermissionsFilter from documents.filters import ObjectOwnedPermissionsFilter from documents.filters import PaperlessTaskFilterSet +from documents.filters import ShareLinkBundleFilterSet from documents.filters import ShareLinkFilterSet from documents.filters import StoragePathFilterSet from documents.filters import TagFilterSet @@ -134,6 +140,7 @@ from documents.models import Note from documents.models import PaperlessTask from documents.models import SavedView from documents.models import ShareLink +from documents.models import ShareLinkBundle from documents.models import StoragePath from documents.models import Tag from documents.models import UiSettings @@ -167,6 +174,7 @@ from documents.serialisers import PostDocumentSerializer from documents.serialisers import RunTaskViewSerializer from documents.serialisers import SavedViewSerializer from documents.serialisers import SearchResultSerializer +from documents.serialisers import ShareLinkBundleSerializer from documents.serialisers import ShareLinkSerializer from documents.serialisers import StoragePathSerializer from documents.serialisers import StoragePathTestSerializer @@ -179,21 +187,30 @@ from documents.serialisers import WorkflowActionSerializer from documents.serialisers import WorkflowSerializer from documents.serialisers import WorkflowTriggerSerializer from documents.signals import document_updated +from documents.tasks import build_share_link_bundle from documents.tasks import consume_file from documents.tasks import empty_trash from documents.tasks import index_optimize +from documents.tasks import llmindex_index from documents.tasks import sanity_check from documents.tasks import train_classifier from documents.tasks import update_document_parent_tags from documents.utils import get_boolean from paperless import version from paperless.celery import app as celery_app +from paperless.config import AIConfig from paperless.config import GeneralConfig -from paperless.db import GnuPG from paperless.models import ApplicationConfiguration from paperless.serialisers import GroupSerializer from paperless.serialisers import UserSerializer from paperless.views import StandardPagination +from paperless_ai.ai_classifier import get_ai_document_classification +from paperless_ai.chat import stream_chat_with_documents +from paperless_ai.matching import extract_unmatched_names +from paperless_ai.matching import match_correspondents_by_name +from paperless_ai.matching import match_document_types_by_name +from paperless_ai.matching import match_storage_paths_by_name +from paperless_ai.matching import match_tags_by_name from paperless_mail.models import MailAccount from paperless_mail.models import MailRule from paperless_mail.oauth import PaperlessMailOAuth2Manager @@ -448,8 +465,47 @@ class TagViewSet(ModelViewSet, PermissionsAwareDocumentCountMixin): def get_serializer_context(self): context = super().get_serializer_context() context["document_count_filter"] = self.get_document_count_filter() + if hasattr(self, "_children_map"): + context["children_map"] = self._children_map return context + def list(self, request, *args, **kwargs): + """ + Build a children map once to avoid per-parent queries in the serializer. + """ + queryset = self.filter_queryset(self.get_queryset()) + ordering = OrderingFilter().get_ordering(request, queryset, self) or ( + Lower("name"), + ) + queryset = queryset.order_by(*ordering) + + all_tags = list(queryset) + descendant_pks = {pk for tag in all_tags for pk in tag.get_descendants_pks()} + + if descendant_pks: + filter_q = self.get_document_count_filter() + children_source = list( + Tag.objects.filter(pk__in=descendant_pks | {t.pk for t in all_tags}) + .select_related("owner") + .annotate(document_count=Count("documents", filter=filter_q)) + .order_by(*ordering), + ) + else: + children_source = all_tags + + children_map = {} + for tag in children_source: + children_map.setdefault(tag.tn_parent_id, []).append(tag) + self._children_map = children_map + + page = self.paginate_queryset(queryset) + serializer = self.get_serializer(page, many=True) + response = self.get_paginated_response(serializer.data) + if descendant_pks: + # Include children in the "all" field, if needed + response.data["all"] = [tag.pk for tag in children_source] + return response + def perform_update(self, serializer): old_parent = self.get_object().get_parent() tag = serializer.save() @@ -899,37 +955,103 @@ class DocumentViewSet( ): return HttpResponseForbidden("Insufficient permissions") - document_suggestions = get_suggestion_cache(doc.pk) + ai_config = AIConfig() - if document_suggestions is not None: - refresh_suggestions_cache(doc.pk) - return Response(document_suggestions.suggestions) - - classifier = load_classifier() - - dates = [] - if settings.NUMBER_OF_SUGGESTED_DATES > 0: - gen = parse_date_generator(doc.filename, doc.content) - dates = sorted( - {i for i in itertools.islice(gen, settings.NUMBER_OF_SUGGESTED_DATES)}, + if ai_config.ai_enabled: + cached_llm_suggestions = get_llm_suggestion_cache( + doc.pk, + backend=ai_config.llm_backend, ) - resp_data = { - "correspondents": [ - c.id for c in match_correspondents(doc, classifier, request.user) - ], - "tags": [t.id for t in match_tags(doc, classifier, request.user)], - "document_types": [ - dt.id for dt in match_document_types(doc, classifier, request.user) - ], - "storage_paths": [ - dt.id for dt in match_storage_paths(doc, classifier, request.user) - ], - "dates": [date.strftime("%Y-%m-%d") for date in dates if date is not None], - } + if cached_llm_suggestions: + refresh_suggestions_cache(doc.pk) + return Response(cached_llm_suggestions.suggestions) - # Cache the suggestions and the classifier hash for later - set_suggestions_cache(doc.pk, resp_data, classifier) + llm_suggestions = get_ai_document_classification(doc, request.user) + + matched_tags = match_tags_by_name( + llm_suggestions.get("tags", []), + request.user, + ) + matched_correspondents = match_correspondents_by_name( + llm_suggestions.get("correspondents", []), + request.user, + ) + matched_types = match_document_types_by_name( + llm_suggestions.get("document_types", []), + request.user, + ) + matched_paths = match_storage_paths_by_name( + llm_suggestions.get("storage_paths", []), + request.user, + ) + + resp_data = { + "title": llm_suggestions.get("title"), + "tags": [t.id for t in matched_tags], + "suggested_tags": extract_unmatched_names( + llm_suggestions.get("tags", []), + matched_tags, + ), + "correspondents": [c.id for c in matched_correspondents], + "suggested_correspondents": extract_unmatched_names( + llm_suggestions.get("correspondents", []), + matched_correspondents, + ), + "document_types": [d.id for d in matched_types], + "suggested_document_types": extract_unmatched_names( + llm_suggestions.get("document_types", []), + matched_types, + ), + "storage_paths": [s.id for s in matched_paths], + "suggested_storage_paths": extract_unmatched_names( + llm_suggestions.get("storage_paths", []), + matched_paths, + ), + "dates": llm_suggestions.get("dates", []), + } + + set_llm_suggestions_cache(doc.pk, resp_data, backend=ai_config.llm_backend) + else: + document_suggestions = get_suggestion_cache(doc.pk) + + if document_suggestions is not None: + refresh_suggestions_cache(doc.pk) + return Response(document_suggestions.suggestions) + + classifier = load_classifier() + + dates = [] + if settings.NUMBER_OF_SUGGESTED_DATES > 0: + gen = parse_date_generator(doc.filename, doc.content) + dates = sorted( + { + i + for i in itertools.islice( + gen, + settings.NUMBER_OF_SUGGESTED_DATES, + ) + }, + ) + + resp_data = { + "correspondents": [ + c.id for c in match_correspondents(doc, classifier, request.user) + ], + "tags": [t.id for t in match_tags(doc, classifier, request.user)], + "document_types": [ + dt.id for dt in match_document_types(doc, classifier, request.user) + ], + "storage_paths": [ + dt.id for dt in match_storage_paths(doc, classifier, request.user) + ], + "dates": [ + date.strftime("%Y-%m-%d") for date in dates if date is not None + ], + } + + # Cache the suggestions and the classifier hash for later + set_suggestions_cache(doc.pk, resp_data, classifier) return Response(resp_data) @@ -957,10 +1079,8 @@ class DocumentViewSet( doc, ): return HttpResponseForbidden("Insufficient permissions") - if doc.storage_type == Document.STORAGE_TYPE_GPG: - handle = GnuPG.decrypted(doc.thumbnail_file) - else: - handle = doc.thumbnail_file + + handle = doc.thumbnail_file return HttpResponse(handle, content_type="image/webp") except (FileNotFoundError, Document.DoesNotExist): @@ -1253,6 +1373,59 @@ class DocumentViewSet( ) +class ChatStreamingSerializer(serializers.Serializer): + q = serializers.CharField(required=True) + document_id = serializers.IntegerField(required=False, allow_null=True) + + +@method_decorator( + [ + ensure_csrf_cookie, + cache_control(no_cache=True), + ], + name="dispatch", +) +class ChatStreamingView(GenericAPIView): + permission_classes = (IsAuthenticated,) + serializer_class = ChatStreamingSerializer + + def post(self, request, *args, **kwargs): + request.compress_exempt = True + ai_config = AIConfig() + if not ai_config.ai_enabled: + return HttpResponseBadRequest("AI is required for this feature") + + try: + question = request.data["q"] + except KeyError: + return HttpResponseBadRequest("Invalid request") + + doc_id = request.data.get("document_id") + + if doc_id: + try: + document = Document.objects.get(id=doc_id) + except Document.DoesNotExist: + return HttpResponseBadRequest("Document not found") + + if not has_perms_owner_aware(request.user, "view_document", document): + return HttpResponseForbidden("Insufficient permissions") + + documents = [document] + else: + documents = get_objects_for_user_owner_aware( + request.user, + "view_document", + Document, + ) + + response = StreamingHttpResponse( + stream_chat_with_documents(query_str=question, documents=documents), + content_type="text/event-stream", + ) + return response + + @extend_schema_view( list=extend_schema( description="Document views including search", @@ -2158,23 +2331,19 @@ class StatisticsView(GenericAPIView): user = request.user if request.user is not None else None documents = ( - ( - Document.objects.all() - if user is None - else get_objects_for_user_owner_aware( - user, - "documents.view_document", - Document, - ) + Document.objects.all() + if user is None + else get_objects_for_user_owner_aware( + user, + "documents.view_document", + Document, ) - .only("mime_type", "content") - .prefetch_related("tags") ) tags = ( Tag.objects.all() if user is None else get_objects_for_user_owner_aware(user, "documents.view_tag", Tag) - ) + ).only("id", "is_inbox_tag") correspondent_count = ( Correspondent.objects.count() if user is None @@ -2203,31 +2372,33 @@ class StatisticsView(GenericAPIView): ).count() ) - documents_total = documents.count() - - inbox_tags = tags.filter(is_inbox_tag=True) + inbox_tag_pks = list( + tags.filter(is_inbox_tag=True).values_list("pk", flat=True), + ) documents_inbox = ( - documents.filter(tags__id__in=inbox_tags).distinct().count() - if inbox_tags.exists() + documents.filter(tags__id__in=inbox_tag_pks).values("id").distinct().count() + if inbox_tag_pks else None ) - document_file_type_counts = ( + # Single SQL request for document stats and mime type counts + mime_type_stats = list( documents.values("mime_type") - .annotate(mime_type_count=Count("mime_type")) - .order_by("-mime_type_count") - if documents_total > 0 - else [] + .annotate( + mime_type_count=Count("id"), + mime_type_chars=Sum("content_length"), + ) + .order_by("-mime_type_count"), ) - character_count = ( - documents.annotate( - characters=Length("content"), - ) - .aggregate(Sum("characters")) - .get("characters__sum") - ) + # Calculate totals from grouped results + documents_total = sum(row["mime_type_count"] for row in mime_type_stats) + character_count = sum(row["mime_type_chars"] or 0 for row in mime_type_stats) + document_file_type_counts = [ + {"mime_type": row["mime_type"], "mime_type_count": row["mime_type_count"]} + for row in mime_type_stats + ] current_asn = Document.objects.aggregate( Max("archive_serial_number", default=0), @@ -2240,11 +2411,9 @@ class StatisticsView(GenericAPIView): "documents_total": documents_total, "documents_inbox": documents_inbox, "inbox_tag": ( - inbox_tags.first().pk if inbox_tags.exists() else None + inbox_tag_pks[0] if inbox_tag_pks else None ), # backwards compatibility - "inbox_tags": ( - [tag.pk for tag in inbox_tags] if inbox_tags.exists() else None - ), + "inbox_tags": (inbox_tag_pks if inbox_tag_pks else None), "document_file_type_counts": document_file_type_counts, "character_count": character_count, "tag_count": len(tags), @@ -2272,7 +2441,7 @@ class BulkDownloadView(GenericAPIView): follow_filename_format = serializer.validated_data.get("follow_formatting") for document in documents: - if not has_perms_owner_aware(request.user, "view_document", document): + if not has_perms_owner_aware(request.user, "change_document", document): return HttpResponseForbidden("Insufficient permissions") settings.SCRATCH_DIR.mkdir(parents=True, exist_ok=True) @@ -2411,6 +2580,10 @@ class UiSettingsView(GenericAPIView): ui_settings["email_enabled"] = settings.EMAIL_ENABLED + ai_config = AIConfig() + + ui_settings["ai_enabled"] = ai_config.ai_enabled + user_resp = { "id": user.id, "username": user.username, @@ -2552,6 +2725,10 @@ class TasksViewSet(ReadOnlyModelViewSet): sanity_check, {"scheduled": False, "raise_on_error": False}, ), + PaperlessTask.TaskName.LLMINDEX_UPDATE: ( + llmindex_index, + {"scheduled": False, "rebuild": False}, + ), } def get_queryset(self): @@ -2619,21 +2796,187 @@ class ShareLinkViewSet(ModelViewSet, PassUserMixin): ordering_fields = ("created", "expiration", "document") +class ShareLinkBundleViewSet(ModelViewSet, PassUserMixin): + model = ShareLinkBundle + + queryset = ShareLinkBundle.objects.all() + + serializer_class = ShareLinkBundleSerializer + pagination_class = StandardPagination + permission_classes = (IsAuthenticated, PaperlessObjectPermissions) + filter_backends = ( + DjangoFilterBackend, + OrderingFilter, + ObjectOwnedOrGrantedPermissionsFilter, + ) + filterset_class = ShareLinkBundleFilterSet + ordering_fields = ("created", "expiration", "status") + + def get_queryset(self): + return ( + super() + .get_queryset() + .prefetch_related("documents") + .annotate(document_total=Count("documents", distinct=True)) + ) + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + document_ids = serializer.validated_data["document_ids"] + documents_qs = Document.objects.filter(pk__in=document_ids).select_related( + "owner", + ) + found_ids = set(documents_qs.values_list("pk", flat=True)) + missing = sorted(set(document_ids) - found_ids) + if missing: + raise ValidationError( + { + "document_ids": _( + "Documents not found: %(ids)s", + ) + % {"ids": ", ".join(str(item) for item in missing)}, + }, + ) + + documents = list(documents_qs) + for document in documents: + if not has_perms_owner_aware(request.user, "view_document", document): + raise ValidationError( + { + "document_ids": _( + "Insufficient permissions to share document %(id)s.", + ) + % {"id": document.pk}, + }, + ) + + document_map = {document.pk: document for document in documents} + ordered_documents = [document_map[doc_id] for doc_id in document_ids] + + bundle = serializer.save( + owner=request.user, + documents=ordered_documents, + ) + bundle.remove_file() + bundle.status = ShareLinkBundle.Status.PENDING + bundle.last_error = None + bundle.size_bytes = None + bundle.built_at = None + bundle.file_path = "" + bundle.save( + update_fields=[ + "status", + "last_error", + "size_bytes", + "built_at", + "file_path", + ], + ) + build_share_link_bundle.delay(bundle.pk) + bundle.document_total = len(ordered_documents) + response_serializer = self.get_serializer(bundle) + headers = self.get_success_headers(response_serializer.data) + return Response( + response_serializer.data, + status=status.HTTP_201_CREATED, + headers=headers, + ) + + @action(detail=True, methods=["post"]) + def rebuild(self, request, pk=None): + bundle = self.get_object() + if bundle.status == ShareLinkBundle.Status.PROCESSING: + return Response( + {"detail": _("Bundle is already being processed.")}, + status=status.HTTP_400_BAD_REQUEST, + ) + bundle.remove_file() + bundle.status = ShareLinkBundle.Status.PENDING + bundle.last_error = None + bundle.size_bytes = None + bundle.built_at = None + bundle.file_path = "" + bundle.save( + update_fields=[ + "status", + "last_error", + "size_bytes", + "built_at", + "file_path", + ], + ) + build_share_link_bundle.delay(bundle.pk) + bundle.document_total = ( + getattr(bundle, "document_total", None) or bundle.documents.count() + ) + serializer = self.get_serializer(bundle) + return Response(serializer.data) + + class SharedLinkView(View): authentication_classes = [] permission_classes = [] def get(self, request, slug): share_link = ShareLink.objects.filter(slug=slug).first() - if share_link is None: + if share_link is not None: + if ( + share_link.expiration is not None + and share_link.expiration < timezone.now() + ): + return HttpResponseRedirect("/accounts/login/?sharelink_expired=1") + return serve_file( + doc=share_link.document, + use_archive=share_link.file_version == "archive", + disposition="inline", + ) + + bundle = ShareLinkBundle.objects.filter(slug=slug).first() + if bundle is None: return HttpResponseRedirect("/accounts/login/?sharelink_notfound=1") - if share_link.expiration is not None and share_link.expiration < timezone.now(): + + if bundle.expiration is not None and bundle.expiration < timezone.now(): return HttpResponseRedirect("/accounts/login/?sharelink_expired=1") - return serve_file( - doc=share_link.document, - use_archive=share_link.file_version == "archive", - disposition="inline", + + if bundle.status in { + ShareLinkBundle.Status.PENDING, + ShareLinkBundle.Status.PROCESSING, + }: + return HttpResponse( + _( + "The share link bundle is still being prepared. Please try again later.", + ), + status=status.HTTP_202_ACCEPTED, + ) + + file_path = bundle.absolute_file_path + + if bundle.status == ShareLinkBundle.Status.FAILED or file_path is None: + return HttpResponse( + _( + "The share link bundle is unavailable.", + ), + status=status.HTTP_503_SERVICE_UNAVAILABLE, + ) + + response = FileResponse(file_path.open("rb"), content_type="application/zip") + short_slug = bundle.slug[:12] + download_name = f"paperless-share-{short_slug}.zip" + filename_normalized = ( + normalize("NFKD", download_name) + .encode( + "ascii", + "ignore", + ) + .decode("ascii") ) + filename_encoded = quote(download_name) + response["Content-Disposition"] = ( + f"attachment; filename='{filename_normalized}'; " + f"filename*=utf-8''{filename_encoded}" + ) + return response def serve_file(*, doc: Document, use_archive: bool, disposition: str): @@ -2649,9 +2992,6 @@ def serve_file(*, doc: Document, use_archive: bool, disposition: str): if mime_type in {"application/csv", "text/csv"} and disposition == "inline": mime_type = "text/plain" - if doc.storage_type == Document.STORAGE_TYPE_GPG: - file_handle = GnuPG.decrypted(file_handle) - response = HttpResponse(file_handle, content_type=mime_type) # Firefox is not able to handle unicode characters in filename field # RFC 5987 addresses this issue @@ -3071,6 +3411,31 @@ class SystemStatusView(PassUserMixin): last_sanity_check.date_done if last_sanity_check else None ) + ai_config = AIConfig() + if not ai_config.llm_index_enabled: + llmindex_status = "DISABLED" + llmindex_error = None + llmindex_last_modified = None + else: + last_llmindex_update = ( + PaperlessTask.objects.filter( + task_name=PaperlessTask.TaskName.LLMINDEX_UPDATE, + ) + .order_by("-date_done") + .first() + ) + llmindex_status = "OK" + llmindex_error = None + if last_llmindex_update is None: + llmindex_status = "WARNING" + llmindex_error = "No LLM index update tasks found" + elif last_llmindex_update and last_llmindex_update.status == states.FAILURE: + llmindex_status = "ERROR" + llmindex_error = last_llmindex_update.result + llmindex_last_modified = ( + last_llmindex_update.date_done if last_llmindex_update else None + ) + return Response( { "pngx_version": current_version, @@ -3108,6 +3473,9 @@ class SystemStatusView(PassUserMixin): "sanity_check_status": sanity_check_status, "sanity_check_last_run": sanity_check_last_run, "sanity_check_error": sanity_check_error, + "llmindex_status": llmindex_status, + "llmindex_last_modified": llmindex_last_modified, + "llmindex_error": llmindex_error, }, }, ) diff --git a/src/documents/workflows/actions.py b/src/documents/workflows/actions.py index 040cbc127..a61b9930e 100644 --- a/src/documents/workflows/actions.py +++ b/src/documents/workflows/actions.py @@ -44,6 +44,7 @@ def build_workflow_action_context( "current_filename": document.filename or "", "added": timezone.localtime(document.added), "created": document.created, + "id": document.pk, } correspondent_obj = ( @@ -75,6 +76,7 @@ def build_workflow_action_context( "current_filename": filename, "added": timezone.localtime(timezone.now()), "created": overrides.created if overrides else None, + "id": "", } @@ -109,6 +111,7 @@ def execute_email_action( context["created"], context["title"], context["doc_url"], + context["id"], ) if action.email.subject else "" @@ -125,6 +128,7 @@ def execute_email_action( context["created"], context["title"], context["doc_url"], + context["id"], ) if action.email.body else "" @@ -203,6 +207,7 @@ def execute_webhook_action( context["created"], context["title"], context["doc_url"], + context["id"], ) except Exception as e: logger.error( @@ -221,6 +226,7 @@ def execute_webhook_action( context["created"], context["title"], context["doc_url"], + context["id"], ) headers = {} if action.webhook.headers: diff --git a/src/documents/workflows/mutations.py b/src/documents/workflows/mutations.py index ef85dba0f..b93a26781 100644 --- a/src/documents/workflows/mutations.py +++ b/src/documents/workflows/mutations.py @@ -55,6 +55,9 @@ def apply_assignment_to_document( document.original_filename or "", document.filename or "", document.created, + "", # dont pass the title to avoid recursion + "", # no urls in titles + document.pk, ) except Exception: # pragma: no cover logger.exception( diff --git a/src/documents/workflows/utils.py b/src/documents/workflows/utils.py index 553622252..0a644b0eb 100644 --- a/src/documents/workflows/utils.py +++ b/src/documents/workflows/utils.py @@ -20,9 +20,6 @@ def get_workflows_for_trigger( wrap it in a list; otherwise fetch enabled workflows for the trigger with the prefetches used by the runner. """ - if workflow_to_run is not None: - return [workflow_to_run] - annotated_actions = ( WorkflowAction.objects.select_related( "assign_correspondent", @@ -105,10 +102,25 @@ def get_workflows_for_trigger( ) ) + action_prefetch = Prefetch( + "actions", + queryset=annotated_actions.order_by("order", "pk"), + ) + + if workflow_to_run is not None: + return ( + Workflow.objects.filter(pk=workflow_to_run.pk) + .prefetch_related( + action_prefetch, + "triggers", + ) + .distinct() + ) + return ( Workflow.objects.filter(enabled=True, triggers__type=trigger_type) .prefetch_related( - Prefetch("actions", queryset=annotated_actions), + action_prefetch, "triggers", ) .order_by("order") diff --git a/src/locale/en_US/LC_MESSAGES/django.po b/src/locale/en_US/LC_MESSAGES/django.po index 850c20ed5..51b44110b 100644 --- a/src/locale/en_US/LC_MESSAGES/django.po +++ b/src/locale/en_US/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: paperless-ngx\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-01-08 21:50+0000\n" +"POT-Creation-Date: 2026-01-29 16:06+0000\n" "PO-Revision-Date: 2022-02-17 04:17\n" "Last-Translator: \n" "Language-Team: English\n" @@ -21,1236 +21,1295 @@ msgstr "" msgid "Documents" msgstr "" -#: documents/filters.py:395 +#: documents/filters.py:396 msgid "Value must be valid JSON." msgstr "" -#: documents/filters.py:414 +#: documents/filters.py:415 msgid "Invalid custom field query expression" msgstr "" -#: documents/filters.py:424 +#: documents/filters.py:425 msgid "Invalid expression list. Must be nonempty." msgstr "" -#: documents/filters.py:445 +#: documents/filters.py:446 msgid "Invalid logical operator {op!r}" msgstr "" -#: documents/filters.py:459 +#: documents/filters.py:460 msgid "Maximum number of query conditions exceeded." msgstr "" -#: documents/filters.py:524 +#: documents/filters.py:525 msgid "{name!r} is not a valid custom field." msgstr "" -#: documents/filters.py:561 +#: documents/filters.py:562 msgid "{data_type} does not support query expr {expr!r}." msgstr "" -#: documents/filters.py:669 documents/models.py:135 +#: documents/filters.py:670 documents/models.py:137 msgid "Maximum nesting depth exceeded." msgstr "" -#: documents/filters.py:854 +#: documents/filters.py:878 msgid "Custom field not found" msgstr "" -#: documents/models.py:38 documents/models.py:768 +#: documents/models.py:40 documents/models.py:757 documents/models.py:805 msgid "owner" msgstr "" -#: documents/models.py:55 documents/models.py:983 +#: documents/models.py:57 documents/models.py:1080 msgid "None" msgstr "" -#: documents/models.py:56 documents/models.py:984 +#: documents/models.py:58 documents/models.py:1081 msgid "Any word" msgstr "" -#: documents/models.py:57 documents/models.py:985 +#: documents/models.py:59 documents/models.py:1082 msgid "All words" msgstr "" -#: documents/models.py:58 documents/models.py:986 +#: documents/models.py:60 documents/models.py:1083 msgid "Exact match" msgstr "" -#: documents/models.py:59 documents/models.py:987 +#: documents/models.py:61 documents/models.py:1084 msgid "Regular expression" msgstr "" -#: documents/models.py:60 documents/models.py:988 +#: documents/models.py:62 documents/models.py:1085 msgid "Fuzzy word" msgstr "" -#: documents/models.py:61 +#: documents/models.py:63 msgid "Automatic" msgstr "" -#: documents/models.py:64 documents/models.py:456 documents/models.py:1526 +#: documents/models.py:66 documents/models.py:444 documents/models.py:1646 #: paperless_mail/models.py:23 paperless_mail/models.py:143 msgid "name" msgstr "" -#: documents/models.py:66 documents/models.py:1052 +#: documents/models.py:68 documents/models.py:1149 msgid "match" msgstr "" -#: documents/models.py:69 documents/models.py:1055 +#: documents/models.py:71 documents/models.py:1152 msgid "matching algorithm" msgstr "" -#: documents/models.py:74 documents/models.py:1060 +#: documents/models.py:76 documents/models.py:1157 msgid "is insensitive" msgstr "" -#: documents/models.py:97 documents/models.py:170 +#: documents/models.py:99 documents/models.py:165 msgid "correspondent" msgstr "" -#: documents/models.py:98 +#: documents/models.py:100 msgid "correspondents" msgstr "" -#: documents/models.py:102 +#: documents/models.py:104 msgid "color" msgstr "" -#: documents/models.py:107 +#: documents/models.py:109 msgid "is inbox tag" msgstr "" -#: documents/models.py:110 +#: documents/models.py:112 msgid "" "Marks this tag as an inbox tag: All newly consumed documents will be tagged " "with inbox tags." msgstr "" -#: documents/models.py:116 +#: documents/models.py:118 msgid "tag" msgstr "" -#: documents/models.py:117 documents/models.py:208 +#: documents/models.py:119 documents/models.py:212 msgid "tags" msgstr "" -#: documents/models.py:123 +#: documents/models.py:125 msgid "Cannot set itself as parent." msgstr "" -#: documents/models.py:125 +#: documents/models.py:127 msgid "Cannot set parent to a descendant." msgstr "" -#: documents/models.py:142 documents/models.py:190 +#: documents/models.py:144 documents/models.py:185 msgid "document type" msgstr "" -#: documents/models.py:143 +#: documents/models.py:145 msgid "document types" msgstr "" -#: documents/models.py:148 +#: documents/models.py:150 msgid "path" msgstr "" -#: documents/models.py:152 documents/models.py:179 +#: documents/models.py:154 documents/models.py:174 msgid "storage path" msgstr "" -#: documents/models.py:153 +#: documents/models.py:155 msgid "storage paths" msgstr "" -#: documents/models.py:160 -msgid "Unencrypted" -msgstr "" - -#: documents/models.py:161 -msgid "Encrypted with GNU Privacy Guard" -msgstr "" - -#: documents/models.py:182 +#: documents/models.py:177 msgid "title" msgstr "" -#: documents/models.py:194 documents/models.py:682 +#: documents/models.py:189 documents/models.py:671 msgid "content" msgstr "" -#: documents/models.py:197 +#: documents/models.py:192 msgid "" "The raw, text-only data of the document. This field is primarily used for " "searching." msgstr "" -#: documents/models.py:202 +#: documents/models.py:206 msgid "mime type" msgstr "" -#: documents/models.py:212 +#: documents/models.py:216 msgid "checksum" msgstr "" -#: documents/models.py:216 +#: documents/models.py:219 msgid "The checksum of the original document." msgstr "" -#: documents/models.py:220 +#: documents/models.py:223 msgid "archive checksum" msgstr "" -#: documents/models.py:225 +#: documents/models.py:228 msgid "The checksum of the archived document." msgstr "" -#: documents/models.py:229 +#: documents/models.py:232 msgid "page count" msgstr "" -#: documents/models.py:236 +#: documents/models.py:239 msgid "The number of pages of the document." msgstr "" -#: documents/models.py:241 documents/models.py:688 documents/models.py:726 -#: documents/models.py:798 documents/models.py:857 +#: documents/models.py:244 documents/models.py:677 documents/models.py:715 +#: documents/models.py:777 documents/models.py:895 documents/models.py:954 msgid "created" msgstr "" -#: documents/models.py:247 +#: documents/models.py:250 msgid "modified" msgstr "" -#: documents/models.py:254 -msgid "storage type" -msgstr "" - -#: documents/models.py:262 +#: documents/models.py:257 msgid "added" msgstr "" -#: documents/models.py:269 +#: documents/models.py:264 msgid "filename" msgstr "" -#: documents/models.py:275 +#: documents/models.py:270 msgid "Current filename in storage" msgstr "" -#: documents/models.py:279 +#: documents/models.py:274 msgid "archive filename" msgstr "" -#: documents/models.py:285 +#: documents/models.py:280 msgid "Current archive filename in storage" msgstr "" -#: documents/models.py:289 +#: documents/models.py:284 msgid "original filename" msgstr "" -#: documents/models.py:295 +#: documents/models.py:290 msgid "The original name of the file when it was uploaded" msgstr "" -#: documents/models.py:302 +#: documents/models.py:297 msgid "archive serial number" msgstr "" -#: documents/models.py:312 +#: documents/models.py:307 msgid "The position of this document in your physical document archive." msgstr "" -#: documents/models.py:318 documents/models.py:699 documents/models.py:753 -#: documents/models.py:1569 +#: documents/models.py:313 documents/models.py:688 documents/models.py:742 +#: documents/models.py:1689 msgid "document" msgstr "" -#: documents/models.py:319 +#: documents/models.py:314 documents/models.py:848 msgid "documents" msgstr "" -#: documents/models.py:437 +#: documents/models.py:425 msgid "Table" msgstr "" -#: documents/models.py:438 +#: documents/models.py:426 msgid "Small Cards" msgstr "" -#: documents/models.py:439 +#: documents/models.py:427 msgid "Large Cards" msgstr "" -#: documents/models.py:442 +#: documents/models.py:430 msgid "Title" msgstr "" -#: documents/models.py:443 documents/models.py:1004 +#: documents/models.py:431 documents/models.py:1101 msgid "Created" msgstr "" -#: documents/models.py:444 documents/models.py:1003 +#: documents/models.py:432 documents/models.py:1100 msgid "Added" msgstr "" -#: documents/models.py:445 +#: documents/models.py:433 msgid "Tags" msgstr "" -#: documents/models.py:446 +#: documents/models.py:434 msgid "Correspondent" msgstr "" -#: documents/models.py:447 +#: documents/models.py:435 msgid "Document Type" msgstr "" -#: documents/models.py:448 +#: documents/models.py:436 msgid "Storage Path" msgstr "" -#: documents/models.py:449 +#: documents/models.py:437 msgid "Note" msgstr "" -#: documents/models.py:450 +#: documents/models.py:438 msgid "Owner" msgstr "" -#: documents/models.py:451 +#: documents/models.py:439 msgid "Shared" msgstr "" -#: documents/models.py:452 +#: documents/models.py:440 msgid "ASN" msgstr "" -#: documents/models.py:453 +#: documents/models.py:441 msgid "Pages" msgstr "" -#: documents/models.py:459 +#: documents/models.py:447 msgid "show on dashboard" msgstr "" -#: documents/models.py:462 +#: documents/models.py:450 msgid "show in sidebar" msgstr "" -#: documents/models.py:466 +#: documents/models.py:454 msgid "sort field" msgstr "" -#: documents/models.py:471 +#: documents/models.py:459 msgid "sort reverse" msgstr "" -#: documents/models.py:474 +#: documents/models.py:462 msgid "View page size" msgstr "" -#: documents/models.py:482 +#: documents/models.py:470 msgid "View display mode" msgstr "" -#: documents/models.py:489 +#: documents/models.py:477 msgid "Document display fields" msgstr "" -#: documents/models.py:496 documents/models.py:559 +#: documents/models.py:484 documents/models.py:547 msgid "saved view" msgstr "" -#: documents/models.py:497 +#: documents/models.py:485 msgid "saved views" msgstr "" -#: documents/models.py:505 +#: documents/models.py:493 msgid "title contains" msgstr "" -#: documents/models.py:506 +#: documents/models.py:494 msgid "content contains" msgstr "" -#: documents/models.py:507 +#: documents/models.py:495 msgid "ASN is" msgstr "" -#: documents/models.py:508 +#: documents/models.py:496 msgid "correspondent is" msgstr "" -#: documents/models.py:509 +#: documents/models.py:497 msgid "document type is" msgstr "" -#: documents/models.py:510 +#: documents/models.py:498 msgid "is in inbox" msgstr "" -#: documents/models.py:511 +#: documents/models.py:499 msgid "has tag" msgstr "" -#: documents/models.py:512 +#: documents/models.py:500 msgid "has any tag" msgstr "" -#: documents/models.py:513 +#: documents/models.py:501 msgid "created before" msgstr "" -#: documents/models.py:514 +#: documents/models.py:502 msgid "created after" msgstr "" -#: documents/models.py:515 +#: documents/models.py:503 msgid "created year is" msgstr "" -#: documents/models.py:516 +#: documents/models.py:504 msgid "created month is" msgstr "" -#: documents/models.py:517 +#: documents/models.py:505 msgid "created day is" msgstr "" -#: documents/models.py:518 +#: documents/models.py:506 msgid "added before" msgstr "" -#: documents/models.py:519 +#: documents/models.py:507 msgid "added after" msgstr "" -#: documents/models.py:520 +#: documents/models.py:508 msgid "modified before" msgstr "" -#: documents/models.py:521 +#: documents/models.py:509 msgid "modified after" msgstr "" -#: documents/models.py:522 +#: documents/models.py:510 msgid "does not have tag" msgstr "" -#: documents/models.py:523 +#: documents/models.py:511 msgid "does not have ASN" msgstr "" -#: documents/models.py:524 +#: documents/models.py:512 msgid "title or content contains" msgstr "" -#: documents/models.py:525 +#: documents/models.py:513 msgid "fulltext query" msgstr "" -#: documents/models.py:526 +#: documents/models.py:514 msgid "more like this" msgstr "" -#: documents/models.py:527 +#: documents/models.py:515 msgid "has tags in" msgstr "" -#: documents/models.py:528 +#: documents/models.py:516 msgid "ASN greater than" msgstr "" -#: documents/models.py:529 +#: documents/models.py:517 msgid "ASN less than" msgstr "" -#: documents/models.py:530 +#: documents/models.py:518 msgid "storage path is" msgstr "" -#: documents/models.py:531 +#: documents/models.py:519 msgid "has correspondent in" msgstr "" -#: documents/models.py:532 +#: documents/models.py:520 msgid "does not have correspondent in" msgstr "" -#: documents/models.py:533 +#: documents/models.py:521 msgid "has document type in" msgstr "" -#: documents/models.py:534 +#: documents/models.py:522 msgid "does not have document type in" msgstr "" -#: documents/models.py:535 +#: documents/models.py:523 msgid "has storage path in" msgstr "" -#: documents/models.py:536 +#: documents/models.py:524 msgid "does not have storage path in" msgstr "" -#: documents/models.py:537 +#: documents/models.py:525 msgid "owner is" msgstr "" -#: documents/models.py:538 +#: documents/models.py:526 msgid "has owner in" msgstr "" -#: documents/models.py:539 +#: documents/models.py:527 msgid "does not have owner" msgstr "" -#: documents/models.py:540 +#: documents/models.py:528 msgid "does not have owner in" msgstr "" -#: documents/models.py:541 +#: documents/models.py:529 msgid "has custom field value" msgstr "" -#: documents/models.py:542 +#: documents/models.py:530 msgid "is shared by me" msgstr "" -#: documents/models.py:543 +#: documents/models.py:531 msgid "has custom fields" msgstr "" -#: documents/models.py:544 +#: documents/models.py:532 msgid "has custom field in" msgstr "" -#: documents/models.py:545 +#: documents/models.py:533 msgid "does not have custom field in" msgstr "" -#: documents/models.py:546 +#: documents/models.py:534 msgid "does not have custom field" msgstr "" -#: documents/models.py:547 +#: documents/models.py:535 msgid "custom fields query" msgstr "" -#: documents/models.py:548 +#: documents/models.py:536 msgid "created to" msgstr "" -#: documents/models.py:549 +#: documents/models.py:537 msgid "created from" msgstr "" -#: documents/models.py:550 +#: documents/models.py:538 msgid "added to" msgstr "" -#: documents/models.py:551 +#: documents/models.py:539 msgid "added from" msgstr "" -#: documents/models.py:552 +#: documents/models.py:540 msgid "mime type is" msgstr "" -#: documents/models.py:562 +#: documents/models.py:550 msgid "rule type" msgstr "" -#: documents/models.py:564 +#: documents/models.py:552 msgid "value" msgstr "" -#: documents/models.py:567 +#: documents/models.py:555 msgid "filter rule" msgstr "" -#: documents/models.py:568 +#: documents/models.py:556 msgid "filter rules" msgstr "" -#: documents/models.py:592 +#: documents/models.py:580 msgid "Auto Task" msgstr "" -#: documents/models.py:593 +#: documents/models.py:581 msgid "Scheduled Task" msgstr "" -#: documents/models.py:594 +#: documents/models.py:582 msgid "Manual Task" msgstr "" -#: documents/models.py:597 +#: documents/models.py:585 msgid "Consume File" msgstr "" -#: documents/models.py:598 +#: documents/models.py:586 msgid "Train Classifier" msgstr "" -#: documents/models.py:599 +#: documents/models.py:587 msgid "Check Sanity" msgstr "" -#: documents/models.py:600 +#: documents/models.py:588 msgid "Index Optimize" msgstr "" -#: documents/models.py:605 +#: documents/models.py:589 +msgid "LLM Index Update" +msgstr "" + +#: documents/models.py:594 msgid "Task ID" msgstr "" -#: documents/models.py:606 +#: documents/models.py:595 msgid "Celery ID for the Task that was run" msgstr "" -#: documents/models.py:611 +#: documents/models.py:600 msgid "Acknowledged" msgstr "" -#: documents/models.py:612 +#: documents/models.py:601 msgid "If the task is acknowledged via the frontend or API" msgstr "" -#: documents/models.py:618 +#: documents/models.py:607 msgid "Task Filename" msgstr "" -#: documents/models.py:619 +#: documents/models.py:608 msgid "Name of the file which the Task was run for" msgstr "" -#: documents/models.py:626 +#: documents/models.py:615 msgid "Task Name" msgstr "" -#: documents/models.py:627 +#: documents/models.py:616 msgid "Name of the task that was run" msgstr "" -#: documents/models.py:634 +#: documents/models.py:623 msgid "Task State" msgstr "" -#: documents/models.py:635 +#: documents/models.py:624 msgid "Current state of the task being run" msgstr "" -#: documents/models.py:641 +#: documents/models.py:630 msgid "Created DateTime" msgstr "" -#: documents/models.py:642 +#: documents/models.py:631 msgid "Datetime field when the task result was created in UTC" msgstr "" -#: documents/models.py:648 +#: documents/models.py:637 msgid "Started DateTime" msgstr "" -#: documents/models.py:649 +#: documents/models.py:638 msgid "Datetime field when the task was started in UTC" msgstr "" -#: documents/models.py:655 +#: documents/models.py:644 msgid "Completed DateTime" msgstr "" -#: documents/models.py:656 +#: documents/models.py:645 msgid "Datetime field when the task was completed in UTC" msgstr "" -#: documents/models.py:662 +#: documents/models.py:651 msgid "Result Data" msgstr "" -#: documents/models.py:664 +#: documents/models.py:653 msgid "The data returned by the task" msgstr "" -#: documents/models.py:672 +#: documents/models.py:661 msgid "Task Type" msgstr "" -#: documents/models.py:673 +#: documents/models.py:662 msgid "The type of task that was run" msgstr "" -#: documents/models.py:684 +#: documents/models.py:673 msgid "Note for the document" msgstr "" -#: documents/models.py:708 +#: documents/models.py:697 msgid "user" msgstr "" -#: documents/models.py:713 +#: documents/models.py:702 msgid "note" msgstr "" -#: documents/models.py:714 +#: documents/models.py:703 msgid "notes" msgstr "" -#: documents/models.py:722 +#: documents/models.py:711 msgid "Archive" msgstr "" -#: documents/models.py:723 +#: documents/models.py:712 msgid "Original" msgstr "" -#: documents/models.py:734 paperless_mail/models.py:75 +#: documents/models.py:723 documents/models.py:785 paperless_mail/models.py:75 msgid "expiration" msgstr "" -#: documents/models.py:741 +#: documents/models.py:730 documents/models.py:792 msgid "slug" msgstr "" -#: documents/models.py:773 +#: documents/models.py:762 msgid "share link" msgstr "" -#: documents/models.py:774 +#: documents/models.py:763 msgid "share links" msgstr "" -#: documents/models.py:786 +#: documents/models.py:771 +msgid "Pending" +msgstr "" + +#: documents/models.py:772 +msgid "Processing" +msgstr "" + +#: documents/models.py:773 +msgid "Ready" +msgstr "" + +#: documents/models.py:774 +msgid "Failed" +msgstr "" + +#: documents/models.py:821 +msgid "size (bytes)" +msgstr "" + +#: documents/models.py:827 +msgid "last error" +msgstr "" + +#: documents/models.py:834 +msgid "file path" +msgstr "" + +#: documents/models.py:840 +msgid "built at" +msgstr "" + +#: documents/models.py:853 +msgid "share link bundle" +msgstr "" + +#: documents/models.py:854 +msgid "share link bundles" +msgstr "" + +#: documents/models.py:857 +#, python-format +msgid "Share link bundle %(slug)s" +msgstr "" + +#: documents/models.py:883 msgid "String" msgstr "" -#: documents/models.py:787 +#: documents/models.py:884 msgid "URL" msgstr "" -#: documents/models.py:788 +#: documents/models.py:885 msgid "Date" msgstr "" -#: documents/models.py:789 +#: documents/models.py:886 msgid "Boolean" msgstr "" -#: documents/models.py:790 +#: documents/models.py:887 msgid "Integer" msgstr "" -#: documents/models.py:791 +#: documents/models.py:888 msgid "Float" msgstr "" -#: documents/models.py:792 +#: documents/models.py:889 msgid "Monetary" msgstr "" -#: documents/models.py:793 +#: documents/models.py:890 msgid "Document Link" msgstr "" -#: documents/models.py:794 +#: documents/models.py:891 msgid "Select" msgstr "" -#: documents/models.py:795 +#: documents/models.py:892 msgid "Long Text" msgstr "" -#: documents/models.py:807 +#: documents/models.py:904 msgid "data type" msgstr "" -#: documents/models.py:814 +#: documents/models.py:911 msgid "extra data" msgstr "" -#: documents/models.py:818 +#: documents/models.py:915 msgid "Extra data for the custom field, such as select options" msgstr "" -#: documents/models.py:824 +#: documents/models.py:921 msgid "custom field" msgstr "" -#: documents/models.py:825 +#: documents/models.py:922 msgid "custom fields" msgstr "" -#: documents/models.py:925 +#: documents/models.py:1022 msgid "custom field instance" msgstr "" -#: documents/models.py:926 +#: documents/models.py:1023 msgid "custom field instances" msgstr "" -#: documents/models.py:991 +#: documents/models.py:1088 msgid "Consumption Started" msgstr "" -#: documents/models.py:992 +#: documents/models.py:1089 msgid "Document Added" msgstr "" -#: documents/models.py:993 +#: documents/models.py:1090 msgid "Document Updated" msgstr "" -#: documents/models.py:994 +#: documents/models.py:1091 msgid "Scheduled" msgstr "" -#: documents/models.py:997 +#: documents/models.py:1094 msgid "Consume Folder" msgstr "" -#: documents/models.py:998 +#: documents/models.py:1095 msgid "Api Upload" msgstr "" -#: documents/models.py:999 +#: documents/models.py:1096 msgid "Mail Fetch" msgstr "" -#: documents/models.py:1000 +#: documents/models.py:1097 msgid "Web UI" msgstr "" -#: documents/models.py:1005 +#: documents/models.py:1102 msgid "Modified" msgstr "" -#: documents/models.py:1006 +#: documents/models.py:1103 msgid "Custom Field" msgstr "" -#: documents/models.py:1009 +#: documents/models.py:1106 msgid "Workflow Trigger Type" msgstr "" -#: documents/models.py:1021 +#: documents/models.py:1118 msgid "filter path" msgstr "" -#: documents/models.py:1026 +#: documents/models.py:1123 msgid "" "Only consume documents with a path that matches this if specified. Wildcards " "specified as * are allowed. Case insensitive." msgstr "" -#: documents/models.py:1033 +#: documents/models.py:1130 msgid "filter filename" msgstr "" -#: documents/models.py:1038 paperless_mail/models.py:200 +#: documents/models.py:1135 paperless_mail/models.py:200 msgid "" "Only consume documents which entirely match this filename if specified. " "Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." msgstr "" -#: documents/models.py:1049 +#: documents/models.py:1146 msgid "filter documents from this mail rule" msgstr "" -#: documents/models.py:1065 +#: documents/models.py:1162 msgid "has these tag(s)" msgstr "" -#: documents/models.py:1072 +#: documents/models.py:1169 msgid "has all of these tag(s)" msgstr "" -#: documents/models.py:1079 +#: documents/models.py:1176 msgid "does not have these tag(s)" msgstr "" -#: documents/models.py:1087 +#: documents/models.py:1184 msgid "has this document type" msgstr "" -#: documents/models.py:1094 +#: documents/models.py:1191 +msgid "has one of these document types" +msgstr "" + +#: documents/models.py:1198 msgid "does not have these document type(s)" msgstr "" -#: documents/models.py:1102 +#: documents/models.py:1206 msgid "has this correspondent" msgstr "" -#: documents/models.py:1109 +#: documents/models.py:1213 msgid "does not have these correspondent(s)" msgstr "" -#: documents/models.py:1117 +#: documents/models.py:1220 +msgid "has one of these correspondents" +msgstr "" + +#: documents/models.py:1228 msgid "has this storage path" msgstr "" -#: documents/models.py:1124 +#: documents/models.py:1235 +msgid "has one of these storage paths" +msgstr "" + +#: documents/models.py:1242 msgid "does not have these storage path(s)" msgstr "" -#: documents/models.py:1128 +#: documents/models.py:1246 msgid "filter custom field query" msgstr "" -#: documents/models.py:1131 +#: documents/models.py:1249 msgid "JSON-encoded custom field query expression." msgstr "" -#: documents/models.py:1135 +#: documents/models.py:1253 msgid "schedule offset days" msgstr "" -#: documents/models.py:1138 +#: documents/models.py:1256 msgid "The number of days to offset the schedule trigger by." msgstr "" -#: documents/models.py:1143 +#: documents/models.py:1261 msgid "schedule is recurring" msgstr "" -#: documents/models.py:1146 +#: documents/models.py:1264 msgid "If the schedule should be recurring." msgstr "" -#: documents/models.py:1151 +#: documents/models.py:1269 msgid "schedule recurring delay in days" msgstr "" -#: documents/models.py:1155 +#: documents/models.py:1273 msgid "The number of days between recurring schedule triggers." msgstr "" -#: documents/models.py:1160 +#: documents/models.py:1278 msgid "schedule date field" msgstr "" -#: documents/models.py:1165 +#: documents/models.py:1283 msgid "The field to check for a schedule trigger." msgstr "" -#: documents/models.py:1174 +#: documents/models.py:1292 msgid "schedule date custom field" msgstr "" -#: documents/models.py:1178 +#: documents/models.py:1296 msgid "workflow trigger" msgstr "" -#: documents/models.py:1179 +#: documents/models.py:1297 msgid "workflow triggers" msgstr "" -#: documents/models.py:1187 +#: documents/models.py:1305 msgid "email subject" msgstr "" -#: documents/models.py:1191 +#: documents/models.py:1309 msgid "" "The subject of the email, can include some placeholders, see documentation." msgstr "" -#: documents/models.py:1197 +#: documents/models.py:1315 msgid "email body" msgstr "" -#: documents/models.py:1200 +#: documents/models.py:1318 msgid "" "The body (message) of the email, can include some placeholders, see " "documentation." msgstr "" -#: documents/models.py:1206 +#: documents/models.py:1324 msgid "emails to" msgstr "" -#: documents/models.py:1209 +#: documents/models.py:1327 msgid "The destination email addresses, comma separated." msgstr "" -#: documents/models.py:1215 +#: documents/models.py:1333 msgid "include document in email" msgstr "" -#: documents/models.py:1226 +#: documents/models.py:1344 msgid "webhook url" msgstr "" -#: documents/models.py:1229 +#: documents/models.py:1347 msgid "The destination URL for the notification." msgstr "" -#: documents/models.py:1234 +#: documents/models.py:1352 msgid "use parameters" msgstr "" -#: documents/models.py:1239 +#: documents/models.py:1357 msgid "send as JSON" msgstr "" -#: documents/models.py:1243 +#: documents/models.py:1361 msgid "webhook parameters" msgstr "" -#: documents/models.py:1246 +#: documents/models.py:1364 msgid "The parameters to send with the webhook URL if body not used." msgstr "" -#: documents/models.py:1250 +#: documents/models.py:1368 msgid "webhook body" msgstr "" -#: documents/models.py:1253 +#: documents/models.py:1371 msgid "The body to send with the webhook URL if parameters not used." msgstr "" -#: documents/models.py:1257 +#: documents/models.py:1375 msgid "webhook headers" msgstr "" -#: documents/models.py:1260 +#: documents/models.py:1378 msgid "The headers to send with the webhook URL." msgstr "" -#: documents/models.py:1265 +#: documents/models.py:1383 msgid "include document in webhook" msgstr "" -#: documents/models.py:1276 +#: documents/models.py:1394 msgid "Assignment" msgstr "" -#: documents/models.py:1280 +#: documents/models.py:1398 msgid "Removal" msgstr "" -#: documents/models.py:1284 documents/templates/account/password_reset.html:15 +#: documents/models.py:1402 documents/templates/account/password_reset.html:15 msgid "Email" msgstr "" -#: documents/models.py:1288 +#: documents/models.py:1406 msgid "Webhook" msgstr "" -#: documents/models.py:1292 +#: documents/models.py:1410 msgid "Workflow Action Type" msgstr "" -#: documents/models.py:1298 -msgid "assign title" -msgstr "" - -#: documents/models.py:1302 -msgid "Assign a document title, must be a Jinja2 template, see documentation." -msgstr "" - -#: documents/models.py:1310 paperless_mail/models.py:274 -msgid "assign this tag" -msgstr "" - -#: documents/models.py:1319 paperless_mail/models.py:282 -msgid "assign this document type" -msgstr "" - -#: documents/models.py:1328 paperless_mail/models.py:296 -msgid "assign this correspondent" -msgstr "" - -#: documents/models.py:1337 -msgid "assign this storage path" -msgstr "" - -#: documents/models.py:1346 -msgid "assign this owner" -msgstr "" - -#: documents/models.py:1353 -msgid "grant view permissions to these users" -msgstr "" - -#: documents/models.py:1360 -msgid "grant view permissions to these groups" -msgstr "" - -#: documents/models.py:1367 -msgid "grant change permissions to these users" -msgstr "" - -#: documents/models.py:1374 -msgid "grant change permissions to these groups" -msgstr "" - -#: documents/models.py:1381 -msgid "assign these custom fields" -msgstr "" - -#: documents/models.py:1385 -msgid "custom field values" -msgstr "" - -#: documents/models.py:1389 -msgid "Optional values to assign to the custom fields." -msgstr "" - -#: documents/models.py:1398 -msgid "remove these tag(s)" -msgstr "" - -#: documents/models.py:1403 -msgid "remove all tags" -msgstr "" - -#: documents/models.py:1410 -msgid "remove these document type(s)" -msgstr "" - -#: documents/models.py:1415 -msgid "remove all document types" -msgstr "" - -#: documents/models.py:1422 -msgid "remove these correspondent(s)" -msgstr "" - -#: documents/models.py:1427 -msgid "remove all correspondents" -msgstr "" - -#: documents/models.py:1434 -msgid "remove these storage path(s)" -msgstr "" - -#: documents/models.py:1439 -msgid "remove all storage paths" -msgstr "" - -#: documents/models.py:1446 -msgid "remove these owner(s)" -msgstr "" - -#: documents/models.py:1451 -msgid "remove all owners" -msgstr "" - -#: documents/models.py:1458 -msgid "remove view permissions for these users" -msgstr "" - -#: documents/models.py:1465 -msgid "remove view permissions for these groups" -msgstr "" - -#: documents/models.py:1472 -msgid "remove change permissions for these users" -msgstr "" - -#: documents/models.py:1479 -msgid "remove change permissions for these groups" -msgstr "" - -#: documents/models.py:1484 -msgid "remove all permissions" -msgstr "" - -#: documents/models.py:1491 -msgid "remove these custom fields" -msgstr "" - -#: documents/models.py:1496 -msgid "remove all custom fields" -msgstr "" - -#: documents/models.py:1505 -msgid "email" -msgstr "" - -#: documents/models.py:1514 -msgid "webhook" -msgstr "" - -#: documents/models.py:1518 -msgid "workflow action" -msgstr "" - -#: documents/models.py:1519 -msgid "workflow actions" -msgstr "" - -#: documents/models.py:1528 paperless_mail/models.py:145 +#: documents/models.py:1415 documents/models.py:1648 +#: paperless_mail/models.py:145 msgid "order" msgstr "" -#: documents/models.py:1534 -msgid "triggers" +#: documents/models.py:1418 +msgid "assign title" msgstr "" -#: documents/models.py:1541 -msgid "actions" +#: documents/models.py:1422 +msgid "Assign a document title, must be a Jinja2 template, see documentation." msgstr "" -#: documents/models.py:1544 paperless_mail/models.py:154 -msgid "enabled" +#: documents/models.py:1430 paperless_mail/models.py:274 +msgid "assign this tag" msgstr "" -#: documents/models.py:1555 -msgid "workflow" +#: documents/models.py:1439 paperless_mail/models.py:282 +msgid "assign this document type" +msgstr "" + +#: documents/models.py:1448 paperless_mail/models.py:296 +msgid "assign this correspondent" +msgstr "" + +#: documents/models.py:1457 +msgid "assign this storage path" +msgstr "" + +#: documents/models.py:1466 +msgid "assign this owner" +msgstr "" + +#: documents/models.py:1473 +msgid "grant view permissions to these users" +msgstr "" + +#: documents/models.py:1480 +msgid "grant view permissions to these groups" +msgstr "" + +#: documents/models.py:1487 +msgid "grant change permissions to these users" +msgstr "" + +#: documents/models.py:1494 +msgid "grant change permissions to these groups" +msgstr "" + +#: documents/models.py:1501 +msgid "assign these custom fields" +msgstr "" + +#: documents/models.py:1505 +msgid "custom field values" +msgstr "" + +#: documents/models.py:1509 +msgid "Optional values to assign to the custom fields." +msgstr "" + +#: documents/models.py:1518 +msgid "remove these tag(s)" +msgstr "" + +#: documents/models.py:1523 +msgid "remove all tags" +msgstr "" + +#: documents/models.py:1530 +msgid "remove these document type(s)" +msgstr "" + +#: documents/models.py:1535 +msgid "remove all document types" +msgstr "" + +#: documents/models.py:1542 +msgid "remove these correspondent(s)" +msgstr "" + +#: documents/models.py:1547 +msgid "remove all correspondents" +msgstr "" + +#: documents/models.py:1554 +msgid "remove these storage path(s)" msgstr "" #: documents/models.py:1559 +msgid "remove all storage paths" +msgstr "" + +#: documents/models.py:1566 +msgid "remove these owner(s)" +msgstr "" + +#: documents/models.py:1571 +msgid "remove all owners" +msgstr "" + +#: documents/models.py:1578 +msgid "remove view permissions for these users" +msgstr "" + +#: documents/models.py:1585 +msgid "remove view permissions for these groups" +msgstr "" + +#: documents/models.py:1592 +msgid "remove change permissions for these users" +msgstr "" + +#: documents/models.py:1599 +msgid "remove change permissions for these groups" +msgstr "" + +#: documents/models.py:1604 +msgid "remove all permissions" +msgstr "" + +#: documents/models.py:1611 +msgid "remove these custom fields" +msgstr "" + +#: documents/models.py:1616 +msgid "remove all custom fields" +msgstr "" + +#: documents/models.py:1625 +msgid "email" +msgstr "" + +#: documents/models.py:1634 +msgid "webhook" +msgstr "" + +#: documents/models.py:1638 +msgid "workflow action" +msgstr "" + +#: documents/models.py:1639 +msgid "workflow actions" +msgstr "" + +#: documents/models.py:1654 +msgid "triggers" +msgstr "" + +#: documents/models.py:1661 +msgid "actions" +msgstr "" + +#: documents/models.py:1664 paperless_mail/models.py:154 +msgid "enabled" +msgstr "" + +#: documents/models.py:1675 +msgid "workflow" +msgstr "" + +#: documents/models.py:1679 msgid "workflow trigger type" msgstr "" -#: documents/models.py:1573 +#: documents/models.py:1693 msgid "date run" msgstr "" -#: documents/models.py:1579 +#: documents/models.py:1699 msgid "workflow run" msgstr "" -#: documents/models.py:1580 +#: documents/models.py:1700 msgid "workflow runs" msgstr "" -#: documents/serialisers.py:642 +#: documents/serialisers.py:654 msgid "Invalid color." msgstr "" -#: documents/serialisers.py:1846 +#: documents/serialisers.py:1896 #, python-format msgid "File type %(type)s not supported" msgstr "" -#: documents/serialisers.py:1890 +#: documents/serialisers.py:1940 #, python-format msgid "Custom field id must be an integer: %(id)s" msgstr "" -#: documents/serialisers.py:1897 +#: documents/serialisers.py:1947 #, python-format msgid "Custom field with id %(id)s does not exist" msgstr "" -#: documents/serialisers.py:1914 documents/serialisers.py:1924 +#: documents/serialisers.py:1964 documents/serialisers.py:1974 msgid "" "Custom fields must be a list of integers or an object mapping ids to values." msgstr "" -#: documents/serialisers.py:1919 +#: documents/serialisers.py:1969 msgid "Some custom fields don't exist or were specified twice." msgstr "" -#: documents/serialisers.py:2034 +#: documents/serialisers.py:2084 msgid "Invalid variable detected." msgstr "" +#: documents/serialisers.py:2286 +msgid "Duplicate document identifiers are not allowed." +msgstr "" + +#: documents/serialisers.py:2316 documents/views.py:2836 +#, python-format +msgid "Documents not found: %(ids)s" +msgstr "" + #: documents/templates/account/account_inactive.html:5 msgid "Paperless-ngx account inactive" msgstr "" @@ -1510,6 +1569,23 @@ msgstr "" msgid "Unable to parse URI {value}" msgstr "" +#: documents/views.py:2848 +#, python-format +msgid "Insufficient permissions to share document %(id)s." +msgstr "" + +#: documents/views.py:2891 +msgid "Bundle is already being processed." +msgstr "" + +#: documents/views.py:2948 +msgid "The share link bundle is still being prepared. Please try again later." +msgstr "" + +#: documents/views.py:2958 +msgid "The share link bundle is unavailable." +msgstr "" + #: paperless/apps.py:11 msgid "Paperless" msgstr "" @@ -1594,263 +1670,307 @@ msgstr "" msgid "CMYK" msgstr "" -#: paperless/models.py:83 +#: paperless/models.py:78 paperless/models.py:87 +msgid "OpenAI" +msgstr "" + +#: paperless/models.py:79 +msgid "Huggingface" +msgstr "" + +#: paperless/models.py:88 +msgid "Ollama" +msgstr "" + +#: paperless/models.py:97 msgid "Sets the output PDF type" msgstr "" -#: paperless/models.py:95 +#: paperless/models.py:109 msgid "Do OCR from page 1 to this value" msgstr "" -#: paperless/models.py:101 +#: paperless/models.py:115 msgid "Do OCR using these languages" msgstr "" -#: paperless/models.py:108 +#: paperless/models.py:122 msgid "Sets the OCR mode" msgstr "" -#: paperless/models.py:116 +#: paperless/models.py:130 msgid "Controls the generation of an archive file" msgstr "" -#: paperless/models.py:124 +#: paperless/models.py:138 msgid "Sets image DPI fallback value" msgstr "" -#: paperless/models.py:131 +#: paperless/models.py:145 msgid "Controls the unpaper cleaning" msgstr "" -#: paperless/models.py:138 +#: paperless/models.py:152 msgid "Enables deskew" msgstr "" -#: paperless/models.py:141 +#: paperless/models.py:155 msgid "Enables page rotation" msgstr "" -#: paperless/models.py:146 +#: paperless/models.py:160 msgid "Sets the threshold for rotation of pages" msgstr "" -#: paperless/models.py:152 +#: paperless/models.py:166 msgid "Sets the maximum image size for decompression" msgstr "" -#: paperless/models.py:158 +#: paperless/models.py:172 msgid "Sets the Ghostscript color conversion strategy" msgstr "" -#: paperless/models.py:166 +#: paperless/models.py:180 msgid "Adds additional user arguments for OCRMyPDF" msgstr "" -#: paperless/models.py:175 +#: paperless/models.py:189 msgid "Application title" msgstr "" -#: paperless/models.py:182 +#: paperless/models.py:196 msgid "Application logo" msgstr "" -#: paperless/models.py:197 +#: paperless/models.py:211 msgid "Enables barcode scanning" msgstr "" -#: paperless/models.py:203 +#: paperless/models.py:217 msgid "Enables barcode TIFF support" msgstr "" -#: paperless/models.py:209 +#: paperless/models.py:223 msgid "Sets the barcode string" msgstr "" -#: paperless/models.py:217 +#: paperless/models.py:231 msgid "Retains split pages" msgstr "" -#: paperless/models.py:223 +#: paperless/models.py:237 msgid "Enables ASN barcode" msgstr "" -#: paperless/models.py:229 +#: paperless/models.py:243 msgid "Sets the ASN barcode prefix" msgstr "" -#: paperless/models.py:237 +#: paperless/models.py:251 msgid "Sets the barcode upscale factor" msgstr "" -#: paperless/models.py:244 +#: paperless/models.py:258 msgid "Sets the barcode DPI" msgstr "" -#: paperless/models.py:251 +#: paperless/models.py:265 msgid "Sets the maximum pages for barcode" msgstr "" -#: paperless/models.py:258 +#: paperless/models.py:272 msgid "Enables tag barcode" msgstr "" -#: paperless/models.py:264 +#: paperless/models.py:278 msgid "Sets the tag barcode mapping" msgstr "" -#: paperless/models.py:269 +#: paperless/models.py:284 +msgid "Enables splitting on tag barcodes" +msgstr "" + +#: paperless/models.py:293 +msgid "Enables AI features" +msgstr "" + +#: paperless/models.py:299 +msgid "Sets the LLM embedding backend" +msgstr "" + +#: paperless/models.py:307 +msgid "Sets the LLM embedding model" +msgstr "" + +#: paperless/models.py:314 +msgid "Sets the LLM backend" +msgstr "" + +#: paperless/models.py:322 +msgid "Sets the LLM model" +msgstr "" + +#: paperless/models.py:329 +msgid "Sets the LLM API key" +msgstr "" + +#: paperless/models.py:336 +msgid "Sets the LLM endpoint, optional" +msgstr "" + +#: paperless/models.py:343 msgid "paperless application settings" msgstr "" -#: paperless/settings.py:768 +#: paperless/settings.py:819 msgid "English (US)" msgstr "" -#: paperless/settings.py:769 +#: paperless/settings.py:820 msgid "Arabic" msgstr "" -#: paperless/settings.py:770 +#: paperless/settings.py:821 msgid "Afrikaans" msgstr "" -#: paperless/settings.py:771 +#: paperless/settings.py:822 msgid "Belarusian" msgstr "" -#: paperless/settings.py:772 +#: paperless/settings.py:823 msgid "Bulgarian" msgstr "" -#: paperless/settings.py:773 +#: paperless/settings.py:824 msgid "Catalan" msgstr "" -#: paperless/settings.py:774 +#: paperless/settings.py:825 msgid "Czech" msgstr "" -#: paperless/settings.py:775 +#: paperless/settings.py:826 msgid "Danish" msgstr "" -#: paperless/settings.py:776 +#: paperless/settings.py:827 msgid "German" msgstr "" -#: paperless/settings.py:777 +#: paperless/settings.py:828 msgid "Greek" msgstr "" -#: paperless/settings.py:778 +#: paperless/settings.py:829 msgid "English (GB)" msgstr "" -#: paperless/settings.py:779 +#: paperless/settings.py:830 msgid "Spanish" msgstr "" -#: paperless/settings.py:780 +#: paperless/settings.py:831 msgid "Persian" msgstr "" -#: paperless/settings.py:781 +#: paperless/settings.py:832 msgid "Finnish" msgstr "" -#: paperless/settings.py:782 +#: paperless/settings.py:833 msgid "French" msgstr "" -#: paperless/settings.py:783 +#: paperless/settings.py:834 msgid "Hungarian" msgstr "" -#: paperless/settings.py:784 +#: paperless/settings.py:835 msgid "Indonesian" msgstr "" -#: paperless/settings.py:785 +#: paperless/settings.py:836 msgid "Italian" msgstr "" -#: paperless/settings.py:786 +#: paperless/settings.py:837 msgid "Japanese" msgstr "" -#: paperless/settings.py:787 +#: paperless/settings.py:838 msgid "Korean" msgstr "" -#: paperless/settings.py:788 +#: paperless/settings.py:839 msgid "Luxembourgish" msgstr "" -#: paperless/settings.py:789 +#: paperless/settings.py:840 msgid "Norwegian" msgstr "" -#: paperless/settings.py:790 +#: paperless/settings.py:841 msgid "Dutch" msgstr "" -#: paperless/settings.py:791 +#: paperless/settings.py:842 msgid "Polish" msgstr "" -#: paperless/settings.py:792 +#: paperless/settings.py:843 msgid "Portuguese (Brazil)" msgstr "" -#: paperless/settings.py:793 +#: paperless/settings.py:844 msgid "Portuguese" msgstr "" -#: paperless/settings.py:794 +#: paperless/settings.py:845 msgid "Romanian" msgstr "" -#: paperless/settings.py:795 +#: paperless/settings.py:846 msgid "Russian" msgstr "" -#: paperless/settings.py:796 +#: paperless/settings.py:847 msgid "Slovak" msgstr "" -#: paperless/settings.py:797 +#: paperless/settings.py:848 msgid "Slovenian" msgstr "" -#: paperless/settings.py:798 +#: paperless/settings.py:849 msgid "Serbian" msgstr "" -#: paperless/settings.py:799 +#: paperless/settings.py:850 msgid "Swedish" msgstr "" -#: paperless/settings.py:800 +#: paperless/settings.py:851 msgid "Turkish" msgstr "" -#: paperless/settings.py:801 +#: paperless/settings.py:852 msgid "Ukrainian" msgstr "" -#: paperless/settings.py:802 +#: paperless/settings.py:853 msgid "Vietnamese" msgstr "" -#: paperless/settings.py:803 +#: paperless/settings.py:854 msgid "Chinese Simplified" msgstr "" -#: paperless/settings.py:804 +#: paperless/settings.py:855 msgid "Chinese Traditional" msgstr "" -#: paperless/urls.py:370 +#: paperless/urls.py:379 msgid "Paperless-ngx administration" msgstr "" diff --git a/src/paperless/adapter.py b/src/paperless/adapter.py index a4506275e..bd6a86f77 100644 --- a/src/paperless/adapter.py +++ b/src/paperless/adapter.py @@ -3,12 +3,15 @@ from urllib.parse import quote from allauth.account.adapter import DefaultAccountAdapter from allauth.core import context +from allauth.headless.tokens.strategies.sessions import SessionTokenStrategy from allauth.socialaccount.adapter import DefaultSocialAccountAdapter from django.conf import settings from django.contrib.auth.models import Group from django.contrib.auth.models import User from django.forms import ValidationError +from django.http import HttpRequest from django.urls import reverse +from rest_framework.authtoken.models import Token from documents.models import Document from paperless.signals import handle_social_account_updated @@ -159,3 +162,11 @@ class CustomSocialAccountAdapter(DefaultSocialAccountAdapter): exception, extra_context, ) + + +class DrfTokenStrategy(SessionTokenStrategy): + def create_access_token(self, request: HttpRequest) -> str | None: + if not request.user.is_authenticated: + return None + token, _ = Token.objects.get_or_create(user=request.user) + return token.key diff --git a/src/paperless/config.py b/src/paperless/config.py index fb3139d79..65d790ff4 100644 --- a/src/paperless/config.py +++ b/src/paperless/config.py @@ -116,6 +116,7 @@ class BarcodeConfig(BaseConfig): barcode_max_pages: int = dataclasses.field(init=False) barcode_enable_tag: bool = dataclasses.field(init=False) barcode_tag_mapping: dict[str, str] = dataclasses.field(init=False) + barcode_tag_split: bool = dataclasses.field(init=False) def __post_init__(self) -> None: app_config = self._get_config_instance() @@ -153,6 +154,9 @@ class BarcodeConfig(BaseConfig): self.barcode_tag_mapping = ( app_config.barcode_tag_mapping or settings.CONSUMER_TAG_BARCODE_MAPPING ) + self.barcode_tag_split = ( + app_config.barcode_tag_split or settings.CONSUMER_TAG_BARCODE_SPLIT + ) @dataclasses.dataclass @@ -169,3 +173,37 @@ class GeneralConfig(BaseConfig): self.app_title = app_config.app_title or None self.app_logo = app_config.app_logo.url if app_config.app_logo else None + + +@dataclasses.dataclass +class AIConfig(BaseConfig): + """ + AI related settings that require global scope + """ + + ai_enabled: bool = dataclasses.field(init=False) + llm_embedding_backend: str = dataclasses.field(init=False) + llm_embedding_model: str = dataclasses.field(init=False) + llm_backend: str = dataclasses.field(init=False) + llm_model: str = dataclasses.field(init=False) + llm_api_key: str = dataclasses.field(init=False) + llm_endpoint: str = dataclasses.field(init=False) + + def __post_init__(self) -> None: + app_config = self._get_config_instance() + + self.ai_enabled = app_config.ai_enabled or settings.AI_ENABLED + self.llm_embedding_backend = ( + app_config.llm_embedding_backend or settings.LLM_EMBEDDING_BACKEND + ) + self.llm_embedding_model = ( + app_config.llm_embedding_model or settings.LLM_EMBEDDING_MODEL + ) + self.llm_backend = app_config.llm_backend or settings.LLM_BACKEND + self.llm_model = app_config.llm_model or settings.LLM_MODEL + self.llm_api_key = app_config.llm_api_key or settings.LLM_API_KEY + self.llm_endpoint = app_config.llm_endpoint or settings.LLM_ENDPOINT + + @property + def llm_index_enabled(self) -> bool: + return bool(self.ai_enabled and self.llm_embedding_backend) diff --git a/src/paperless/db.py b/src/paperless/db.py deleted file mode 100644 index 286ccb094..000000000 --- a/src/paperless/db.py +++ /dev/null @@ -1,17 +0,0 @@ -import gnupg -from django.conf import settings - - -class GnuPG: - """ - A handy singleton to use when handling encrypted files. - """ - - gpg = gnupg.GPG(gnupghome=settings.GNUPG_HOME) - - @classmethod - def decrypted(cls, file_handle, passphrase=None): - if not passphrase: - passphrase = settings.PASSPHRASE - - return cls.gpg.decrypt_file(file_handle, passphrase=passphrase).data diff --git a/src/paperless/migrations/0005_applicationconfiguration_ai_enabled_and_more.py b/src/paperless/migrations/0005_applicationconfiguration_ai_enabled_and_more.py new file mode 100644 index 000000000..f8a71ea6b --- /dev/null +++ b/src/paperless/migrations/0005_applicationconfiguration_ai_enabled_and_more.py @@ -0,0 +1,84 @@ +# Generated by Django 5.2.6 on 2025-09-30 17:43 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("paperless", "0004_applicationconfiguration_barcode_asn_prefix_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="applicationconfiguration", + name="ai_enabled", + field=models.BooleanField( + default=False, + null=True, + verbose_name="Enables AI features", + ), + ), + migrations.AddField( + model_name="applicationconfiguration", + name="llm_api_key", + field=models.CharField( + blank=True, + max_length=1024, + null=True, + verbose_name="Sets the LLM API key", + ), + ), + migrations.AddField( + model_name="applicationconfiguration", + name="llm_backend", + field=models.CharField( + blank=True, + choices=[("openai", "OpenAI"), ("ollama", "Ollama")], + max_length=128, + null=True, + verbose_name="Sets the LLM backend", + ), + ), + migrations.AddField( + model_name="applicationconfiguration", + name="llm_embedding_backend", + field=models.CharField( + blank=True, + choices=[("openai", "OpenAI"), ("huggingface", "Huggingface")], + max_length=128, + null=True, + verbose_name="Sets the LLM embedding backend", + ), + ), + migrations.AddField( + model_name="applicationconfiguration", + name="llm_embedding_model", + field=models.CharField( + blank=True, + max_length=128, + null=True, + verbose_name="Sets the LLM embedding model", + ), + ), + migrations.AddField( + model_name="applicationconfiguration", + name="llm_endpoint", + field=models.CharField( + blank=True, + max_length=256, + null=True, + verbose_name="Sets the LLM endpoint, optional", + ), + ), + migrations.AddField( + model_name="applicationconfiguration", + name="llm_model", + field=models.CharField( + blank=True, + max_length=128, + null=True, + verbose_name="Sets the LLM model", + ), + ), + ] diff --git a/src/paperless/migrations/0006_applicationconfiguration_barcode_tag_split.py b/src/paperless/migrations/0006_applicationconfiguration_barcode_tag_split.py new file mode 100644 index 000000000..03b2d14cc --- /dev/null +++ b/src/paperless/migrations/0006_applicationconfiguration_barcode_tag_split.py @@ -0,0 +1,21 @@ +# Generated by Django 5.1.7 on 2025-12-15 21:30 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("paperless", "0005_applicationconfiguration_ai_enabled_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="applicationconfiguration", + name="barcode_tag_split", + field=models.BooleanField( + null=True, + verbose_name="Enables splitting on tag barcodes", + ), + ), + ] diff --git a/src/paperless/models.py b/src/paperless/models.py index 1c44f1414..725bff683 100644 --- a/src/paperless/models.py +++ b/src/paperless/models.py @@ -74,6 +74,20 @@ class ColorConvertChoices(models.TextChoices): CMYK = ("CMYK", _("CMYK")) +class LLMEmbeddingBackend(models.TextChoices): + OPENAI = ("openai", _("OpenAI")) + HUGGINGFACE = ("huggingface", _("Huggingface")) + + +class LLMBackend(models.TextChoices): + """ + Matches to --llm-backend + """ + + OPENAI = ("openai", _("OpenAI")) + OLLAMA = ("ollama", _("Ollama")) + + class ApplicationConfiguration(AbstractSingletonModel): """ Settings which are common across more than 1 parser @@ -265,6 +279,66 @@ class ApplicationConfiguration(AbstractSingletonModel): null=True, ) + # PAPERLESS_CONSUMER_TAG_BARCODE_SPLIT + barcode_tag_split = models.BooleanField( + verbose_name=_("Enables splitting on tag barcodes"), + null=True, + ) + + """ + AI related settings + """ + + ai_enabled = models.BooleanField( + verbose_name=_("Enables AI features"), + null=True, + default=False, + ) + + llm_embedding_backend = models.CharField( + verbose_name=_("Sets the LLM embedding backend"), + blank=True, + null=True, + max_length=128, + choices=LLMEmbeddingBackend.choices, + ) + + llm_embedding_model = models.CharField( + verbose_name=_("Sets the LLM embedding model"), + blank=True, + null=True, + max_length=128, + ) + + llm_backend = models.CharField( + verbose_name=_("Sets the LLM backend"), + blank=True, + null=True, + max_length=128, + choices=LLMBackend.choices, + ) + + llm_model = models.CharField( + verbose_name=_("Sets the LLM model"), + blank=True, + null=True, + max_length=128, + ) + + llm_api_key = models.CharField( + verbose_name=_("Sets the LLM API key"), + blank=True, + null=True, + max_length=1024, + ) + + llm_endpoint = models.CharField( + verbose_name=_("Sets the LLM endpoint, optional"), + blank=True, + null=True, + max_length=256, + ) + class Meta: verbose_name = _("paperless application settings") diff --git a/src/paperless/serialisers.py b/src/paperless/serialisers.py index 256f40680..97a2bee7e 100644 --- a/src/paperless/serialisers.py +++ b/src/paperless/serialisers.py @@ -206,6 +206,10 @@ class ProfileSerializer(PasswordValidationMixin, serializers.ModelSerializer): class ApplicationConfigurationSerializer(serializers.ModelSerializer): user_args = serializers.JSONField(binary=True, allow_null=True) barcode_tag_mapping = serializers.JSONField(binary=True, allow_null=True) + llm_api_key = ObfuscatedPasswordField( + required=False, + allow_null=True, + ) def run_validation(self, data): # Empty strings treated as None to avoid unexpected behavior @@ -215,6 +219,11 @@ class ApplicationConfigurationSerializer(serializers.ModelSerializer): data["barcode_tag_mapping"] = None if "language" in data and data["language"] == "": data["language"] = None + if "llm_api_key" in data and data["llm_api_key"] is not None: + if data["llm_api_key"] == "": + data["llm_api_key"] = None + elif len(data["llm_api_key"].replace("*", "")) == 0: + del data["llm_api_key"] return super().run_validation(data) def update(self, instance, validated_data): diff --git a/src/paperless/settings.py b/src/paperless/settings.py index 1cd357f86..88213813a 100644 --- a/src/paperless/settings.py +++ b/src/paperless/settings.py @@ -12,6 +12,7 @@ from typing import Final from urllib.parse import urlparse from celery.schedules import crontab +from compression_middleware.middleware import CompressionMiddleware from dateparser.languages.loader import LocaleDataLoader from django.utils.translation import gettext_lazy as _ from dotenv import load_dotenv @@ -229,6 +230,28 @@ def _parse_beat_schedule() -> dict: "expires": 59.0 * 60.0, }, }, + { + "name": "Rebuild LLM index", + "env_key": "PAPERLESS_LLM_INDEX_TASK_CRON", + # Default daily at 02:10 + "env_default": "10 2 * * *", + "task": "documents.tasks.llmindex_index", + "options": { + # 1 hour before default schedule sends again + "expires": 23.0 * 60.0 * 60.0, + }, + }, + { + "name": "Cleanup expired share link bundles", + "env_key": "PAPERLESS_SHARE_LINK_BUNDLE_CLEANUP_CRON", + # Default daily at 02:00 + "env_default": "0 2 * * *", + "task": "documents.tasks.cleanup_expired_share_link_bundles", + "options": { + # 1 hour before default schedule sends again + "expires": 23.0 * 60.0 * 60.0, + }, + }, ] for task in tasks: # Either get the environment setting or use the default @@ -267,6 +290,7 @@ MEDIA_ROOT = __get_path("PAPERLESS_MEDIA_ROOT", BASE_DIR.parent / "media") ORIGINALS_DIR = MEDIA_ROOT / "documents" / "originals" ARCHIVE_DIR = MEDIA_ROOT / "documents" / "archive" THUMBNAIL_DIR = MEDIA_ROOT / "documents" / "thumbnails" +SHARE_LINK_BUNDLE_DIR = MEDIA_ROOT / "documents" / "share_link_bundles" DATA_DIR = __get_path("PAPERLESS_DATA_DIR", BASE_DIR.parent / "data") @@ -287,6 +311,7 @@ MODEL_FILE = __get_path( "PAPERLESS_MODEL_FILE", DATA_DIR / "classification_model.pickle", ) +LLM_INDEX_DIR = DATA_DIR / "llm_index" LOGGING_DIR = __get_path("PAPERLESS_LOGGING_DIR", DATA_DIR / "log") @@ -332,6 +357,7 @@ INSTALLED_APPS = [ "allauth.account", "allauth.socialaccount", "allauth.mfa", + "allauth.headless", "drf_spectacular", "drf_spectacular_sidecar", "treenode", @@ -380,6 +406,19 @@ MIDDLEWARE = [ if __get_boolean("PAPERLESS_ENABLE_COMPRESSION", "yes"): # pragma: no cover MIDDLEWARE.insert(0, "compression_middleware.middleware.CompressionMiddleware") +# Workaround to not compress streaming responses (e.g. chat). +# See https://github.com/friedelwolff/django-compression-middleware/pull/7 +original_process_response = CompressionMiddleware.process_response + + +def patched_process_response(self, request, response): + if getattr(request, "compress_exempt", False): + return response + return original_process_response(self, request, response) + + +CompressionMiddleware.process_response = patched_process_response + ROOT_URLCONF = "paperless.urls" @@ -513,6 +552,12 @@ SOCIALACCOUNT_PROVIDERS = json.loads( ) SOCIAL_ACCOUNT_DEFAULT_GROUPS = __get_list("PAPERLESS_SOCIAL_ACCOUNT_DEFAULT_GROUPS") SOCIAL_ACCOUNT_SYNC_GROUPS = __get_boolean("PAPERLESS_SOCIAL_ACCOUNT_SYNC_GROUPS") +SOCIAL_ACCOUNT_SYNC_GROUPS_CLAIM: Final[str] = os.getenv( + "PAPERLESS_SOCIAL_ACCOUNT_SYNC_GROUPS_CLAIM", + "groups", +) + +HEADLESS_TOKEN_STRATEGY = "paperless.adapter.DrfTokenStrategy" MFA_TOTP_ISSUER = "Paperless-ngx" @@ -585,6 +630,10 @@ X_FRAME_OPTIONS = "SAMEORIGIN" # The next 3 settings can also be set using just PAPERLESS_URL CSRF_TRUSTED_ORIGINS = __get_list("PAPERLESS_CSRF_TRUSTED_ORIGINS") +if DEBUG: + # Allow access from the angular development server during debugging + CSRF_TRUSTED_ORIGINS.append("http://localhost:4200") + # We allow CORS from localhost:8000 CORS_ALLOWED_ORIGINS = __get_list( "PAPERLESS_CORS_ALLOWED_HOSTS", @@ -595,6 +644,8 @@ if DEBUG: # Allow access from the angular development server during debugging CORS_ALLOWED_ORIGINS.append("http://localhost:4200") +CORS_ALLOW_CREDENTIALS = True + CORS_EXPOSE_HEADERS = [ "Content-Disposition", ] @@ -868,6 +919,7 @@ LOGGING = { "loggers": { "paperless": {"handlers": ["file_paperless"], "level": "DEBUG"}, "paperless_mail": {"handlers": ["file_mail"], "level": "DEBUG"}, + "paperless_ai": {"handlers": ["file_paperless"], "level": "DEBUG"}, "ocrmypdf": {"handlers": ["file_paperless"], "level": "INFO"}, "celery": {"handlers": ["file_celery"], "level": "DEBUG"}, "kombu": {"handlers": ["file_celery"], "level": "DEBUG"}, @@ -1011,29 +1063,30 @@ IGNORABLE_FILES: Final[list[str]] = [ "Thumbs.db", ] -CONSUMER_POLLING = int(os.getenv("PAPERLESS_CONSUMER_POLLING", 0)) +CONSUMER_POLLING_INTERVAL = float(os.getenv("PAPERLESS_CONSUMER_POLLING_INTERVAL", 0)) -CONSUMER_POLLING_DELAY = int(os.getenv("PAPERLESS_CONSUMER_POLLING_DELAY", 5)) - -CONSUMER_POLLING_RETRY_COUNT = int( - os.getenv("PAPERLESS_CONSUMER_POLLING_RETRY_COUNT", 5), -) - -CONSUMER_INOTIFY_DELAY: Final[float] = __get_float( - "PAPERLESS_CONSUMER_INOTIFY_DELAY", - 0.5, -) +CONSUMER_STABILITY_DELAY = float(os.getenv("PAPERLESS_CONSUMER_STABILITY_DELAY", 5)) CONSUMER_DELETE_DUPLICATES = __get_boolean("PAPERLESS_CONSUMER_DELETE_DUPLICATES") CONSUMER_RECURSIVE = __get_boolean("PAPERLESS_CONSUMER_RECURSIVE") -# Ignore glob patterns, relative to PAPERLESS_CONSUMPTION_DIR +# Ignore regex patterns, matched against filename only CONSUMER_IGNORE_PATTERNS = list( json.loads( os.getenv( "PAPERLESS_CONSUMER_IGNORE_PATTERNS", - json.dumps(IGNORABLE_FILES), + json.dumps([]), + ), + ), +) + +# Directories to always ignore. These are matched by directory name, not full path +CONSUMER_IGNORE_DIRS = list( + json.loads( + os.getenv( + "PAPERLESS_CONSUMER_IGNORE_DIRS", + json.dumps([]), ), ), ) @@ -1096,6 +1149,10 @@ CONSUMER_TAG_BARCODE_MAPPING = dict( ), ) +CONSUMER_TAG_BARCODE_SPLIT: Final[bool] = __get_boolean( + "PAPERLESS_CONSUMER_TAG_BARCODE_SPLIT", +) + CONSUMER_ENABLE_COLLATE_DOUBLE_SIDED: Final[bool] = __get_boolean( "PAPERLESS_CONSUMER_ENABLE_COLLATE_DOUBLE_SIDED", ) @@ -1169,19 +1226,6 @@ EMAIL_PARSE_DEFAULT_LAYOUT = __get_int( 1, # MailRule.PdfLayout.TEXT_HTML but that can't be imported here ) -# Pre-2.x versions of Paperless stored your documents locally with GPG -# encryption, but that is no longer the default. This behaviour is still -# available, but it must be explicitly enabled by setting -# `PAPERLESS_PASSPHRASE` in your environment or config file. The default is to -# store these files unencrypted. -# -# Translation: -# * If you're a new user, you can safely ignore this setting. -# * If you're upgrading from 1.x, this must be set, OR you can run -# `./manage.py change_storage_type gpg unencrypted` to decrypt your files, -# after which you can unset this value. -PASSPHRASE = os.getenv("PAPERLESS_PASSPHRASE") - # Trigger a script after every successful document consumption? PRE_CONSUME_SCRIPT = os.getenv("PAPERLESS_PRE_CONSUME_SCRIPT") POST_CONSUME_SCRIPT = os.getenv("PAPERLESS_POST_CONSUME_SCRIPT") @@ -1404,3 +1448,16 @@ WEBHOOKS_ALLOW_INTERNAL_REQUESTS = __get_boolean( REMOTE_OCR_ENGINE = os.getenv("PAPERLESS_REMOTE_OCR_ENGINE") REMOTE_OCR_API_KEY = os.getenv("PAPERLESS_REMOTE_OCR_API_KEY") REMOTE_OCR_ENDPOINT = os.getenv("PAPERLESS_REMOTE_OCR_ENDPOINT") + +################################################################################ +# AI Settings # +################################################################################ +AI_ENABLED = __get_boolean("PAPERLESS_AI_ENABLED", "NO") +LLM_EMBEDDING_BACKEND = os.getenv( + "PAPERLESS_AI_LLM_EMBEDDING_BACKEND", +) # "huggingface" or "openai" +LLM_EMBEDDING_MODEL = os.getenv("PAPERLESS_AI_LLM_EMBEDDING_MODEL") +LLM_BACKEND = os.getenv("PAPERLESS_AI_LLM_BACKEND") # "ollama" or "openai" +LLM_MODEL = os.getenv("PAPERLESS_AI_LLM_MODEL") +LLM_API_KEY = os.getenv("PAPERLESS_AI_LLM_API_KEY") +LLM_ENDPOINT = os.getenv("PAPERLESS_AI_LLM_ENDPOINT") diff --git a/src/paperless/signals.py b/src/paperless/signals.py index cfad29dbd..1ed88c051 100644 --- a/src/paperless/signals.py +++ b/src/paperless/signals.py @@ -40,15 +40,19 @@ def handle_social_account_updated(sender, request, sociallogin, **kwargs): extra_data = sociallogin.account.extra_data or {} social_account_groups = extra_data.get( - "groups", + settings.SOCIAL_ACCOUNT_SYNC_GROUPS_CLAIM, [], ) # pre-allauth 65.11.0 structure if not social_account_groups: # allauth 65.11.0+ nests claims under `userinfo`/`id_token` social_account_groups = ( - extra_data.get("userinfo", {}).get("groups") - or extra_data.get("id_token", {}).get("groups") + extra_data.get("userinfo", {}).get( + settings.SOCIAL_ACCOUNT_SYNC_GROUPS_CLAIM, + ) + or extra_data.get("id_token", {}).get( + settings.SOCIAL_ACCOUNT_SYNC_GROUPS_CLAIM, + ) or [] ) if settings.SOCIAL_ACCOUNT_SYNC_GROUPS and social_account_groups is not None: diff --git a/src/paperless/tests/test_adapter.py b/src/paperless/tests/test_adapter.py index 37b8aaa3b..dbef3fde7 100644 --- a/src/paperless/tests/test_adapter.py +++ b/src/paperless/tests/test_adapter.py @@ -4,6 +4,7 @@ from allauth.account.adapter import get_adapter from allauth.core import context from allauth.socialaccount.adapter import get_adapter as get_social_adapter from django.conf import settings +from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import Group from django.contrib.auth.models import User from django.forms import ValidationError @@ -11,6 +12,9 @@ from django.http import HttpRequest from django.test import TestCase from django.test import override_settings from django.urls import reverse +from rest_framework.authtoken.models import Token + +from paperless.adapter import DrfTokenStrategy class TestCustomAccountAdapter(TestCase): @@ -181,3 +185,74 @@ class TestCustomSocialAccountAdapter(TestCase): self.assertTrue( any("Test authentication error" in message for message in log_cm.output), ) + + +class TestDrfTokenStrategy(TestCase): + def test_create_access_token_creates_new_token(self): + """ + GIVEN: + - A user with no existing DRF token + WHEN: + - create_access_token is called + THEN: + - A new token is created and its key is returned + """ + + user = User.objects.create_user("testuser") + request = HttpRequest() + request.user = user + + strategy = DrfTokenStrategy() + token_key = strategy.create_access_token(request) + + # Verify a token was created + self.assertIsNotNone(token_key) + self.assertTrue(Token.objects.filter(user=user).exists()) + + # Verify the returned key matches the created token + token = Token.objects.get(user=user) + self.assertEqual(token_key, token.key) + + def test_create_access_token_returns_existing_token(self): + """ + GIVEN: + - A user with an existing DRF token + WHEN: + - create_access_token is called again + THEN: + - The same token key is returned (no new token created) + """ + + user = User.objects.create_user("testuser") + existing_token = Token.objects.create(user=user) + + request = HttpRequest() + request.user = user + + strategy = DrfTokenStrategy() + token_key = strategy.create_access_token(request) + + # Verify the existing token key is returned + self.assertEqual(token_key, existing_token.key) + + # Verify only one token exists (no duplicate created) + self.assertEqual(Token.objects.filter(user=user).count(), 1) + + def test_create_access_token_returns_none_for_unauthenticated_user(self): + """ + GIVEN: + - An unauthenticated request + WHEN: + - create_access_token is called + THEN: + - None is returned and no token is created + """ + + request = HttpRequest() + request.user = AnonymousUser() + + strategy = DrfTokenStrategy() + token_key = strategy.create_access_token(request) + + self.assertIsNone(token_key) + self.assertEqual(Token.objects.count(), 0) diff --git a/src/paperless/tests/test_settings.py b/src/paperless/tests/test_settings.py index 10995291e..9957de4fc 100644 --- a/src/paperless/tests/test_settings.py +++ b/src/paperless/tests/test_settings.py @@ -160,6 +160,8 @@ class TestCeleryScheduleParsing(TestCase): SANITY_EXPIRE_TIME = ((7.0 * 24.0) - 1.0) * 60.0 * 60.0 EMPTY_TRASH_EXPIRE_TIME = 23.0 * 60.0 * 60.0 RUN_SCHEDULED_WORKFLOWS_EXPIRE_TIME = 59.0 * 60.0 + LLM_INDEX_EXPIRE_TIME = 23.0 * 60.0 * 60.0 + CLEANUP_EXPIRED_SHARE_BUNDLES_EXPIRE_TIME = 23.0 * 60.0 * 60.0 def test_schedule_configuration_default(self): """ @@ -204,6 +206,20 @@ class TestCeleryScheduleParsing(TestCase): "schedule": crontab(minute="5", hour="*/1"), "options": {"expires": self.RUN_SCHEDULED_WORKFLOWS_EXPIRE_TIME}, }, + "Rebuild LLM index": { + "task": "documents.tasks.llmindex_index", + "schedule": crontab(minute=10, hour=2), + "options": { + "expires": self.LLM_INDEX_EXPIRE_TIME, + }, + }, + "Cleanup expired share link bundles": { + "task": "documents.tasks.cleanup_expired_share_link_bundles", + "schedule": crontab(minute=0, hour=2), + "options": { + "expires": self.CLEANUP_EXPIRED_SHARE_BUNDLES_EXPIRE_TIME, + }, + }, }, schedule, ) @@ -256,6 +272,20 @@ class TestCeleryScheduleParsing(TestCase): "schedule": crontab(minute="5", hour="*/1"), "options": {"expires": self.RUN_SCHEDULED_WORKFLOWS_EXPIRE_TIME}, }, + "Rebuild LLM index": { + "task": "documents.tasks.llmindex_index", + "schedule": crontab(minute=10, hour=2), + "options": { + "expires": self.LLM_INDEX_EXPIRE_TIME, + }, + }, + "Cleanup expired share link bundles": { + "task": "documents.tasks.cleanup_expired_share_link_bundles", + "schedule": crontab(minute=0, hour=2), + "options": { + "expires": self.CLEANUP_EXPIRED_SHARE_BUNDLES_EXPIRE_TIME, + }, + }, }, schedule, ) @@ -300,6 +330,20 @@ class TestCeleryScheduleParsing(TestCase): "schedule": crontab(minute="5", hour="*/1"), "options": {"expires": self.RUN_SCHEDULED_WORKFLOWS_EXPIRE_TIME}, }, + "Rebuild LLM index": { + "task": "documents.tasks.llmindex_index", + "schedule": crontab(minute=10, hour=2), + "options": { + "expires": self.LLM_INDEX_EXPIRE_TIME, + }, + }, + "Cleanup expired share link bundles": { + "task": "documents.tasks.cleanup_expired_share_link_bundles", + "schedule": crontab(minute=0, hour=2), + "options": { + "expires": self.CLEANUP_EXPIRED_SHARE_BUNDLES_EXPIRE_TIME, + }, + }, }, schedule, ) @@ -322,6 +366,8 @@ class TestCeleryScheduleParsing(TestCase): "PAPERLESS_INDEX_TASK_CRON": "disable", "PAPERLESS_EMPTY_TRASH_TASK_CRON": "disable", "PAPERLESS_WORKFLOW_SCHEDULED_TASK_CRON": "disable", + "PAPERLESS_LLM_INDEX_TASK_CRON": "disable", + "PAPERLESS_SHARE_LINK_BUNDLE_CLEANUP_CRON": "disable", }, ): schedule = _parse_beat_schedule() diff --git a/src/paperless/urls.py b/src/paperless/urls.py index e24d1a459..04b0c2200 100644 --- a/src/paperless/urls.py +++ b/src/paperless/urls.py @@ -18,6 +18,7 @@ from rest_framework.routers import DefaultRouter from documents.views import BulkDownloadView from documents.views import BulkEditObjectsView from documents.views import BulkEditView +from documents.views import ChatStreamingView from documents.views import CorrespondentViewSet from documents.views import CustomFieldViewSet from documents.views import DocumentTypeViewSet @@ -30,6 +31,7 @@ from documents.views import SavedViewViewSet from documents.views import SearchAutoCompleteView from documents.views import SelectionDataView from documents.views import SharedLinkView +from documents.views import ShareLinkBundleViewSet from documents.views import ShareLinkViewSet from documents.views import StatisticsView from documents.views import StoragePathViewSet @@ -72,6 +74,7 @@ api_router.register(r"users", UserViewSet, basename="users") api_router.register(r"groups", GroupViewSet, basename="groups") api_router.register(r"mail_accounts", MailAccountViewSet) api_router.register(r"mail_rules", MailRuleViewSet) +api_router.register(r"share_link_bundles", ShareLinkBundleViewSet) api_router.register(r"share_links", ShareLinkViewSet) api_router.register(r"workflow_triggers", WorkflowTriggerViewSet) api_router.register(r"workflow_actions", WorkflowActionViewSet) @@ -139,6 +142,11 @@ urlpatterns = [ SelectionDataView.as_view(), name="selection_data", ), + re_path( + "^chat/", + ChatStreamingView.as_view(), + name="chat_streaming_view", + ), ], ), ), @@ -222,6 +230,7 @@ urlpatterns = [ ], ), ), + re_path("^auth/headless/", include("allauth.headless.urls")), re_path( "^$", # Redirect to the API swagger view RedirectView.as_view(url="schema/view/"), diff --git a/src/paperless/version.py b/src/paperless/version.py index c0c6439d4..aeeee68e0 100644 --- a/src/paperless/version.py +++ b/src/paperless/version.py @@ -1,6 +1,6 @@ from typing import Final -__version__: Final[tuple[int, int, int]] = (2, 20, 3) +__version__: Final[tuple[int, int, int]] = (2, 20, 5) # Version string like X.Y.Z __full_version_str__: Final[str] = ".".join(map(str, __version__)) # Version string like X.Y diff --git a/src/paperless/views.py b/src/paperless/views.py index e79c0e668..f9aa68297 100644 --- a/src/paperless/views.py +++ b/src/paperless/views.py @@ -35,6 +35,7 @@ from rest_framework.viewsets import ModelViewSet from documents.index import DelayedQuery from documents.permissions import PaperlessObjectPermissions +from documents.tasks import llmindex_index from paperless.filters import GroupFilterSet from paperless.filters import UserFilterSet from paperless.models import ApplicationConfiguration @@ -43,6 +44,7 @@ from paperless.serialisers import GroupSerializer from paperless.serialisers import PaperlessAuthTokenSerializer from paperless.serialisers import ProfileSerializer from paperless.serialisers import UserSerializer +from paperless_ai.indexing import vector_store_file_exists class PaperlessObtainAuthTokenView(ObtainAuthToken): @@ -358,6 +360,30 @@ class ApplicationConfigurationViewSet(ModelViewSet): def create(self, request, *args, **kwargs): return Response(status=405) # Not Allowed + def perform_update(self, serializer): + old_instance = ApplicationConfiguration.objects.all().first() + old_ai_index_enabled = ( + old_instance.ai_enabled and old_instance.llm_embedding_backend + ) + + new_instance: ApplicationConfiguration = serializer.save() + new_ai_index_enabled = ( + new_instance.ai_enabled and new_instance.llm_embedding_backend + ) + + if ( + not old_ai_index_enabled + and new_ai_index_enabled + and not vector_store_file_exists() + ): + # AI index was just enabled and vector store file does not exist + llmindex_index.delay( + progress_bar_disable=True, + rebuild=True, + scheduled=False, + auto=True, + ) + @extend_schema_view( post=extend_schema( diff --git a/src/paperless_ai/__init__.py b/src/paperless_ai/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/paperless_ai/ai_classifier.py b/src/paperless_ai/ai_classifier.py new file mode 100644 index 000000000..e60ca37ff --- /dev/null +++ b/src/paperless_ai/ai_classifier.py @@ -0,0 +1,102 @@ +import logging + +from django.contrib.auth.models import User + +from documents.models import Document +from documents.permissions import get_objects_for_user_owner_aware +from paperless.config import AIConfig +from paperless_ai.client import AIClient +from paperless_ai.indexing import query_similar_documents +from paperless_ai.indexing import truncate_content + +logger = logging.getLogger("paperless_ai.rag_classifier") + + +def build_prompt_without_rag(document: Document) -> str: + filename = document.filename or "" + content = truncate_content(document.content[:4000] or "") + + return f""" + You are a document classification assistant. + + Analyze the following document and extract the following information: + - A short descriptive title + - Tags that reflect the content + - Names of people or organizations mentioned + - The type or category of the document + - Suggested folder paths for storing the document + - Up to 3 relevant dates in YYYY-MM-DD format + + Filename: + {filename} + + Content: + {content} + """.strip() + + +def build_prompt_with_rag(document: Document, user: User | None = None) -> str: + base_prompt = build_prompt_without_rag(document) + context = truncate_content(get_context_for_document(document, user)) + + return f"""{base_prompt} + + Additional context from similar documents: + {context} + """.strip() + + +def get_context_for_document( + doc: Document, + user: User | None = None, + max_docs: int = 5, +) -> str: + visible_documents = ( + get_objects_for_user_owner_aware( + user, + "view_document", + Document, + ) + if user + else None + ) + similar_docs = query_similar_documents( + document=doc, + document_ids=[document.pk for document in visible_documents] + if visible_documents + else None, + )[:max_docs] + context_blocks = [] + for similar in similar_docs: + text = similar.content[:1000] or "" + title = similar.title or similar.filename or "Untitled" + context_blocks.append(f"TITLE: {title}\n{text}") + return "\n\n".join(context_blocks) + + +def parse_ai_response(raw: dict) -> dict: + return { + "title": raw.get("title", ""), + "tags": raw.get("tags", []), + "correspondents": raw.get("correspondents", []), + "document_types": raw.get("document_types", []), + "storage_paths": raw.get("storage_paths", []), + "dates": raw.get("dates", []), + } + + +def get_ai_document_classification( + document: Document, + user: User | None = None, +) -> dict: + ai_config = AIConfig() + + prompt = ( + build_prompt_with_rag(document, user) + if ai_config.llm_embedding_backend + else build_prompt_without_rag(document) + ) + + client = AIClient() + result = client.run_llm_query(prompt) + return parse_ai_response(result) diff --git a/src/paperless_ai/base_model.py b/src/paperless_ai/base_model.py new file mode 100644 index 000000000..2924f2c8c --- /dev/null +++ b/src/paperless_ai/base_model.py @@ -0,0 +1,10 @@ +from llama_index.core.bridge.pydantic import BaseModel + + +class DocumentClassifierSchema(BaseModel): + title: str + tags: list[str] + correspondents: list[str] + document_types: list[str] + storage_paths: list[str] + dates: list[str] diff --git a/src/paperless_ai/chat.py b/src/paperless_ai/chat.py new file mode 100644 index 000000000..f662a7bee --- /dev/null +++ b/src/paperless_ai/chat.py @@ -0,0 +1,105 @@ +import logging +import sys + +from llama_index.core import VectorStoreIndex +from llama_index.core.prompts import PromptTemplate +from llama_index.core.query_engine import RetrieverQueryEngine + +from documents.models import Document +from paperless_ai.client import AIClient +from paperless_ai.indexing import load_or_build_index + +logger = logging.getLogger("paperless_ai.chat") + +MAX_SINGLE_DOC_CONTEXT_CHARS = 15000 +SINGLE_DOC_SNIPPET_CHARS = 800 + +CHAT_PROMPT_TMPL = PromptTemplate( + template="""Context information is below. + --------------------- + {context_str} + --------------------- + Given the context information and not prior knowledge, answer the query. + Query: {query_str} + Answer:""", +) + + +def stream_chat_with_documents(query_str: str, documents: list[Document]): + client = AIClient() + index = load_or_build_index() + + doc_ids = [str(doc.pk) for doc in documents] + + # Filter only the node(s) that match the document IDs + nodes = [ + node + for node in index.docstore.docs.values() + if node.metadata.get("document_id") in doc_ids + ] + + if len(nodes) == 0: + logger.warning("No nodes found for the given documents.") + yield "Sorry, I couldn't find any content to answer your question." + return + + local_index = VectorStoreIndex(nodes=nodes) + retriever = local_index.as_retriever( + similarity_top_k=3 if len(documents) == 1 else 5, + ) + + if len(documents) == 1: + # Just one doc — provide full content + doc = documents[0] + # TODO: include document metadata in the context + content = doc.content or "" + context_body = content + + if len(content) > MAX_SINGLE_DOC_CONTEXT_CHARS: + logger.info( + "Truncating single-document context from %s to %s characters", + len(content), + MAX_SINGLE_DOC_CONTEXT_CHARS, + ) + context_body = content[:MAX_SINGLE_DOC_CONTEXT_CHARS] + + top_nodes = retriever.retrieve(query_str) + if len(top_nodes) > 0: + snippets = "\n\n".join( + f"TITLE: {node.metadata.get('title')}\n{node.text[:SINGLE_DOC_SNIPPET_CHARS]}" + for node in top_nodes + ) + context_body = f"{context_body}\n\nTOP MATCHES:\n{snippets}" + + context = f"TITLE: {doc.title or doc.filename}\n{context_body}" + else: + top_nodes = retriever.retrieve(query_str) + + if len(top_nodes) == 0: + logger.warning("Retriever returned no nodes for the given documents.") + yield "Sorry, I couldn't find any content to answer your question." + return + + context = "\n\n".join( + f"TITLE: {node.metadata.get('title')}\n{node.text[:SINGLE_DOC_SNIPPET_CHARS]}" + for node in top_nodes + ) + + prompt = CHAT_PROMPT_TMPL.partial_format( + context_str=context, + query_str=query_str, + ).format(llm=client.llm) + + query_engine = RetrieverQueryEngine.from_args( + retriever=retriever, + llm=client.llm, + streaming=True, + ) + + logger.debug("Document chat prompt: %s", prompt) + + response_stream = query_engine.query(prompt) + + for chunk in response_stream.response_gen: + yield chunk + sys.stdout.flush() diff --git a/src/paperless_ai/client.py b/src/paperless_ai/client.py new file mode 100644 index 000000000..1f52c56c7 --- /dev/null +++ b/src/paperless_ai/client.py @@ -0,0 +1,69 @@ +import logging + +from llama_index.core.llms import ChatMessage +from llama_index.core.program.function_program import get_function_tool +from llama_index.llms.ollama import Ollama +from llama_index.llms.openai import OpenAI + +from paperless.config import AIConfig +from paperless_ai.base_model import DocumentClassifierSchema + +logger = logging.getLogger("paperless_ai.client") + + +class AIClient: + """ + A client for interacting with an LLM backend. + """ + + def __init__(self): + self.settings = AIConfig() + self.llm = self.get_llm() + + def get_llm(self) -> Ollama | OpenAI: + if self.settings.llm_backend == "ollama": + return Ollama( + model=self.settings.llm_model or "llama3.1", + base_url=self.settings.llm_endpoint or "http://localhost:11434", + request_timeout=120, + ) + elif self.settings.llm_backend == "openai": + return OpenAI( + model=self.settings.llm_model or "gpt-3.5-turbo", + api_base=self.settings.llm_endpoint or None, + api_key=self.settings.llm_api_key, + ) + else: + raise ValueError(f"Unsupported LLM backend: {self.settings.llm_backend}") + + def run_llm_query(self, prompt: str) -> str: + logger.debug( + "Running LLM query against %s with model %s", + self.settings.llm_backend, + self.settings.llm_model, + ) + + user_msg = ChatMessage(role="user", content=prompt) + tool = get_function_tool(DocumentClassifierSchema) + result = self.llm.chat_with_tools( + tools=[tool], + user_msg=user_msg, + chat_history=[], + ) + tool_calls = self.llm.get_tool_calls_from_response( + result, + error_on_no_tool_call=True, + ) + logger.debug("LLM query result: %s", tool_calls) + parsed = DocumentClassifierSchema(**tool_calls[0].tool_kwargs) + return parsed.model_dump() + + def run_chat(self, messages: list[ChatMessage]) -> str: + logger.debug( + "Running chat query against %s with model %s", + self.settings.llm_backend, + self.settings.llm_model, + ) + result = self.llm.chat(messages) + logger.debug("Chat result: %s", result) + return result diff --git a/src/paperless_ai/embedding.py b/src/paperless_ai/embedding.py new file mode 100644 index 000000000..993c9ae30 --- /dev/null +++ b/src/paperless_ai/embedding.py @@ -0,0 +1,92 @@ +import json +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pathlib import Path + +from django.conf import settings +from llama_index.core.base.embeddings.base import BaseEmbedding +from llama_index.embeddings.huggingface import HuggingFaceEmbedding +from llama_index.embeddings.openai import OpenAIEmbedding + +from documents.models import Document +from documents.models import Note +from paperless.config import AIConfig +from paperless.models import LLMEmbeddingBackend + + +def get_embedding_model() -> BaseEmbedding: + config = AIConfig() + + match config.llm_embedding_backend: + case LLMEmbeddingBackend.OPENAI: + return OpenAIEmbedding( + model=config.llm_embedding_model or "text-embedding-3-small", + api_key=config.llm_api_key, + ) + case LLMEmbeddingBackend.HUGGINGFACE: + return HuggingFaceEmbedding( + model_name=config.llm_embedding_model + or "sentence-transformers/all-MiniLM-L6-v2", + ) + case _: + raise ValueError( + f"Unsupported embedding backend: {config.llm_embedding_backend}", + ) + + +def get_embedding_dim() -> int: + """ + Loads embedding dimension from meta.json if available, otherwise infers it + from a dummy embedding and stores it for future use. + """ + config = AIConfig() + model = config.llm_embedding_model or ( + "text-embedding-3-small" + if config.llm_embedding_backend == "openai" + else "sentence-transformers/all-MiniLM-L6-v2" + ) + + meta_path: Path = settings.LLM_INDEX_DIR / "meta.json" + if meta_path.exists(): + with meta_path.open() as f: + meta = json.load(f) + if meta.get("embedding_model") != model: + raise RuntimeError( + f"Embedding model changed from {meta.get('embedding_model')} to {model}. " + "You must rebuild the index.", + ) + return meta["dim"] + + embedding_model = get_embedding_model() + test_embed = embedding_model.get_text_embedding("test") + dim = len(test_embed) + + with meta_path.open("w") as f: + json.dump({"embedding_model": model, "dim": dim}, f) + + return dim + + +def build_llm_index_text(doc: Document) -> str: + lines = [ + f"Title: {doc.title}", + f"Filename: {doc.filename}", + f"Created: {doc.created}", + f"Added: {doc.added}", + f"Modified: {doc.modified}", + f"Tags: {', '.join(tag.name for tag in doc.tags.all())}", + f"Document Type: {doc.document_type.name if doc.document_type else ''}", + f"Correspondent: {doc.correspondent.name if doc.correspondent else ''}", + f"Storage Path: {doc.storage_path.name if doc.storage_path else ''}", + f"Archive Serial Number: {doc.archive_serial_number or ''}", + f"Notes: {','.join([str(c.note) for c in Note.objects.filter(document=doc)])}", + ] + + for instance in doc.custom_fields.all(): + lines.append(f"Custom Field - {instance.field.name}: {instance}") + + lines.append("\nContent:\n") + lines.append(doc.content or "") + + return "\n".join(lines) diff --git a/src/paperless_ai/indexing.py b/src/paperless_ai/indexing.py new file mode 100644 index 000000000..654c56f3b --- /dev/null +++ b/src/paperless_ai/indexing.py @@ -0,0 +1,321 @@ +import logging +import shutil +from datetime import timedelta +from pathlib import Path + +import faiss +import llama_index.core.settings as llama_settings +import tqdm +from celery import states +from django.conf import settings +from django.utils import timezone +from llama_index.core import Document as LlamaDocument +from llama_index.core import StorageContext +from llama_index.core import VectorStoreIndex +from llama_index.core import load_index_from_storage +from llama_index.core.indices.prompt_helper import PromptHelper +from llama_index.core.node_parser import SimpleNodeParser +from llama_index.core.prompts import PromptTemplate +from llama_index.core.retrievers import VectorIndexRetriever +from llama_index.core.schema import BaseNode +from llama_index.core.storage.docstore import SimpleDocumentStore +from llama_index.core.storage.index_store import SimpleIndexStore +from llama_index.core.text_splitter import TokenTextSplitter +from llama_index.vector_stores.faiss import FaissVectorStore + +from documents.models import Document +from documents.models import PaperlessTask +from paperless_ai.embedding import build_llm_index_text +from paperless_ai.embedding import get_embedding_dim +from paperless_ai.embedding import get_embedding_model + +logger = logging.getLogger("paperless_ai.indexing") + + +def queue_llm_index_update_if_needed(*, rebuild: bool, reason: str) -> bool: + from documents.tasks import llmindex_index + + has_running = PaperlessTask.objects.filter( + task_name=PaperlessTask.TaskName.LLMINDEX_UPDATE, + status__in=[states.PENDING, states.STARTED], + ).exists() + has_recent = PaperlessTask.objects.filter( + task_name=PaperlessTask.TaskName.LLMINDEX_UPDATE, + date_created__gte=(timezone.now() - timedelta(minutes=5)), + ).exists() + if has_running or has_recent: + return False + + llmindex_index.delay(rebuild=rebuild, scheduled=False, auto=True) + logger.warning( + "Queued LLM index update%s: %s", + " (rebuild)" if rebuild else "", + reason, + ) + return True + + +def get_or_create_storage_context(*, rebuild=False): + """ + Loads or creates the StorageContext (vector store, docstore, index store). + If rebuild=True, deletes and recreates everything. + """ + if rebuild: + shutil.rmtree(settings.LLM_INDEX_DIR, ignore_errors=True) + settings.LLM_INDEX_DIR.mkdir(parents=True, exist_ok=True) + + if rebuild or not settings.LLM_INDEX_DIR.exists(): + embedding_dim = get_embedding_dim() + faiss_index = faiss.IndexFlatL2(embedding_dim) + vector_store = FaissVectorStore(faiss_index=faiss_index) + docstore = SimpleDocumentStore() + index_store = SimpleIndexStore() + else: + vector_store = FaissVectorStore.from_persist_dir(settings.LLM_INDEX_DIR) + docstore = SimpleDocumentStore.from_persist_dir(settings.LLM_INDEX_DIR) + index_store = SimpleIndexStore.from_persist_dir(settings.LLM_INDEX_DIR) + + return StorageContext.from_defaults( + docstore=docstore, + index_store=index_store, + vector_store=vector_store, + persist_dir=settings.LLM_INDEX_DIR, + ) + + +def build_document_node(document: Document) -> list[BaseNode]: + """ + Given a Document, returns parsed Nodes ready for indexing. + """ + text = build_llm_index_text(document) + metadata = { + "document_id": str(document.id), + "title": document.title, + "tags": [t.name for t in document.tags.all()], + "correspondent": document.correspondent.name + if document.correspondent + else None, + "document_type": document.document_type.name + if document.document_type + else None, + "created": document.created.isoformat() if document.created else None, + "added": document.added.isoformat() if document.added else None, + "modified": document.modified.isoformat(), + } + doc = LlamaDocument(text=text, metadata=metadata) + parser = SimpleNodeParser() + return parser.get_nodes_from_documents([doc]) + + +def load_or_build_index(nodes=None): + """ + Load an existing VectorStoreIndex if present, + or build a new one using provided nodes if storage is empty. + """ + embed_model = get_embedding_model() + llama_settings.Settings.embed_model = embed_model + storage_context = get_or_create_storage_context() + try: + return load_index_from_storage(storage_context=storage_context) + except ValueError as e: + logger.warning("Failed to load index from storage: %s", e) + if not nodes: + queue_llm_index_update_if_needed( + rebuild=vector_store_file_exists(), + reason="LLM index missing or invalid while loading.", + ) + logger.info("No nodes provided for index creation.") + raise + return VectorStoreIndex( + nodes=nodes, + storage_context=storage_context, + embed_model=embed_model, + ) + + +def remove_document_docstore_nodes(document: Document, index: VectorStoreIndex): + """ + Removes existing documents from docstore for a given document from the index. + This is necessary because FAISS IndexFlatL2 is append-only. + """ + all_node_ids = list(index.docstore.docs.keys()) + existing_nodes = [ + node.node_id + for node in index.docstore.get_nodes(all_node_ids) + if node.metadata.get("document_id") == str(document.id) + ] + for node_id in existing_nodes: + # Delete from docstore, FAISS IndexFlatL2 are append-only + index.docstore.delete_document(node_id) + + +def vector_store_file_exists(): + """ + Check if the vector store file exists in the LLM index directory. + """ + return Path(settings.LLM_INDEX_DIR / "default__vector_store.json").exists() + + +def update_llm_index(*, progress_bar_disable=False, rebuild=False) -> str: + """ + Rebuild or update the LLM index. + """ + nodes = [] + + documents = Document.objects.all() + if not documents.exists(): + msg = "No documents found to index." + logger.warning(msg) + return msg + + if rebuild or not vector_store_file_exists(): + # remove meta.json to force re-detection of embedding dim + (settings.LLM_INDEX_DIR / "meta.json").unlink(missing_ok=True) + # Rebuild index from scratch + logger.info("Rebuilding LLM index.") + embed_model = get_embedding_model() + llama_settings.Settings.embed_model = embed_model + storage_context = get_or_create_storage_context(rebuild=True) + for document in tqdm.tqdm(documents, disable=progress_bar_disable): + document_nodes = build_document_node(document) + nodes.extend(document_nodes) + + index = VectorStoreIndex( + nodes=nodes, + storage_context=storage_context, + embed_model=embed_model, + show_progress=not progress_bar_disable, + ) + msg = "LLM index rebuilt successfully." + else: + # Update existing index + index = load_or_build_index() + all_node_ids = list(index.docstore.docs.keys()) + existing_nodes = { + node.metadata.get("document_id"): node + for node in index.docstore.get_nodes(all_node_ids) + } + + for document in tqdm.tqdm(documents, disable=progress_bar_disable): + doc_id = str(document.id) + document_modified = document.modified.isoformat() + + if doc_id in existing_nodes: + node = existing_nodes[doc_id] + node_modified = node.metadata.get("modified") + + if node_modified == document_modified: + continue + + # Again, delete from docstore, FAISS IndexFlatL2 are append-only + index.docstore.delete_document(node.node_id) + nodes.extend(build_document_node(document)) + else: + # New document, add it + nodes.extend(build_document_node(document)) + + if nodes: + msg = "LLM index updated successfully." + logger.info( + "Updating %d nodes in LLM index.", + len(nodes), + ) + index.insert_nodes(nodes) + else: + msg = "No changes detected in LLM index." + logger.info(msg) + + index.storage_context.persist(persist_dir=settings.LLM_INDEX_DIR) + return msg + + +def llm_index_add_or_update_document(document: Document): + """ + Adds or updates a document in the LLM index. + If the document already exists, it will be replaced. + """ + new_nodes = build_document_node(document) + + index = load_or_build_index(nodes=new_nodes) + + remove_document_docstore_nodes(document, index) + + index.insert_nodes(new_nodes) + + index.storage_context.persist(persist_dir=settings.LLM_INDEX_DIR) + + +def llm_index_remove_document(document: Document): + """ + Removes a document from the LLM index. + """ + index = load_or_build_index() + + remove_document_docstore_nodes(document, index) + + index.storage_context.persist(persist_dir=settings.LLM_INDEX_DIR) + + +def truncate_content(content: str) -> str: + prompt_helper = PromptHelper( + context_window=8192, + num_output=512, + chunk_overlap_ratio=0.1, + chunk_size_limit=None, + ) + splitter = TokenTextSplitter(separator=" ", chunk_size=512, chunk_overlap=50) + content_chunks = splitter.split_text(content) + truncated_chunks = prompt_helper.truncate( + prompt=PromptTemplate(template="{content}"), + text_chunks=content_chunks, + padding=5, + ) + return " ".join(truncated_chunks) + + +def query_similar_documents( + document: Document, + top_k: int = 5, + document_ids: list[int] | None = None, +) -> list[Document]: + """ + Runs a similarity query and returns top-k similar Document objects. + """ + if not vector_store_file_exists(): + queue_llm_index_update_if_needed( + rebuild=False, + reason="LLM index not found for similarity query.", + ) + return [] + + index = load_or_build_index() + + # constrain only the node(s) that match the document IDs, if given + doc_node_ids = ( + [ + node.node_id + for node in index.docstore.docs.values() + if node.metadata.get("document_id") in document_ids + ] + if document_ids + else None + ) + + retriever = VectorIndexRetriever( + index=index, + similarity_top_k=top_k, + doc_ids=doc_node_ids, + ) + + query_text = truncate_content( + (document.title or "") + "\n" + (document.content or ""), + ) + results = retriever.retrieve(query_text) + + document_ids = [ + int(node.metadata["document_id"]) + for node in results + if "document_id" in node.metadata + ] + + return list(Document.objects.filter(pk__in=document_ids)) diff --git a/src/paperless_ai/matching.py b/src/paperless_ai/matching.py new file mode 100644 index 000000000..f1dfc62db --- /dev/null +++ b/src/paperless_ai/matching.py @@ -0,0 +1,102 @@ +import difflib +import logging +import re + +from django.contrib.auth.models import User + +from documents.models import Correspondent +from documents.models import DocumentType +from documents.models import StoragePath +from documents.models import Tag +from documents.permissions import get_objects_for_user_owner_aware + +MATCH_THRESHOLD = 0.8 + +logger = logging.getLogger("paperless_ai.matching") + + +def match_tags_by_name(names: list[str], user: User) -> list[Tag]: + queryset = get_objects_for_user_owner_aware( + user, + ["view_tag"], + Tag, + ) + return _match_names_to_queryset(names, queryset, "name") + + +def match_correspondents_by_name(names: list[str], user: User) -> list[Correspondent]: + queryset = get_objects_for_user_owner_aware( + user, + ["view_correspondent"], + Correspondent, + ) + return _match_names_to_queryset(names, queryset, "name") + + +def match_document_types_by_name(names: list[str], user: User) -> list[DocumentType]: + queryset = get_objects_for_user_owner_aware( + user, + ["view_documenttype"], + DocumentType, + ) + return _match_names_to_queryset(names, queryset, "name") + + +def match_storage_paths_by_name(names: list[str], user: User) -> list[StoragePath]: + queryset = get_objects_for_user_owner_aware( + user, + ["view_storagepath"], + StoragePath, + ) + return _match_names_to_queryset(names, queryset, "name") + + +def _normalize(s: str) -> str: + s = s.lower() + s = re.sub(r"[^\w\s]", "", s) # remove punctuation + s = s.strip() + return s + + +def _match_names_to_queryset(names: list[str], queryset, attr: str): + results = [] + objects = list(queryset) + object_names = [_normalize(getattr(obj, attr)) for obj in objects] + + for name in names: + if not name: + continue + target = _normalize(name) + + # First try exact match + if target in object_names: + index = object_names.index(target) + matched = objects.pop(index) + object_names.pop(index) # keep object list aligned after removal + results.append(matched) + continue + + # Fuzzy match fallback + matches = difflib.get_close_matches( + target, + object_names, + n=1, + cutoff=MATCH_THRESHOLD, + ) + if matches: + index = object_names.index(matches[0]) + matched = objects.pop(index) + object_names.pop(index) + results.append(matched) + else: + pass + return results + + +def extract_unmatched_names( + names: list[str], + matched_objects: list, + attr="name", +) -> list[str]: + matched_names = {getattr(obj, attr).lower() for obj in matched_objects} + return [name for name in names if name.lower() not in matched_names] diff --git a/src/paperless_ai/tests/__init__.py b/src/paperless_ai/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/paperless_ai/tests/test_ai_classifier.py b/src/paperless_ai/tests/test_ai_classifier.py new file mode 100644 index 000000000..115d51cd4 --- /dev/null +++ b/src/paperless_ai/tests/test_ai_classifier.py @@ -0,0 +1,186 @@ +import json +from unittest.mock import MagicMock +from unittest.mock import patch + +import pytest +from django.test import override_settings + +from documents.models import Document +from paperless_ai.ai_classifier import build_prompt_with_rag +from paperless_ai.ai_classifier import build_prompt_without_rag +from paperless_ai.ai_classifier import get_ai_document_classification +from paperless_ai.ai_classifier import get_context_for_document + + +@pytest.fixture +def mock_document(): + doc = MagicMock(spec=Document) + doc.title = "Test Title" + doc.filename = "test_file.pdf" + doc.created = "2023-01-01" + doc.added = "2023-01-02" + doc.modified = "2023-01-03" + + tag1 = MagicMock() + tag1.name = "Tag1" + tag2 = MagicMock() + tag2.name = "Tag2" + doc.tags.all = MagicMock(return_value=[tag1, tag2]) + + doc.document_type = MagicMock() + doc.document_type.name = "Invoice" + doc.correspondent = MagicMock() + doc.correspondent.name = "Test Correspondent" + doc.archive_serial_number = "12345" + doc.content = "This is the document content." + + cf1 = MagicMock(__str__=lambda x: "Value1") + cf1.field = MagicMock() + cf1.field.name = "Field1" + cf1.value = "Value1" + cf2 = MagicMock(__str__=lambda x: "Value2") + cf2.field = MagicMock() + cf2.field.name = "Field2" + cf2.value = "Value2" + doc.custom_fields.all = MagicMock(return_value=[cf1, cf2]) + + return doc + + +@pytest.fixture +def mock_similar_documents(): + doc1 = MagicMock() + doc1.content = "Content of document 1" + doc1.title = "Title 1" + doc1.filename = "file1.txt" + + doc2 = MagicMock() + doc2.content = "Content of document 2" + doc2.title = None + doc2.filename = "file2.txt" + + doc3 = MagicMock() + doc3.content = None + doc3.title = None + doc3.filename = None + + return [doc1, doc2, doc3] + + +@pytest.mark.django_db +@patch("paperless_ai.client.AIClient.run_llm_query") +@override_settings( + LLM_BACKEND="ollama", + LLM_MODEL="some_model", +) +def test_get_ai_document_classification_success(mock_run_llm_query, mock_document): + mock_run_llm_query.return_value = { + "title": "Test Title", + "tags": ["test", "document"], + "correspondents": ["John Doe"], + "document_types": ["report"], + "storage_paths": ["Reports"], + "dates": ["2023-01-01"], + } + + result = get_ai_document_classification(mock_document) + + assert result["title"] == "Test Title" + assert result["tags"] == ["test", "document"] + assert result["correspondents"] == ["John Doe"] + assert result["document_types"] == ["report"] + assert result["storage_paths"] == ["Reports"] + assert result["dates"] == ["2023-01-01"] + + +@pytest.mark.django_db +@patch("paperless_ai.client.AIClient.run_llm_query") +def test_get_ai_document_classification_failure(mock_run_llm_query, mock_document): + mock_run_llm_query.side_effect = Exception("LLM query failed") + + # assert raises an exception + with pytest.raises(Exception): + get_ai_document_classification(mock_document) + + +@pytest.mark.django_db +@patch("paperless_ai.client.AIClient.run_llm_query") +@patch("paperless_ai.ai_classifier.build_prompt_with_rag") +@override_settings( + LLM_EMBEDDING_BACKEND="huggingface", + LLM_EMBEDDING_MODEL="some_model", + LLM_BACKEND="ollama", + LLM_MODEL="some_model", +) +def test_use_rag_if_configured( + mock_build_prompt_with_rag, + mock_run_llm_query, + mock_document, +): + mock_build_prompt_with_rag.return_value = "Prompt with RAG" + mock_run_llm_query.return_value.text = json.dumps({}) + get_ai_document_classification(mock_document) + mock_build_prompt_with_rag.assert_called_once() + + +@pytest.mark.django_db +@patch("paperless_ai.client.AIClient.run_llm_query") +@patch("paperless_ai.ai_classifier.build_prompt_without_rag") +@patch("paperless.config.AIConfig") +@override_settings( + LLM_BACKEND="ollama", + LLM_MODEL="some_model", +) +def test_use_without_rag_if_not_configured( + mock_ai_config, + mock_build_prompt_without_rag, + mock_run_llm_query, + mock_document, +): + mock_ai_config.llm_embedding_backend = None + mock_build_prompt_without_rag.return_value = "Prompt without RAG" + mock_run_llm_query.return_value.text = json.dumps({}) + get_ai_document_classification(mock_document) + mock_build_prompt_without_rag.assert_called_once() + + +@pytest.mark.django_db +@override_settings( + LLM_EMBEDDING_BACKEND="huggingface", + LLM_BACKEND="ollama", + LLM_MODEL="some_model", +) +def test_prompt_with_without_rag(mock_document): + with patch( + "paperless_ai.ai_classifier.get_context_for_document", + return_value="Context from similar documents", + ): + prompt = build_prompt_without_rag(mock_document) + assert "Additional context from similar documents:" not in prompt + + prompt = build_prompt_with_rag(mock_document) + assert "Additional context from similar documents:" in prompt + + +@patch("paperless_ai.ai_classifier.query_similar_documents") +def test_get_context_for_document( + mock_query_similar_documents, + mock_document, + mock_similar_documents, +): + mock_query_similar_documents.return_value = mock_similar_documents + + result = get_context_for_document(mock_document, max_docs=2) + + expected_result = ( + "TITLE: Title 1\nContent of document 1\n\n" + "TITLE: file2.txt\nContent of document 2" + ) + assert result == expected_result + mock_query_similar_documents.assert_called_once() + + +def test_get_context_for_document_no_similar_docs(mock_document): + with patch("paperless_ai.ai_classifier.query_similar_documents", return_value=[]): + result = get_context_for_document(mock_document) + assert result == "" diff --git a/src/paperless_ai/tests/test_ai_indexing.py b/src/paperless_ai/tests/test_ai_indexing.py new file mode 100644 index 000000000..7505d49b0 --- /dev/null +++ b/src/paperless_ai/tests/test_ai_indexing.py @@ -0,0 +1,398 @@ +import json +from unittest.mock import MagicMock +from unittest.mock import patch + +import pytest +from celery import states +from django.test import override_settings +from django.utils import timezone +from llama_index.core.base.embeddings.base import BaseEmbedding + +from documents.models import Document +from documents.models import PaperlessTask +from paperless_ai import indexing + + +@pytest.fixture +def temp_llm_index_dir(tmp_path): + original_dir = indexing.settings.LLM_INDEX_DIR + indexing.settings.LLM_INDEX_DIR = tmp_path + yield tmp_path + indexing.settings.LLM_INDEX_DIR = original_dir + + +@pytest.fixture +def real_document(db): + return Document.objects.create( + title="Test Document", + content="This is some test content.", + added=timezone.now(), + ) + + +@pytest.fixture +def mock_embed_model(): + fake = FakeEmbedding() + with ( + patch("paperless_ai.indexing.get_embedding_model") as mock_index, + patch( + "paperless_ai.embedding.get_embedding_model", + ) as mock_embedding, + ): + mock_index.return_value = fake + mock_embedding.return_value = fake + yield mock_index + + +class FakeEmbedding(BaseEmbedding): + # TODO: maybe a better way to do this? + def _aget_query_embedding(self, query: str) -> list[float]: + return [0.1] * self.get_query_embedding_dim() + + def _get_query_embedding(self, query: str) -> list[float]: + return [0.1] * self.get_query_embedding_dim() + + def _get_text_embedding(self, text: str) -> list[float]: + return [0.1] * self.get_query_embedding_dim() + + def get_query_embedding_dim(self) -> int: + return 384 # Match your real FAISS config + + +@pytest.mark.django_db +def test_build_document_node(real_document): + nodes = indexing.build_document_node(real_document) + assert len(nodes) > 0 + assert nodes[0].metadata["document_id"] == str(real_document.id) + + +@pytest.mark.django_db +def test_update_llm_index( + temp_llm_index_dir, + real_document, + mock_embed_model, +): + with patch("documents.models.Document.objects.all") as mock_all: + mock_queryset = MagicMock() + mock_queryset.exists.return_value = True + mock_queryset.__iter__.return_value = iter([real_document]) + mock_all.return_value = mock_queryset + indexing.update_llm_index(rebuild=True) + + assert any(temp_llm_index_dir.glob("*.json")) + + +@pytest.mark.django_db +def test_update_llm_index_removes_meta( + temp_llm_index_dir, + real_document, + mock_embed_model, +): + # Pre-create a meta.json with incorrect data + (temp_llm_index_dir / "meta.json").write_text( + json.dumps({"embedding_model": "old", "dim": 1}), + ) + + with patch("documents.models.Document.objects.all") as mock_all: + mock_queryset = MagicMock() + mock_queryset.exists.return_value = True + mock_queryset.__iter__.return_value = iter([real_document]) + mock_all.return_value = mock_queryset + indexing.update_llm_index(rebuild=True) + + meta = json.loads((temp_llm_index_dir / "meta.json").read_text()) + from paperless.config import AIConfig + + config = AIConfig() + expected_model = config.llm_embedding_model or ( + "text-embedding-3-small" + if config.llm_embedding_backend == "openai" + else "sentence-transformers/all-MiniLM-L6-v2" + ) + assert meta == {"embedding_model": expected_model, "dim": 384} + + +@pytest.mark.django_db +def test_update_llm_index_partial_update( + temp_llm_index_dir, + real_document, + mock_embed_model, +): + doc2 = Document.objects.create( + title="Test Document 2", + content="This is some test content 2.", + added=timezone.now(), + checksum="1234567890abcdef", + ) + # Initial index + with patch("documents.models.Document.objects.all") as mock_all: + mock_queryset = MagicMock() + mock_queryset.exists.return_value = True + mock_queryset.__iter__.return_value = iter([real_document, doc2]) + mock_all.return_value = mock_queryset + + indexing.update_llm_index(rebuild=True) + + # modify document + updated_document = real_document + updated_document.modified = timezone.now() # simulate modification + + # new doc + doc3 = Document.objects.create( + title="Test Document 3", + content="This is some test content 3.", + added=timezone.now(), + checksum="abcdef1234567890", + ) + + with patch("documents.models.Document.objects.all") as mock_all: + mock_queryset = MagicMock() + mock_queryset.exists.return_value = True + mock_queryset.__iter__.return_value = iter([updated_document, doc2, doc3]) + mock_all.return_value = mock_queryset + + # assert logs "Updating LLM index with %d new nodes and removing %d old nodes." + with patch("paperless_ai.indexing.logger") as mock_logger: + indexing.update_llm_index(rebuild=False) + mock_logger.info.assert_called_once_with( + "Updating %d nodes in LLM index.", + 2, + ) + indexing.update_llm_index(rebuild=False) + + assert any(temp_llm_index_dir.glob("*.json")) + + +def test_get_or_create_storage_context_raises_exception( + temp_llm_index_dir, + mock_embed_model, +): + with pytest.raises(Exception): + indexing.get_or_create_storage_context(rebuild=False) + + +@override_settings( + LLM_EMBEDDING_BACKEND="huggingface", +) +def test_load_or_build_index_builds_when_nodes_given( + temp_llm_index_dir, + real_document, + mock_embed_model, +): + with ( + patch( + "paperless_ai.indexing.load_index_from_storage", + side_effect=ValueError("Index not found"), + ), + patch( + "paperless_ai.indexing.VectorStoreIndex", + return_value=MagicMock(), + ) as mock_index_cls, + patch( + "paperless_ai.indexing.get_or_create_storage_context", + return_value=MagicMock(), + ) as mock_storage, + ): + mock_storage.return_value.persist_dir = temp_llm_index_dir + indexing.load_or_build_index( + nodes=[indexing.build_document_node(real_document)], + ) + mock_index_cls.assert_called_once() + + +def test_load_or_build_index_raises_exception_when_no_nodes( + temp_llm_index_dir, + mock_embed_model, +): + with ( + patch( + "paperless_ai.indexing.load_index_from_storage", + side_effect=ValueError("Index not found"), + ), + patch( + "paperless_ai.indexing.get_or_create_storage_context", + return_value=MagicMock(), + ), + ): + with pytest.raises(Exception): + indexing.load_or_build_index() + + +@pytest.mark.django_db +def test_load_or_build_index_succeeds_when_nodes_given( + temp_llm_index_dir, + mock_embed_model, +): + with ( + patch( + "paperless_ai.indexing.load_index_from_storage", + side_effect=ValueError("Index not found"), + ), + patch( + "paperless_ai.indexing.VectorStoreIndex", + return_value=MagicMock(), + ) as mock_index_cls, + patch( + "paperless_ai.indexing.get_or_create_storage_context", + return_value=MagicMock(), + ) as mock_storage, + ): + mock_storage.return_value.persist_dir = temp_llm_index_dir + indexing.load_or_build_index( + nodes=[MagicMock()], + ) + mock_index_cls.assert_called_once() + + +@pytest.mark.django_db +def test_add_or_update_document_updates_existing_entry( + temp_llm_index_dir, + real_document, + mock_embed_model, +): + indexing.update_llm_index(rebuild=True) + indexing.llm_index_add_or_update_document(real_document) + + assert any(temp_llm_index_dir.glob("*.json")) + + +@pytest.mark.django_db +def test_remove_document_deletes_node_from_docstore( + temp_llm_index_dir, + real_document, + mock_embed_model, +): + indexing.update_llm_index(rebuild=True) + index = indexing.load_or_build_index() + assert len(index.docstore.docs) == 1 + + indexing.llm_index_remove_document(real_document) + index = indexing.load_or_build_index() + assert len(index.docstore.docs) == 0 + + +@pytest.mark.django_db +def test_update_llm_index_no_documents( + temp_llm_index_dir, + mock_embed_model, +): + with patch("documents.models.Document.objects.all") as mock_all: + mock_queryset = MagicMock() + mock_queryset.exists.return_value = False + mock_queryset.__iter__.return_value = iter([]) + mock_all.return_value = mock_queryset + + # check log message + with patch("paperless_ai.indexing.logger") as mock_logger: + indexing.update_llm_index(rebuild=True) + mock_logger.warning.assert_called_once_with( + "No documents found to index.", + ) + + +@pytest.mark.django_db +def test_queue_llm_index_update_if_needed_enqueues_when_idle_or_skips_recent(): + # No existing tasks + with patch("documents.tasks.llmindex_index") as mock_task: + result = indexing.queue_llm_index_update_if_needed( + rebuild=True, + reason="test enqueue", + ) + + assert result is True + mock_task.delay.assert_called_once_with(rebuild=True, scheduled=False, auto=True) + + PaperlessTask.objects.create( + task_id="task-1", + task_name=PaperlessTask.TaskName.LLMINDEX_UPDATE, + status=states.STARTED, + date_created=timezone.now(), + ) + + # Existing running task + with patch("documents.tasks.llmindex_index") as mock_task: + result = indexing.queue_llm_index_update_if_needed( + rebuild=False, + reason="should skip", + ) + + assert result is False + mock_task.delay.assert_not_called() + + +@override_settings( + LLM_EMBEDDING_BACKEND="huggingface", + LLM_BACKEND="ollama", +) +def test_query_similar_documents( + temp_llm_index_dir, + real_document, +): + with ( + patch("paperless_ai.indexing.get_or_create_storage_context") as mock_storage, + patch("paperless_ai.indexing.load_or_build_index") as mock_load_or_build_index, + patch( + "paperless_ai.indexing.vector_store_file_exists", + ) as mock_vector_store_exists, + patch("paperless_ai.indexing.VectorIndexRetriever") as mock_retriever_cls, + patch("paperless_ai.indexing.Document.objects.filter") as mock_filter, + ): + mock_storage.return_value = MagicMock() + mock_storage.return_value.persist_dir = temp_llm_index_dir + mock_vector_store_exists.return_value = True + + mock_index = MagicMock() + mock_load_or_build_index.return_value = mock_index + + mock_retriever = MagicMock() + mock_retriever_cls.return_value = mock_retriever + + mock_node1 = MagicMock() + mock_node1.metadata = {"document_id": 1} + + mock_node2 = MagicMock() + mock_node2.metadata = {"document_id": 2} + + mock_retriever.retrieve.return_value = [mock_node1, mock_node2] + + mock_filtered_docs = [MagicMock(pk=1), MagicMock(pk=2)] + mock_filter.return_value = mock_filtered_docs + + result = indexing.query_similar_documents(real_document, top_k=3) + + mock_load_or_build_index.assert_called_once() + mock_retriever_cls.assert_called_once() + mock_retriever.retrieve.assert_called_once_with( + "Test Document\nThis is some test content.", + ) + mock_filter.assert_called_once_with(pk__in=[1, 2]) + + assert result == mock_filtered_docs + + +@pytest.mark.django_db +def test_query_similar_documents_triggers_update_when_index_missing( + temp_llm_index_dir, + real_document, +): + with ( + patch( + "paperless_ai.indexing.vector_store_file_exists", + return_value=False, + ), + patch( + "paperless_ai.indexing.queue_llm_index_update_if_needed", + ) as mock_queue, + patch("paperless_ai.indexing.load_or_build_index") as mock_load, + ): + result = indexing.query_similar_documents( + real_document, + top_k=2, + ) + + mock_queue.assert_called_once_with( + rebuild=False, + reason="LLM index not found for similarity query.", + ) + mock_load.assert_not_called() + assert result == [] diff --git a/src/paperless_ai/tests/test_chat.py b/src/paperless_ai/tests/test_chat.py new file mode 100644 index 000000000..688d78058 --- /dev/null +++ b/src/paperless_ai/tests/test_chat.py @@ -0,0 +1,140 @@ +from unittest.mock import MagicMock +from unittest.mock import patch + +import pytest +from llama_index.core import VectorStoreIndex +from llama_index.core.schema import TextNode + +from paperless_ai.chat import stream_chat_with_documents + + +@pytest.fixture(autouse=True) +def patch_embed_model(): + from llama_index.core import settings as llama_settings + from llama_index.core.embeddings.mock_embed_model import MockEmbedding + + # Use a real BaseEmbedding subclass to satisfy llama-index 0.14 validation + llama_settings.Settings.embed_model = MockEmbedding(embed_dim=1536) + yield + llama_settings.Settings.embed_model = None + + +@pytest.fixture(autouse=True) +def patch_embed_nodes(): + with patch( + "llama_index.core.indices.vector_store.base.embed_nodes", + ) as mock_embed_nodes: + mock_embed_nodes.side_effect = lambda nodes, *_args, **_kwargs: { + node.node_id: [0.1] * 1536 for node in nodes + } + yield + + +@pytest.fixture +def mock_document(): + doc = MagicMock() + doc.pk = 1 + doc.title = "Test Document" + doc.filename = "test_file.pdf" + doc.content = "This is the document content." + return doc + + +def test_stream_chat_with_one_document_full_content(mock_document): + with ( + patch("paperless_ai.chat.AIClient") as mock_client_cls, + patch("paperless_ai.chat.load_or_build_index") as mock_load_index, + patch( + "paperless_ai.chat.RetrieverQueryEngine.from_args", + ) as mock_query_engine_cls, + ): + mock_client = MagicMock() + mock_client_cls.return_value = mock_client + mock_client.llm = MagicMock() + + mock_node = TextNode( + text="This is node content.", + metadata={"document_id": str(mock_document.pk), "title": "Test Document"}, + ) + mock_index = MagicMock() + mock_index.docstore.docs.values.return_value = [mock_node] + mock_load_index.return_value = mock_index + + mock_response_stream = MagicMock() + mock_response_stream.response_gen = iter(["chunk1", "chunk2"]) + mock_query_engine = MagicMock() + mock_query_engine_cls.return_value = mock_query_engine + mock_query_engine.query.return_value = mock_response_stream + + output = list(stream_chat_with_documents("What is this?", [mock_document])) + + assert output == ["chunk1", "chunk2"] + + +def test_stream_chat_with_multiple_documents_retrieval(patch_embed_nodes): + with ( + patch("paperless_ai.chat.AIClient") as mock_client_cls, + patch("paperless_ai.chat.load_or_build_index") as mock_load_index, + patch( + "paperless_ai.chat.RetrieverQueryEngine.from_args", + ) as mock_query_engine_cls, + patch.object(VectorStoreIndex, "as_retriever") as mock_as_retriever, + ): + # Mock AIClient and LLM + mock_client = MagicMock() + mock_client_cls.return_value = mock_client + mock_client.llm = MagicMock() + + # Create two real TextNodes + mock_node1 = TextNode( + text="Content for doc 1.", + metadata={"document_id": "1", "title": "Document 1"}, + ) + mock_node2 = TextNode( + text="Content for doc 2.", + metadata={"document_id": "2", "title": "Document 2"}, + ) + mock_index = MagicMock() + mock_index.docstore.docs.values.return_value = [mock_node1, mock_node2] + mock_load_index.return_value = mock_index + + # Patch as_retriever to return a retriever whose retrieve() returns mock_node1 and mock_node2 + mock_retriever = MagicMock() + mock_retriever.retrieve.return_value = [mock_node1, mock_node2] + mock_as_retriever.return_value = mock_retriever + + # Mock response stream + mock_response_stream = MagicMock() + mock_response_stream.response_gen = iter(["chunk1", "chunk2"]) + + # Mock RetrieverQueryEngine + mock_query_engine = MagicMock() + mock_query_engine_cls.return_value = mock_query_engine + mock_query_engine.query.return_value = mock_response_stream + + # Fake documents + doc1 = MagicMock(pk=1) + doc2 = MagicMock(pk=2) + + output = list(stream_chat_with_documents("What's up?", [doc1, doc2])) + + assert output == ["chunk1", "chunk2"] + + +def test_stream_chat_no_matching_nodes(): + with ( + patch("paperless_ai.chat.AIClient") as mock_client_cls, + patch("paperless_ai.chat.load_or_build_index") as mock_load_index, + ): + mock_client = MagicMock() + mock_client_cls.return_value = mock_client + mock_client.llm = MagicMock() + + mock_index = MagicMock() + # No matching nodes + mock_index.docstore.docs.values.return_value = [] + mock_load_index.return_value = mock_index + + output = list(stream_chat_with_documents("Any info?", [MagicMock(pk=1)])) + + assert output == ["Sorry, I couldn't find any content to answer your question."] diff --git a/src/paperless_ai/tests/test_client.py b/src/paperless_ai/tests/test_client.py new file mode 100644 index 000000000..47053ab20 --- /dev/null +++ b/src/paperless_ai/tests/test_client.py @@ -0,0 +1,111 @@ +from unittest.mock import MagicMock +from unittest.mock import patch + +import pytest +from llama_index.core.llms import ChatMessage +from llama_index.core.llms.llm import ToolSelection + +from paperless_ai.client import AIClient + + +@pytest.fixture +def mock_ai_config(): + with patch("paperless_ai.client.AIConfig") as MockAIConfig: + mock_config = MagicMock() + MockAIConfig.return_value = mock_config + yield mock_config + + +@pytest.fixture +def mock_ollama_llm(): + with patch("paperless_ai.client.Ollama") as MockOllama: + yield MockOllama + + +@pytest.fixture +def mock_openai_llm(): + with patch("paperless_ai.client.OpenAI") as MockOpenAI: + yield MockOpenAI + + +def test_get_llm_ollama(mock_ai_config, mock_ollama_llm): + mock_ai_config.llm_backend = "ollama" + mock_ai_config.llm_model = "test_model" + mock_ai_config.llm_endpoint = "http://test-url" + + client = AIClient() + + mock_ollama_llm.assert_called_once_with( + model="test_model", + base_url="http://test-url", + request_timeout=120, + ) + assert client.llm == mock_ollama_llm.return_value + + +def test_get_llm_openai(mock_ai_config, mock_openai_llm): + mock_ai_config.llm_backend = "openai" + mock_ai_config.llm_model = "test_model" + mock_ai_config.llm_api_key = "test_api_key" + mock_ai_config.llm_endpoint = "http://test-url" + + client = AIClient() + + mock_openai_llm.assert_called_once_with( + model="test_model", + api_base="http://test-url", + api_key="test_api_key", + ) + assert client.llm == mock_openai_llm.return_value + + +def test_get_llm_unsupported_backend(mock_ai_config): + mock_ai_config.llm_backend = "unsupported" + + with pytest.raises(ValueError, match="Unsupported LLM backend: unsupported"): + AIClient() + + +def test_run_llm_query(mock_ai_config, mock_ollama_llm): + mock_ai_config.llm_backend = "ollama" + mock_ai_config.llm_model = "test_model" + mock_ai_config.llm_endpoint = "http://test-url" + + mock_llm_instance = mock_ollama_llm.return_value + + tool_selection = ToolSelection( + tool_id="call_test", + tool_name="DocumentClassifierSchema", + tool_kwargs={ + "title": "Test Title", + "tags": ["test", "document"], + "correspondents": ["John Doe"], + "document_types": ["report"], + "storage_paths": ["Reports"], + "dates": ["2023-01-01"], + }, + ) + + mock_llm_instance.chat_with_tools.return_value = MagicMock() + mock_llm_instance.get_tool_calls_from_response.return_value = [tool_selection] + + client = AIClient() + result = client.run_llm_query("test_prompt") + + assert result["title"] == "Test Title" + + +def test_run_chat(mock_ai_config, mock_ollama_llm): + mock_ai_config.llm_backend = "ollama" + mock_ai_config.llm_model = "test_model" + mock_ai_config.llm_endpoint = "http://test-url" + + mock_llm_instance = mock_ollama_llm.return_value + mock_llm_instance.chat.return_value = "test_chat_result" + + client = AIClient() + messages = [ChatMessage(role="user", content="Hello")] + result = client.run_chat(messages) + + mock_llm_instance.chat.assert_called_once_with(messages) + assert result == "test_chat_result" diff --git a/src/paperless_ai/tests/test_embedding.py b/src/paperless_ai/tests/test_embedding.py new file mode 100644 index 000000000..9430205fa --- /dev/null +++ b/src/paperless_ai/tests/test_embedding.py @@ -0,0 +1,169 @@ +import json +from unittest.mock import MagicMock +from unittest.mock import patch + +import pytest +from django.conf import settings + +from documents.models import Document +from paperless.models import LLMEmbeddingBackend +from paperless_ai.embedding import build_llm_index_text +from paperless_ai.embedding import get_embedding_dim +from paperless_ai.embedding import get_embedding_model + + +@pytest.fixture +def mock_ai_config(): + with patch("paperless_ai.embedding.AIConfig") as MockAIConfig: + yield MockAIConfig + + +@pytest.fixture +def temp_llm_index_dir(tmp_path): + original_dir = settings.LLM_INDEX_DIR + settings.LLM_INDEX_DIR = tmp_path + yield tmp_path + settings.LLM_INDEX_DIR = original_dir + + +@pytest.fixture +def mock_document(): + doc = MagicMock(spec=Document) + doc.title = "Test Title" + doc.filename = "test_file.pdf" + doc.created = "2023-01-01" + doc.added = "2023-01-02" + doc.modified = "2023-01-03" + + tag1 = MagicMock() + tag1.name = "Tag1" + tag2 = MagicMock() + tag2.name = "Tag2" + doc.tags.all = MagicMock(return_value=[tag1, tag2]) + + doc.document_type = MagicMock() + doc.document_type.name = "Invoice" + doc.correspondent = MagicMock() + doc.correspondent.name = "Test Correspondent" + doc.archive_serial_number = "12345" + doc.content = "This is the document content." + + cf1 = MagicMock(__str__=lambda x: "Value1") + cf1.field = MagicMock() + cf1.field.name = "Field1" + cf1.value = "Value1" + cf2 = MagicMock(__str__=lambda x: "Value2") + cf2.field = MagicMock() + cf2.field.name = "Field2" + cf2.value = "Value2" + doc.custom_fields.all = MagicMock(return_value=[cf1, cf2]) + + return doc + + +def test_get_embedding_model_openai(mock_ai_config): + mock_ai_config.return_value.llm_embedding_backend = LLMEmbeddingBackend.OPENAI + mock_ai_config.return_value.llm_embedding_model = "text-embedding-3-small" + mock_ai_config.return_value.llm_api_key = "test_api_key" + + with patch("paperless_ai.embedding.OpenAIEmbedding") as MockOpenAIEmbedding: + model = get_embedding_model() + MockOpenAIEmbedding.assert_called_once_with( + model="text-embedding-3-small", + api_key="test_api_key", + ) + assert model == MockOpenAIEmbedding.return_value + + +def test_get_embedding_model_huggingface(mock_ai_config): + mock_ai_config.return_value.llm_embedding_backend = LLMEmbeddingBackend.HUGGINGFACE + mock_ai_config.return_value.llm_embedding_model = ( + "sentence-transformers/all-MiniLM-L6-v2" + ) + + with patch( + "paperless_ai.embedding.HuggingFaceEmbedding", + ) as MockHuggingFaceEmbedding: + model = get_embedding_model() + MockHuggingFaceEmbedding.assert_called_once_with( + model_name="sentence-transformers/all-MiniLM-L6-v2", + ) + assert model == MockHuggingFaceEmbedding.return_value + + +def test_get_embedding_model_invalid_backend(mock_ai_config): + mock_ai_config.return_value.llm_embedding_backend = "INVALID_BACKEND" + + with pytest.raises( + ValueError, + match="Unsupported embedding backend: INVALID_BACKEND", + ): + get_embedding_model() + + +def test_get_embedding_dim_infers_and_saves(temp_llm_index_dir, mock_ai_config): + mock_ai_config.return_value.llm_embedding_backend = "openai" + mock_ai_config.return_value.llm_embedding_model = None + + class DummyEmbedding: + def get_text_embedding(self, text): + return [0.0] * 7 + + with patch( + "paperless_ai.embedding.get_embedding_model", + return_value=DummyEmbedding(), + ) as mock_get: + dim = get_embedding_dim() + mock_get.assert_called_once() + + assert dim == 7 + meta = json.loads((temp_llm_index_dir / "meta.json").read_text()) + assert meta == {"embedding_model": "text-embedding-3-small", "dim": 7} + + +def test_get_embedding_dim_reads_existing_meta(temp_llm_index_dir, mock_ai_config): + mock_ai_config.return_value.llm_embedding_backend = "openai" + mock_ai_config.return_value.llm_embedding_model = None + + (temp_llm_index_dir / "meta.json").write_text( + json.dumps({"embedding_model": "text-embedding-3-small", "dim": 11}), + ) + + with patch("paperless_ai.embedding.get_embedding_model") as mock_get: + assert get_embedding_dim() == 11 + mock_get.assert_not_called() + + +def test_get_embedding_dim_raises_on_model_change(temp_llm_index_dir, mock_ai_config): + mock_ai_config.return_value.llm_embedding_backend = "openai" + mock_ai_config.return_value.llm_embedding_model = None + + (temp_llm_index_dir / "meta.json").write_text( + json.dumps({"embedding_model": "old", "dim": 11}), + ) + + with pytest.raises( + RuntimeError, + match="Embedding model changed from old to text-embedding-3-small", + ): + get_embedding_dim() + + +def test_build_llm_index_text(mock_document): + with patch("documents.models.Note.objects.filter") as mock_notes_filter: + mock_notes_filter.return_value = [ + MagicMock(note="Note1"), + MagicMock(note="Note2"), + ] + + result = build_llm_index_text(mock_document) + + assert "Title: Test Title" in result + assert "Filename: test_file.pdf" in result + assert "Created: 2023-01-01" in result + assert "Tags: Tag1, Tag2" in result + assert "Document Type: Invoice" in result + assert "Correspondent: Test Correspondent" in result + assert "Notes: Note1,Note2" in result + assert "Content:\n\nThis is the document content." in result + assert "Custom Field - Field1: Value1\nCustom Field - Field2: Value2" in result diff --git a/src/paperless_ai/tests/test_matching.py b/src/paperless_ai/tests/test_matching.py new file mode 100644 index 000000000..87a42a1a4 --- /dev/null +++ b/src/paperless_ai/tests/test_matching.py @@ -0,0 +1,86 @@ +from unittest.mock import patch + +from django.test import TestCase + +from documents.models import Correspondent +from documents.models import DocumentType +from documents.models import StoragePath +from documents.models import Tag +from paperless_ai.matching import extract_unmatched_names +from paperless_ai.matching import match_correspondents_by_name +from paperless_ai.matching import match_document_types_by_name +from paperless_ai.matching import match_storage_paths_by_name +from paperless_ai.matching import match_tags_by_name + + +class TestAIMatching(TestCase): + def setUp(self): + # Create test data for Tag + self.tag1 = Tag.objects.create(name="Test Tag 1") + self.tag2 = Tag.objects.create(name="Test Tag 2") + + # Create test data for Correspondent + self.correspondent1 = Correspondent.objects.create(name="Test Correspondent 1") + self.correspondent2 = Correspondent.objects.create(name="Test Correspondent 2") + + # Create test data for DocumentType + self.document_type1 = DocumentType.objects.create(name="Test Document Type 1") + self.document_type2 = DocumentType.objects.create(name="Test Document Type 2") + + # Create test data for StoragePath + self.storage_path1 = StoragePath.objects.create(name="Test Storage Path 1") + self.storage_path2 = StoragePath.objects.create(name="Test Storage Path 2") + + @patch("paperless_ai.matching.get_objects_for_user_owner_aware") + def test_match_tags_by_name(self, mock_get_objects): + mock_get_objects.return_value = Tag.objects.all() + names = ["Test Tag 1", "Nonexistent Tag"] + result = match_tags_by_name(names, user=None) + self.assertEqual(len(result), 1) + self.assertEqual(result[0].name, "Test Tag 1") + + @patch("paperless_ai.matching.get_objects_for_user_owner_aware") + def test_match_correspondents_by_name(self, mock_get_objects): + mock_get_objects.return_value = Correspondent.objects.all() + names = ["Test Correspondent 1", "Nonexistent Correspondent"] + result = match_correspondents_by_name(names, user=None) + self.assertEqual(len(result), 1) + self.assertEqual(result[0].name, "Test Correspondent 1") + + @patch("paperless_ai.matching.get_objects_for_user_owner_aware") + def test_match_document_types_by_name(self, mock_get_objects): + mock_get_objects.return_value = DocumentType.objects.all() + names = ["Test Document Type 1", "Nonexistent Document Type"] + result = match_document_types_by_name(names, user=None) + self.assertEqual(len(result), 1) + self.assertEqual(result[0].name, "Test Document Type 1") + + @patch("paperless_ai.matching.get_objects_for_user_owner_aware") + def test_match_storage_paths_by_name(self, mock_get_objects): + mock_get_objects.return_value = StoragePath.objects.all() + names = ["Test Storage Path 1", "Nonexistent Storage Path"] + result = match_storage_paths_by_name(names, user=None) + self.assertEqual(len(result), 1) + self.assertEqual(result[0].name, "Test Storage Path 1") + + def test_extract_unmatched_names(self): + llm_names = ["Test Tag 1", "Nonexistent Tag"] + matched_objects = [self.tag1] + unmatched_names = extract_unmatched_names(llm_names, matched_objects) + self.assertEqual(unmatched_names, ["Nonexistent Tag"]) + + @patch("paperless_ai.matching.get_objects_for_user_owner_aware") + def test_match_tags_by_name_with_empty_names(self, mock_get_objects): + mock_get_objects.return_value = Tag.objects.all() + names = [None, "", " "] + result = match_tags_by_name(names, user=None) + self.assertEqual(result, []) + + @patch("paperless_ai.matching.get_objects_for_user_owner_aware") + def test_match_tags_with_fuzzy_matching(self, mock_get_objects): + mock_get_objects.return_value = Tag.objects.all() + names = ["Test Taag 1", "Teest Tag 2"] + result = match_tags_by_name(names, user=None) + self.assertEqual(len(result), 2) + self.assertEqual(result[0].name, "Test Tag 1") + self.assertEqual(result[1].name, "Test Tag 2") diff --git a/src/paperless_mail/migrations/0001_initial.py b/src/paperless_mail/migrations/0001_initial.py index f7e717f0e..aab6a8239 100644 --- a/src/paperless_mail/migrations/0001_initial.py +++ b/src/paperless_mail/migrations/0001_initial.py @@ -1,6 +1,8 @@ -# Generated by Django 3.1.3 on 2020-11-15 22:54 +# Generated by Django 5.2.9 on 2026-01-20 18:46 import django.db.models.deletion +import django.utils.timezone +from django.conf import settings from django.db import migrations from django.db import models @@ -9,7 +11,8 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ("documents", "1002_auto_20201111_1105"), + ("documents", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ @@ -25,9 +28,23 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), - ("name", models.CharField(max_length=256, unique=True)), - ("imap_server", models.CharField(max_length=256)), - ("imap_port", models.IntegerField(blank=True, null=True)), + ( + "name", + models.CharField(max_length=256, unique=True, verbose_name="name"), + ), + ( + "imap_server", + models.CharField(max_length=256, verbose_name="IMAP server"), + ), + ( + "imap_port", + models.IntegerField( + blank=True, + help_text="This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections.", + null=True, + verbose_name="IMAP port", + ), + ), ( "imap_security", models.PositiveIntegerField( @@ -37,11 +54,69 @@ class Migration(migrations.Migration): (3, "Use STARTTLS"), ], default=2, + verbose_name="IMAP security", + ), + ), + ("username", models.CharField(max_length=256, verbose_name="username")), + ("password", models.TextField(verbose_name="password")), + ( + "is_token", + models.BooleanField( + default=False, + verbose_name="Is token authentication", + ), + ), + ( + "character_set", + models.CharField( + default="UTF-8", + help_text="The character set to use when communicating with the mail server, such as 'UTF-8' or 'US-ASCII'.", + max_length=256, + verbose_name="character set", + ), + ), + ( + "account_type", + models.PositiveIntegerField( + choices=[(1, "IMAP"), (2, "Gmail OAuth"), (3, "Outlook OAuth")], + default=1, + verbose_name="account type", + ), + ), + ( + "refresh_token", + models.TextField( + blank=True, + help_text="The refresh token to use for token authentication e.g. with oauth2.", + null=True, + verbose_name="refresh token", + ), + ), + ( + "expiration", + models.DateTimeField( + blank=True, + help_text="The expiration date of the refresh token. ", + null=True, + verbose_name="expiration", + ), + ), + ( + "owner", + models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + verbose_name="owner", ), ), - ("username", models.CharField(max_length=256)), - ("password", models.CharField(max_length=256)), ], + options={ + "verbose_name": "mail account", + "verbose_name_plural": "mail accounts", + }, ), migrations.CreateModel( name="MailRule", @@ -55,21 +130,126 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), - ("name", models.CharField(max_length=256)), - ("folder", models.CharField(default="INBOX", max_length=256)), + ("name", models.CharField(max_length=256, verbose_name="name")), + ("order", models.IntegerField(default=0, verbose_name="order")), + ("enabled", models.BooleanField(default=True, verbose_name="enabled")), + ( + "folder", + models.CharField( + default="INBOX", + help_text="Subfolders must be separated by a delimiter, often a dot ('.') or slash ('/'), but it varies by mail server.", + max_length=256, + verbose_name="folder", + ), + ), ( "filter_from", - models.CharField(blank=True, max_length=256, null=True), + models.CharField( + blank=True, + max_length=256, + null=True, + verbose_name="filter from", + ), + ), + ( + "filter_to", + models.CharField( + blank=True, + max_length=256, + null=True, + verbose_name="filter to", + ), ), ( "filter_subject", - models.CharField(blank=True, max_length=256, null=True), + models.CharField( + blank=True, + max_length=256, + null=True, + verbose_name="filter subject", + ), ), ( "filter_body", - models.CharField(blank=True, max_length=256, null=True), + models.CharField( + blank=True, + max_length=256, + null=True, + verbose_name="filter body", + ), + ), + ( + "filter_attachment_filename_include", + models.CharField( + blank=True, + help_text="Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.", + max_length=256, + null=True, + verbose_name="filter attachment filename inclusive", + ), + ), + ( + "filter_attachment_filename_exclude", + models.CharField( + blank=True, + help_text="Do not consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.", + max_length=256, + null=True, + verbose_name="filter attachment filename exclusive", + ), + ), + ( + "maximum_age", + models.PositiveIntegerField( + default=30, + help_text="Specified in days.", + verbose_name="maximum age", + ), + ), + ( + "attachment_type", + models.PositiveIntegerField( + choices=[ + (1, "Only process attachments."), + (2, "Process all files, including 'inline' attachments."), + ], + default=1, + help_text="Inline attachments include embedded images, so it's best to combine this option with a filename filter.", + verbose_name="attachment type", + ), + ), + ( + "consumption_scope", + models.PositiveIntegerField( + choices=[ + (1, "Only process attachments."), + ( + 2, + "Process full Mail (with embedded attachments in file) as .eml", + ), + ( + 3, + "Process full Mail (with embedded attachments in file) as .eml + process attachments as separate documents", + ), + ], + default=1, + verbose_name="consumption scope", + ), + ), + ( + "pdf_layout", + models.PositiveIntegerField( + choices=[ + (0, "System default"), + (1, "Text, then HTML"), + (2, "HTML, then text"), + (3, "HTML only"), + (4, "Text only"), + ], + default=0, + verbose_name="pdf layout", + ), ), - ("maximum_age", models.PositiveIntegerField(default=30)), ( "action", models.PositiveIntegerField( @@ -78,18 +258,23 @@ class Migration(migrations.Migration): (2, "Move to specified folder"), (3, "Mark as read, don't process read mails"), (4, "Flag the mail, don't process flagged mails"), + ( + 5, + "Tag the mail with specified tag, don't process tagged mails", + ), ], default=3, - help_text="The action applied to the mail. This action is only performed when documents were consumed from the mail. Mails without attachments will remain entirely untouched.", + verbose_name="action", ), ), ( "action_parameter", models.CharField( blank=True, - help_text="Additional parameter for the action selected above, i.e., the target folder of the move to folder action.", + help_text="Additional parameter for the action selected above, i.e., the target folder of the move to folder action. Subfolders must be separated by dots.", max_length=256, null=True, + verbose_name="action parameter", ), ), ( @@ -98,8 +283,10 @@ class Migration(migrations.Migration): choices=[ (1, "Use subject as title"), (2, "Use attachment filename as title"), + (3, "Do not assign title from rule"), ], default=1, + verbose_name="assign title from", ), ), ( @@ -112,6 +299,14 @@ class Migration(migrations.Migration): (4, "Use correspondent selected below"), ], default=1, + verbose_name="assign correspondent from", + ), + ), + ( + "assign_owner_from_rule", + models.BooleanField( + default=True, + verbose_name="Assign the rule owner to documents", ), ), ( @@ -120,6 +315,7 @@ class Migration(migrations.Migration): on_delete=django.db.models.deletion.CASCADE, related_name="rules", to="paperless_mail.mailaccount", + verbose_name="account", ), ), ( @@ -129,6 +325,7 @@ class Migration(migrations.Migration): null=True, on_delete=django.db.models.deletion.SET_NULL, to="documents.correspondent", + verbose_name="assign this correspondent", ), ), ( @@ -138,17 +335,136 @@ class Migration(migrations.Migration): null=True, on_delete=django.db.models.deletion.SET_NULL, to="documents.documenttype", + verbose_name="assign this document type", ), ), ( - "assign_tag", + "assign_tags", + models.ManyToManyField( + blank=True, + to="documents.tag", + verbose_name="assign this tag", + ), + ), + ( + "owner", models.ForeignKey( blank=True, + default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, - to="documents.tag", + to=settings.AUTH_USER_MODEL, + verbose_name="owner", ), ), ], + options={ + "verbose_name": "mail rule", + "verbose_name_plural": "mail rules", + }, + ), + migrations.CreateModel( + name="ProcessedMail", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "folder", + models.CharField( + editable=False, + max_length=256, + verbose_name="folder", + ), + ), + ( + "uid", + models.CharField( + editable=False, + max_length=256, + verbose_name="uid", + ), + ), + ( + "subject", + models.CharField( + editable=False, + max_length=256, + verbose_name="subject", + ), + ), + ( + "received", + models.DateTimeField(editable=False, verbose_name="received"), + ), + ( + "processed", + models.DateTimeField( + default=django.utils.timezone.now, + editable=False, + verbose_name="processed", + ), + ), + ( + "status", + models.CharField( + editable=False, + max_length=256, + verbose_name="status", + ), + ), + ( + "error", + models.TextField( + blank=True, + editable=False, + null=True, + verbose_name="error", + ), + ), + ( + "owner", + models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + verbose_name="owner", + ), + ), + ( + "rule", + models.ForeignKey( + editable=False, + on_delete=django.db.models.deletion.CASCADE, + to="paperless_mail.mailrule", + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.AddConstraint( + model_name="mailrule", + constraint=models.UniqueConstraint( + fields=("name", "owner"), + name="paperless_mail_mailrule_unique_name_owner", + ), + ), + migrations.AddConstraint( + model_name="mailrule", + constraint=models.UniqueConstraint( + condition=models.Q(("owner__isnull", True)), + fields=("name",), + name="paperless_mail_mailrule_name_unique", + ), ), ] diff --git a/src/paperless_mail/migrations/0001_initial_squashed_0009_mailrule_assign_tags.py b/src/paperless_mail/migrations/0001_initial_squashed_0009_mailrule_assign_tags.py deleted file mode 100644 index aad22918f..000000000 --- a/src/paperless_mail/migrations/0001_initial_squashed_0009_mailrule_assign_tags.py +++ /dev/null @@ -1,477 +0,0 @@ -# Generated by Django 4.2.13 on 2024-06-28 17:46 - -import django.db.migrations.operations.special -import django.db.models.deletion -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - replaces = [ - ("paperless_mail", "0001_initial"), - ("paperless_mail", "0002_auto_20201117_1334"), - ("paperless_mail", "0003_auto_20201118_1940"), - ("paperless_mail", "0004_mailrule_order"), - ("paperless_mail", "0005_help_texts"), - ("paperless_mail", "0006_auto_20210101_2340"), - ("paperless_mail", "0007_auto_20210106_0138"), - ("paperless_mail", "0008_auto_20210516_0940"), - ("paperless_mail", "0009_mailrule_assign_tags"), - ] - - dependencies = [ - ("documents", "1002_auto_20201111_1105"), - ("documents", "1011_auto_20210101_2340"), - ] - - operations = [ - migrations.CreateModel( - name="MailAccount", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("name", models.CharField(max_length=256, unique=True)), - ("imap_server", models.CharField(max_length=256)), - ("imap_port", models.IntegerField(blank=True, null=True)), - ( - "imap_security", - models.PositiveIntegerField( - choices=[ - (1, "No encryption"), - (2, "Use SSL"), - (3, "Use STARTTLS"), - ], - default=2, - ), - ), - ("username", models.CharField(max_length=256)), - ("password", models.CharField(max_length=256)), - ], - ), - migrations.CreateModel( - name="MailRule", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("name", models.CharField(max_length=256)), - ("folder", models.CharField(default="INBOX", max_length=256)), - ( - "filter_from", - models.CharField(blank=True, max_length=256, null=True), - ), - ( - "filter_subject", - models.CharField(blank=True, max_length=256, null=True), - ), - ( - "filter_body", - models.CharField(blank=True, max_length=256, null=True), - ), - ("maximum_age", models.PositiveIntegerField(default=30)), - ( - "action", - models.PositiveIntegerField( - choices=[ - (1, "Delete"), - (2, "Move to specified folder"), - (3, "Mark as read, don't process read mails"), - (4, "Flag the mail, don't process flagged mails"), - ], - default=3, - help_text="The action applied to the mail. This action is only performed when documents were consumed from the mail. Mails without attachments will remain entirely untouched.", - ), - ), - ( - "action_parameter", - models.CharField( - blank=True, - help_text="Additional parameter for the action selected above, i.e., the target folder of the move to folder action.", - max_length=256, - null=True, - ), - ), - ( - "assign_title_from", - models.PositiveIntegerField( - choices=[ - (1, "Use subject as title"), - (2, "Use attachment filename as title"), - ], - default=1, - ), - ), - ( - "assign_correspondent_from", - models.PositiveIntegerField( - choices=[ - (1, "Do not assign a correspondent"), - (2, "Use mail address"), - (3, "Use name (or mail address if not available)"), - (4, "Use correspondent selected below"), - ], - default=1, - ), - ), - ( - "account", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="rules", - to="paperless_mail.mailaccount", - ), - ), - ( - "assign_correspondent", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.correspondent", - ), - ), - ( - "assign_document_type", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.documenttype", - ), - ), - ( - "assign_tag", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.tag", - ), - ), - ], - ), - migrations.RunPython( - code=django.db.migrations.operations.special.RunPython.noop, - reverse_code=django.db.migrations.operations.special.RunPython.noop, - ), - migrations.AlterField( - model_name="mailaccount", - name="imap_port", - field=models.IntegerField( - blank=True, - help_text="This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections.", - null=True, - ), - ), - migrations.AlterField( - model_name="mailrule", - name="name", - field=models.CharField(max_length=256, unique=True), - ), - migrations.AddField( - model_name="mailrule", - name="order", - field=models.IntegerField(default=0), - ), - migrations.AlterField( - model_name="mailrule", - name="action", - field=models.PositiveIntegerField( - choices=[ - (3, "Mark as read, don't process read mails"), - (4, "Flag the mail, don't process flagged mails"), - (2, "Move to specified folder"), - (1, "Delete"), - ], - default=3, - ), - ), - migrations.AlterField( - model_name="mailrule", - name="maximum_age", - field=models.PositiveIntegerField( - default=30, - help_text="Specified in days.", - ), - ), - migrations.AlterModelOptions( - name="mailaccount", - options={ - "verbose_name": "mail account", - "verbose_name_plural": "mail accounts", - }, - ), - migrations.AlterModelOptions( - name="mailrule", - options={"verbose_name": "mail rule", "verbose_name_plural": "mail rules"}, - ), - migrations.AlterField( - model_name="mailaccount", - name="imap_port", - field=models.IntegerField( - blank=True, - help_text="This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections.", - null=True, - verbose_name="IMAP port", - ), - ), - migrations.AlterField( - model_name="mailaccount", - name="imap_security", - field=models.PositiveIntegerField( - choices=[(1, "No encryption"), (2, "Use SSL"), (3, "Use STARTTLS")], - default=2, - verbose_name="IMAP security", - ), - ), - migrations.AlterField( - model_name="mailaccount", - name="imap_server", - field=models.CharField(max_length=256, verbose_name="IMAP server"), - ), - migrations.AlterField( - model_name="mailaccount", - name="name", - field=models.CharField(max_length=256, unique=True, verbose_name="name"), - ), - migrations.AlterField( - model_name="mailaccount", - name="password", - field=models.CharField(max_length=256, verbose_name="password"), - ), - migrations.AlterField( - model_name="mailaccount", - name="username", - field=models.CharField(max_length=256, verbose_name="username"), - ), - migrations.AlterField( - model_name="mailrule", - name="account", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="rules", - to="paperless_mail.mailaccount", - verbose_name="account", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="action", - field=models.PositiveIntegerField( - choices=[ - (3, "Mark as read, don't process read mails"), - (4, "Flag the mail, don't process flagged mails"), - (2, "Move to specified folder"), - (1, "Delete"), - ], - default=3, - verbose_name="action", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="action_parameter", - field=models.CharField( - blank=True, - help_text="Additional parameter for the action selected above, i.e., the target folder of the move to folder action.", - max_length=256, - null=True, - verbose_name="action parameter", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="assign_correspondent", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.correspondent", - verbose_name="assign this correspondent", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="assign_correspondent_from", - field=models.PositiveIntegerField( - choices=[ - (1, "Do not assign a correspondent"), - (2, "Use mail address"), - (3, "Use name (or mail address if not available)"), - (4, "Use correspondent selected below"), - ], - default=1, - verbose_name="assign correspondent from", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="assign_document_type", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.documenttype", - verbose_name="assign this document type", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="assign_tag", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.tag", - verbose_name="assign this tag", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="assign_title_from", - field=models.PositiveIntegerField( - choices=[ - (1, "Use subject as title"), - (2, "Use attachment filename as title"), - ], - default=1, - verbose_name="assign title from", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="filter_body", - field=models.CharField( - blank=True, - max_length=256, - null=True, - verbose_name="filter body", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="filter_from", - field=models.CharField( - blank=True, - max_length=256, - null=True, - verbose_name="filter from", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="filter_subject", - field=models.CharField( - blank=True, - max_length=256, - null=True, - verbose_name="filter subject", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="folder", - field=models.CharField( - default="INBOX", - max_length=256, - verbose_name="folder", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="maximum_age", - field=models.PositiveIntegerField( - default=30, - help_text="Specified in days.", - verbose_name="maximum age", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="name", - field=models.CharField(max_length=256, unique=True, verbose_name="name"), - ), - migrations.AlterField( - model_name="mailrule", - name="order", - field=models.IntegerField(default=0, verbose_name="order"), - ), - migrations.AddField( - model_name="mailrule", - name="attachment_type", - field=models.PositiveIntegerField( - choices=[ - (1, "Only process attachments."), - (2, "Process all files, including 'inline' attachments."), - ], - default=1, - help_text="Inline attachments include embedded images, so it's best to combine this option with a filename filter.", - verbose_name="attachment type", - ), - ), - migrations.AddField( - model_name="mailrule", - name="filter_attachment_filename", - field=models.CharField( - blank=True, - help_text="Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.", - max_length=256, - null=True, - verbose_name="filter attachment filename", - ), - ), - migrations.AddField( - model_name="mailaccount", - name="character_set", - field=models.CharField( - default="UTF-8", - help_text="The character set to use when communicating with the mail server, such as 'UTF-8' or 'US-ASCII'.", - max_length=256, - verbose_name="character set", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="action_parameter", - field=models.CharField( - blank=True, - help_text="Additional parameter for the action selected above, i.e., the target folder of the move to folder action. Subfolders must be separated by dots.", - max_length=256, - null=True, - verbose_name="action parameter", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="folder", - field=models.CharField( - default="INBOX", - help_text="Subfolders must be separated by dots.", - max_length=256, - verbose_name="folder", - ), - ), - migrations.AddField( - model_name="mailrule", - name="assign_tags", - field=models.ManyToManyField( - blank=True, - related_name="mail_rules_multi", - to="documents.tag", - verbose_name="assign this tag", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0002_auto_20201117_1334.py b/src/paperless_mail/migrations/0002_auto_20201117_1334.py deleted file mode 100644 index 1f4df3f6d..000000000 --- a/src/paperless_mail/migrations/0002_auto_20201117_1334.py +++ /dev/null @@ -1,12 +0,0 @@ -# Generated by Django 3.1.3 on 2020-11-17 13:34 - -from django.db import migrations -from django.db.migrations import RunPython - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0001_initial"), - ] - - operations = [RunPython(migrations.RunPython.noop, migrations.RunPython.noop)] diff --git a/src/paperless_mail/migrations/0003_auto_20201118_1940.py b/src/paperless_mail/migrations/0003_auto_20201118_1940.py deleted file mode 100644 index a1263db05..000000000 --- a/src/paperless_mail/migrations/0003_auto_20201118_1940.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 3.1.3 on 2020-11-18 19:40 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0002_auto_20201117_1334"), - ] - - operations = [ - migrations.AlterField( - model_name="mailaccount", - name="imap_port", - field=models.IntegerField( - blank=True, - help_text="This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections.", - null=True, - ), - ), - migrations.AlterField( - model_name="mailrule", - name="name", - field=models.CharField(max_length=256, unique=True), - ), - ] diff --git a/src/paperless_mail/migrations/0005_help_texts.py b/src/paperless_mail/migrations/0005_help_texts.py deleted file mode 100644 index 8e49238e9..000000000 --- a/src/paperless_mail/migrations/0005_help_texts.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 3.1.3 on 2020-11-22 10:36 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0004_mailrule_order"), - ] - - operations = [ - migrations.AlterField( - model_name="mailrule", - name="action", - field=models.PositiveIntegerField( - choices=[ - (3, "Mark as read, don't process read mails"), - (4, "Flag the mail, don't process flagged mails"), - (2, "Move to specified folder"), - (1, "Delete"), - ], - default=3, - ), - ), - migrations.AlterField( - model_name="mailrule", - name="maximum_age", - field=models.PositiveIntegerField( - default=30, - help_text="Specified in days.", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0006_auto_20210101_2340.py b/src/paperless_mail/migrations/0006_auto_20210101_2340.py deleted file mode 100644 index 2c2ff9fa8..000000000 --- a/src/paperless_mail/migrations/0006_auto_20210101_2340.py +++ /dev/null @@ -1,217 +0,0 @@ -# Generated by Django 3.1.4 on 2021-01-01 23:40 - -import django.db.models.deletion -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("documents", "1011_auto_20210101_2340"), - ("paperless_mail", "0005_help_texts"), - ] - - operations = [ - migrations.AlterModelOptions( - name="mailaccount", - options={ - "verbose_name": "mail account", - "verbose_name_plural": "mail accounts", - }, - ), - migrations.AlterModelOptions( - name="mailrule", - options={"verbose_name": "mail rule", "verbose_name_plural": "mail rules"}, - ), - migrations.AlterField( - model_name="mailaccount", - name="imap_port", - field=models.IntegerField( - blank=True, - help_text="This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections.", - null=True, - verbose_name="IMAP port", - ), - ), - migrations.AlterField( - model_name="mailaccount", - name="imap_security", - field=models.PositiveIntegerField( - choices=[(1, "No encryption"), (2, "Use SSL"), (3, "Use STARTTLS")], - default=2, - verbose_name="IMAP security", - ), - ), - migrations.AlterField( - model_name="mailaccount", - name="imap_server", - field=models.CharField(max_length=256, verbose_name="IMAP server"), - ), - migrations.AlterField( - model_name="mailaccount", - name="name", - field=models.CharField(max_length=256, unique=True, verbose_name="name"), - ), - migrations.AlterField( - model_name="mailaccount", - name="password", - field=models.CharField(max_length=256, verbose_name="password"), - ), - migrations.AlterField( - model_name="mailaccount", - name="username", - field=models.CharField(max_length=256, verbose_name="username"), - ), - migrations.AlterField( - model_name="mailrule", - name="account", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="rules", - to="paperless_mail.mailaccount", - verbose_name="account", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="action", - field=models.PositiveIntegerField( - choices=[ - (3, "Mark as read, don't process read mails"), - (4, "Flag the mail, don't process flagged mails"), - (2, "Move to specified folder"), - (1, "Delete"), - ], - default=3, - verbose_name="action", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="action_parameter", - field=models.CharField( - blank=True, - help_text="Additional parameter for the action selected above, i.e., the target folder of the move to folder action.", - max_length=256, - null=True, - verbose_name="action parameter", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="assign_correspondent", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.correspondent", - verbose_name="assign this correspondent", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="assign_correspondent_from", - field=models.PositiveIntegerField( - choices=[ - (1, "Do not assign a correspondent"), - (2, "Use mail address"), - (3, "Use name (or mail address if not available)"), - (4, "Use correspondent selected below"), - ], - default=1, - verbose_name="assign correspondent from", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="assign_document_type", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.documenttype", - verbose_name="assign this document type", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="assign_tag", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="documents.tag", - verbose_name="assign this tag", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="assign_title_from", - field=models.PositiveIntegerField( - choices=[ - (1, "Use subject as title"), - (2, "Use attachment filename as title"), - ], - default=1, - verbose_name="assign title from", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="filter_body", - field=models.CharField( - blank=True, - max_length=256, - null=True, - verbose_name="filter body", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="filter_from", - field=models.CharField( - blank=True, - max_length=256, - null=True, - verbose_name="filter from", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="filter_subject", - field=models.CharField( - blank=True, - max_length=256, - null=True, - verbose_name="filter subject", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="folder", - field=models.CharField( - default="INBOX", - max_length=256, - verbose_name="folder", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="maximum_age", - field=models.PositiveIntegerField( - default=30, - help_text="Specified in days.", - verbose_name="maximum age", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="name", - field=models.CharField(max_length=256, unique=True, verbose_name="name"), - ), - migrations.AlterField( - model_name="mailrule", - name="order", - field=models.IntegerField(default=0, verbose_name="order"), - ), - ] diff --git a/src/paperless_mail/migrations/0007_auto_20210106_0138.py b/src/paperless_mail/migrations/0007_auto_20210106_0138.py deleted file mode 100644 index c51a4aebe..000000000 --- a/src/paperless_mail/migrations/0007_auto_20210106_0138.py +++ /dev/null @@ -1,37 +0,0 @@ -# Generated by Django 3.1.5 on 2021-01-06 01:38 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0006_auto_20210101_2340"), - ] - - operations = [ - migrations.AddField( - model_name="mailrule", - name="attachment_type", - field=models.PositiveIntegerField( - choices=[ - (1, "Only process attachments."), - (2, "Process all files, including 'inline' attachments."), - ], - default=1, - help_text="Inline attachments include embedded images, so it's best to combine this option with a filename filter.", - verbose_name="attachment type", - ), - ), - migrations.AddField( - model_name="mailrule", - name="filter_attachment_filename", - field=models.CharField( - blank=True, - help_text="Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.", - max_length=256, - null=True, - verbose_name="filter attachment filename", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0008_auto_20210516_0940.py b/src/paperless_mail/migrations/0008_auto_20210516_0940.py deleted file mode 100644 index b2fc062dd..000000000 --- a/src/paperless_mail/migrations/0008_auto_20210516_0940.py +++ /dev/null @@ -1,44 +0,0 @@ -# Generated by Django 3.2.3 on 2021-05-16 09:40 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0007_auto_20210106_0138"), - ] - - operations = [ - migrations.AddField( - model_name="mailaccount", - name="character_set", - field=models.CharField( - default="UTF-8", - help_text="The character set to use when communicating with the mail server, such as 'UTF-8' or 'US-ASCII'.", - max_length=256, - verbose_name="character set", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="action_parameter", - field=models.CharField( - blank=True, - help_text="Additional parameter for the action selected above, i.e., the target folder of the move to folder action. Subfolders must be separated by dots.", - max_length=256, - null=True, - verbose_name="action parameter", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="folder", - field=models.CharField( - default="INBOX", - help_text="Subfolders must be separated by dots.", - max_length=256, - verbose_name="folder", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0009_alter_mailrule_action_alter_mailrule_folder.py b/src/paperless_mail/migrations/0009_alter_mailrule_action_alter_mailrule_folder.py deleted file mode 100644 index 47fdaff12..000000000 --- a/src/paperless_mail/migrations/0009_alter_mailrule_action_alter_mailrule_folder.py +++ /dev/null @@ -1,37 +0,0 @@ -# Generated by Django 4.0.3 on 2022-03-28 17:40 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0008_auto_20210516_0940"), - ] - - operations = [ - migrations.AlterField( - model_name="mailrule", - name="action", - field=models.PositiveIntegerField( - choices=[ - (1, "Mark as read, don't process read mails"), - (2, "Flag the mail, don't process flagged mails"), - (3, "Move to specified folder"), - (4, "Delete"), - ], - default=3, - verbose_name="action", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="folder", - field=models.CharField( - default="INBOX", - help_text="Subfolders must be separated by a delimiter, often a dot ('.') or slash ('/'), but it varies by mail server.", - max_length=256, - verbose_name="folder", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0009_mailrule_assign_tags.py b/src/paperless_mail/migrations/0009_mailrule_assign_tags.py deleted file mode 100644 index fbe359814..000000000 --- a/src/paperless_mail/migrations/0009_mailrule_assign_tags.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.2.12 on 2022-03-11 15:00 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0008_auto_20210516_0940"), - ] - - operations = [ - migrations.AddField( - model_name="mailrule", - name="assign_tags", - field=models.ManyToManyField( - blank=True, - related_name="mail_rules_multi", - to="documents.Tag", - verbose_name="assign this tag", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0010_auto_20220311_1602.py b/src/paperless_mail/migrations/0010_auto_20220311_1602.py deleted file mode 100644 index 0511608ca..000000000 --- a/src/paperless_mail/migrations/0010_auto_20220311_1602.py +++ /dev/null @@ -1,39 +0,0 @@ -# Generated by Django 3.2.12 on 2022-03-11 15:02 - -from django.db import migrations - - -def migrate_tag_to_tags(apps, schema_editor): - # Manual data migration, see - # https://docs.djangoproject.com/en/3.2/topics/migrations/#data-migrations - # - # Copy the assign_tag property to the new assign_tags set if it exists. - MailRule = apps.get_model("paperless_mail", "MailRule") - for mail_rule in MailRule.objects.all(): - if mail_rule.assign_tag: - mail_rule.assign_tags.add(mail_rule.assign_tag) - mail_rule.save() - - -def migrate_tags_to_tag(apps, schema_editor): - # Manual data migration, see - # https://docs.djangoproject.com/en/3.2/topics/migrations/#data-migrations - # - # Copy the unique value in the assign_tags set to the old assign_tag property. - # Do nothing if the tag is not unique. - MailRule = apps.get_model("paperless_mail", "MailRule") - for mail_rule in MailRule.objects.all(): - tags = mail_rule.assign_tags.all() - if len(tags) == 1: - mail_rule.assign_tag = tags[0] - mail_rule.save() - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0009_mailrule_assign_tags"), - ] - - operations = [ - migrations.RunPython(migrate_tag_to_tags, migrate_tags_to_tag), - ] diff --git a/src/paperless_mail/migrations/0011_remove_mailrule_assign_tag_squashed_0024_alter_mailrule_name_and_more.py b/src/paperless_mail/migrations/0011_remove_mailrule_assign_tag_squashed_0024_alter_mailrule_name_and_more.py deleted file mode 100644 index c48ebf33b..000000000 --- a/src/paperless_mail/migrations/0011_remove_mailrule_assign_tag_squashed_0024_alter_mailrule_name_and_more.py +++ /dev/null @@ -1,321 +0,0 @@ -# Generated by Django 4.2.13 on 2024-06-28 17:47 - -import django.db.models.deletion -import django.utils.timezone -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - replaces = [ - ("paperless_mail", "0011_remove_mailrule_assign_tag"), - ("paperless_mail", "0012_alter_mailrule_assign_tags"), - ("paperless_mail", "0009_alter_mailrule_action_alter_mailrule_folder"), - ("paperless_mail", "0013_merge_20220412_1051"), - ("paperless_mail", "0014_alter_mailrule_action"), - ("paperless_mail", "0015_alter_mailrule_action"), - ("paperless_mail", "0016_mailrule_consumption_scope"), - ("paperless_mail", "0017_mailaccount_owner_mailrule_owner"), - ("paperless_mail", "0018_processedmail"), - ("paperless_mail", "0019_mailrule_filter_to"), - ("paperless_mail", "0020_mailaccount_is_token"), - ("paperless_mail", "0021_alter_mailaccount_password"), - ("paperless_mail", "0022_mailrule_assign_owner_from_rule_and_more"), - ("paperless_mail", "0023_remove_mailrule_filter_attachment_filename_and_more"), - ("paperless_mail", "0024_alter_mailrule_name_and_more"), - ] - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("paperless_mail", "0010_auto_20220311_1602"), - ] - - operations = [ - migrations.RemoveField( - model_name="mailrule", - name="assign_tag", - ), - migrations.AlterField( - model_name="mailrule", - name="assign_tags", - field=models.ManyToManyField( - blank=True, - to="documents.tag", - verbose_name="assign this tag", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="action", - field=models.PositiveIntegerField( - choices=[ - (1, "Mark as read, don't process read mails"), - (2, "Flag the mail, don't process flagged mails"), - (3, "Move to specified folder"), - (4, "Delete"), - ], - default=3, - verbose_name="action", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="folder", - field=models.CharField( - default="INBOX", - help_text="Subfolders must be separated by a delimiter, often a dot ('.') or slash ('/'), but it varies by mail server.", - max_length=256, - verbose_name="folder", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="action", - field=models.PositiveIntegerField( - choices=[ - (1, "Delete"), - (2, "Move to specified folder"), - (3, "Mark as read, don't process read mails"), - (4, "Flag the mail, don't process flagged mails"), - ], - default=3, - verbose_name="action", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="action", - field=models.PositiveIntegerField( - choices=[ - (1, "Delete"), - (2, "Move to specified folder"), - (3, "Mark as read, don't process read mails"), - (4, "Flag the mail, don't process flagged mails"), - (5, "Tag the mail with specified tag, don't process tagged mails"), - ], - default=3, - verbose_name="action", - ), - ), - migrations.AddField( - model_name="mailrule", - name="consumption_scope", - field=models.PositiveIntegerField( - choices=[ - (1, "Only process attachments."), - ( - 2, - "Process full Mail (with embedded attachments in file) as .eml", - ), - ( - 3, - "Process full Mail (with embedded attachments in file) as .eml + process attachments as separate documents", - ), - ], - default=1, - verbose_name="consumption scope", - ), - ), - migrations.AddField( - model_name="mailaccount", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AddField( - model_name="mailrule", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.CreateModel( - name="ProcessedMail", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "folder", - models.CharField( - editable=False, - max_length=256, - verbose_name="folder", - ), - ), - ( - "uid", - models.CharField( - editable=False, - max_length=256, - verbose_name="uid", - ), - ), - ( - "subject", - models.CharField( - editable=False, - max_length=256, - verbose_name="subject", - ), - ), - ( - "received", - models.DateTimeField(editable=False, verbose_name="received"), - ), - ( - "processed", - models.DateTimeField( - default=django.utils.timezone.now, - editable=False, - verbose_name="processed", - ), - ), - ( - "status", - models.CharField( - editable=False, - max_length=256, - verbose_name="status", - ), - ), - ( - "error", - models.TextField( - blank=True, - editable=False, - null=True, - verbose_name="error", - ), - ), - ( - "owner", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - ( - "rule", - models.ForeignKey( - editable=False, - on_delete=django.db.models.deletion.CASCADE, - to="paperless_mail.mailrule", - ), - ), - ], - options={ - "abstract": False, - }, - ), - migrations.AddField( - model_name="mailrule", - name="filter_to", - field=models.CharField( - blank=True, - max_length=256, - null=True, - verbose_name="filter to", - ), - ), - migrations.AddField( - model_name="mailaccount", - name="is_token", - field=models.BooleanField( - default=False, - verbose_name="Is token authentication", - ), - ), - migrations.AlterField( - model_name="mailaccount", - name="password", - field=models.CharField(max_length=2048, verbose_name="password"), - ), - migrations.AddField( - model_name="mailrule", - name="assign_owner_from_rule", - field=models.BooleanField( - default=True, - verbose_name="Assign the rule owner to documents", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="assign_title_from", - field=models.PositiveIntegerField( - choices=[ - (1, "Use subject as title"), - (2, "Use attachment filename as title"), - (3, "Do not assign title from rule"), - ], - default=1, - verbose_name="assign title from", - ), - ), - migrations.RenameField( - model_name="mailrule", - old_name="filter_attachment_filename", - new_name="filter_attachment_filename_include", - ), - migrations.AddField( - model_name="mailrule", - name="filter_attachment_filename_exclude", - field=models.CharField( - blank=True, - help_text="Do not consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.", - max_length=256, - null=True, - verbose_name="filter attachment filename exclusive", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="filter_attachment_filename_include", - field=models.CharField( - blank=True, - help_text="Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.", - max_length=256, - null=True, - verbose_name="filter attachment filename inclusive", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="name", - field=models.CharField(max_length=256, verbose_name="name"), - ), - migrations.AddConstraint( - model_name="mailrule", - constraint=models.UniqueConstraint( - fields=("name", "owner"), - name="paperless_mail_mailrule_unique_name_owner", - ), - ), - migrations.AddConstraint( - model_name="mailrule", - constraint=models.UniqueConstraint( - condition=models.Q(("owner__isnull", True)), - fields=("name",), - name="paperless_mail_mailrule_name_unique", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0012_alter_mailrule_assign_tags.py b/src/paperless_mail/migrations/0012_alter_mailrule_assign_tags.py deleted file mode 100644 index 83ece3bba..000000000 --- a/src/paperless_mail/migrations/0012_alter_mailrule_assign_tags.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 3.2.12 on 2022-03-11 16:21 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0011_remove_mailrule_assign_tag"), - ] - - operations = [ - migrations.AlterField( - model_name="mailrule", - name="assign_tags", - field=models.ManyToManyField( - blank=True, - to="documents.Tag", - verbose_name="assign this tag", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0013_merge_20220412_1051.py b/src/paperless_mail/migrations/0013_merge_20220412_1051.py deleted file mode 100644 index 0310fd083..000000000 --- a/src/paperless_mail/migrations/0013_merge_20220412_1051.py +++ /dev/null @@ -1,12 +0,0 @@ -# Generated by Django 4.0.4 on 2022-04-12 08:51 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0009_alter_mailrule_action_alter_mailrule_folder"), - ("paperless_mail", "0012_alter_mailrule_assign_tags"), - ] - - operations = [] diff --git a/src/paperless_mail/migrations/0014_alter_mailrule_action.py b/src/paperless_mail/migrations/0014_alter_mailrule_action.py deleted file mode 100644 index 6be3ddf69..000000000 --- a/src/paperless_mail/migrations/0014_alter_mailrule_action.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 4.0.4 on 2022-04-18 22:57 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0013_merge_20220412_1051"), - ] - - operations = [ - migrations.AlterField( - model_name="mailrule", - name="action", - field=models.PositiveIntegerField( - choices=[ - (1, "Delete"), - (2, "Move to specified folder"), - (3, "Mark as read, don't process read mails"), - (4, "Flag the mail, don't process flagged mails"), - ], - default=3, - verbose_name="action", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0015_alter_mailrule_action.py b/src/paperless_mail/migrations/0015_alter_mailrule_action.py deleted file mode 100644 index 80de9b2b1..000000000 --- a/src/paperless_mail/migrations/0015_alter_mailrule_action.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 4.0.4 on 2022-05-29 13:21 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0014_alter_mailrule_action"), - ] - - operations = [ - migrations.AlterField( - model_name="mailrule", - name="action", - field=models.PositiveIntegerField( - choices=[ - (1, "Delete"), - (2, "Move to specified folder"), - (3, "Mark as read, don't process read mails"), - (4, "Flag the mail, don't process flagged mails"), - (5, "Tag the mail with specified tag, don't process tagged mails"), - ], - default=3, - verbose_name="action", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0016_mailrule_consumption_scope.py b/src/paperless_mail/migrations/0016_mailrule_consumption_scope.py deleted file mode 100644 index d4a0ba590..000000000 --- a/src/paperless_mail/migrations/0016_mailrule_consumption_scope.py +++ /dev/null @@ -1,32 +0,0 @@ -# Generated by Django 4.0.4 on 2022-07-11 22:02 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0015_alter_mailrule_action"), - ] - - operations = [ - migrations.AddField( - model_name="mailrule", - name="consumption_scope", - field=models.PositiveIntegerField( - choices=[ - (1, "Only process attachments."), - ( - 2, - "Process full Mail (with embedded attachments in file) as .eml", - ), - ( - 3, - "Process full Mail (with embedded attachments in file) as .eml + process attachments as separate documents", - ), - ], - default=1, - verbose_name="consumption scope", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0017_mailaccount_owner_mailrule_owner.py b/src/paperless_mail/migrations/0017_mailaccount_owner_mailrule_owner.py deleted file mode 100644 index 98cfef014..000000000 --- a/src/paperless_mail/migrations/0017_mailaccount_owner_mailrule_owner.py +++ /dev/null @@ -1,38 +0,0 @@ -# Generated by Django 4.1.3 on 2022-12-06 04:48 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("paperless_mail", "0016_mailrule_consumption_scope"), - ] - - operations = [ - migrations.AddField( - model_name="mailaccount", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AddField( - model_name="mailrule", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0018_processedmail.py b/src/paperless_mail/migrations/0018_processedmail.py deleted file mode 100644 index 3307f7579..000000000 --- a/src/paperless_mail/migrations/0018_processedmail.py +++ /dev/null @@ -1,105 +0,0 @@ -# Generated by Django 4.1.5 on 2023-03-03 18:38 - -import django.db.models.deletion -import django.utils.timezone -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("paperless_mail", "0017_mailaccount_owner_mailrule_owner"), - ] - - operations = [ - migrations.CreateModel( - name="ProcessedMail", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "folder", - models.CharField( - editable=False, - max_length=256, - verbose_name="folder", - ), - ), - ( - "uid", - models.CharField( - editable=False, - max_length=256, - verbose_name="uid", - ), - ), - ( - "subject", - models.CharField( - editable=False, - max_length=256, - verbose_name="subject", - ), - ), - ( - "received", - models.DateTimeField(editable=False, verbose_name="received"), - ), - ( - "processed", - models.DateTimeField( - default=django.utils.timezone.now, - editable=False, - verbose_name="processed", - ), - ), - ( - "status", - models.CharField( - editable=False, - max_length=256, - verbose_name="status", - ), - ), - ( - "error", - models.TextField( - blank=True, - editable=False, - null=True, - verbose_name="error", - ), - ), - ( - "owner", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - ( - "rule", - models.ForeignKey( - editable=False, - on_delete=django.db.models.deletion.CASCADE, - to="paperless_mail.mailrule", - ), - ), - ], - options={ - "abstract": False, - }, - ), - ] diff --git a/src/paperless_mail/migrations/0019_mailrule_filter_to.py b/src/paperless_mail/migrations/0019_mailrule_filter_to.py deleted file mode 100644 index 8951be290..000000000 --- a/src/paperless_mail/migrations/0019_mailrule_filter_to.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 4.1.7 on 2023-03-11 21:08 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0018_processedmail"), - ] - - operations = [ - migrations.AddField( - model_name="mailrule", - name="filter_to", - field=models.CharField( - blank=True, - max_length=256, - null=True, - verbose_name="filter to", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0020_mailaccount_is_token.py b/src/paperless_mail/migrations/0020_mailaccount_is_token.py deleted file mode 100644 index 81ce50a19..000000000 --- a/src/paperless_mail/migrations/0020_mailaccount_is_token.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 4.1.7 on 2023-03-22 17:51 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0019_mailrule_filter_to"), - ] - - operations = [ - migrations.AddField( - model_name="mailaccount", - name="is_token", - field=models.BooleanField( - default=False, - verbose_name="Is token authentication", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0021_alter_mailaccount_password.py b/src/paperless_mail/migrations/0021_alter_mailaccount_password.py deleted file mode 100644 index 0c012b98b..000000000 --- a/src/paperless_mail/migrations/0021_alter_mailaccount_password.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.1.7 on 2023-04-20 15:03 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0020_mailaccount_is_token"), - ] - - operations = [ - migrations.AlterField( - model_name="mailaccount", - name="password", - field=models.CharField(max_length=2048, verbose_name="password"), - ), - ] diff --git a/src/paperless_mail/migrations/0022_mailrule_assign_owner_from_rule_and_more.py b/src/paperless_mail/migrations/0022_mailrule_assign_owner_from_rule_and_more.py deleted file mode 100644 index f2c59a5bf..000000000 --- a/src/paperless_mail/migrations/0022_mailrule_assign_owner_from_rule_and_more.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 4.1.11 on 2023-09-18 18:50 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0021_alter_mailaccount_password"), - ] - - operations = [ - migrations.AddField( - model_name="mailrule", - name="assign_owner_from_rule", - field=models.BooleanField( - default=True, - verbose_name="Assign the rule owner to documents", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="assign_title_from", - field=models.PositiveIntegerField( - choices=[ - (1, "Use subject as title"), - (2, "Use attachment filename as title"), - (3, "Do not assign title from rule"), - ], - default=1, - verbose_name="assign title from", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0023_remove_mailrule_filter_attachment_filename_and_more.py b/src/paperless_mail/migrations/0023_remove_mailrule_filter_attachment_filename_and_more.py deleted file mode 100644 index 1a1eac790..000000000 --- a/src/paperless_mail/migrations/0023_remove_mailrule_filter_attachment_filename_and_more.py +++ /dev/null @@ -1,40 +0,0 @@ -# Generated by Django 4.2.7 on 2023-12-04 03:06 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0022_mailrule_assign_owner_from_rule_and_more"), - ] - - operations = [ - migrations.RenameField( - model_name="mailrule", - old_name="filter_attachment_filename", - new_name="filter_attachment_filename_include", - ), - migrations.AddField( - model_name="mailrule", - name="filter_attachment_filename_exclude", - field=models.CharField( - blank=True, - help_text="Do not consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.", - max_length=256, - null=True, - verbose_name="filter attachment filename exclusive", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="filter_attachment_filename_include", - field=models.CharField( - blank=True, - help_text="Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.", - max_length=256, - null=True, - verbose_name="filter attachment filename inclusive", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0024_alter_mailrule_name_and_more.py b/src/paperless_mail/migrations/0024_alter_mailrule_name_and_more.py deleted file mode 100644 index c2840d0e4..000000000 --- a/src/paperless_mail/migrations/0024_alter_mailrule_name_and_more.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 4.2.11 on 2024-06-05 16:51 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0023_remove_mailrule_filter_attachment_filename_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="mailrule", - name="name", - field=models.CharField(max_length=256, verbose_name="name"), - ), - migrations.AddConstraint( - model_name="mailrule", - constraint=models.UniqueConstraint( - fields=("name", "owner"), - name="paperless_mail_mailrule_unique_name_owner", - ), - ), - migrations.AddConstraint( - model_name="mailrule", - constraint=models.UniqueConstraint( - condition=models.Q(("owner__isnull", True)), - fields=("name",), - name="paperless_mail_mailrule_name_unique", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0025_alter_mailaccount_owner_alter_mailrule_owner_and_more.py b/src/paperless_mail/migrations/0025_alter_mailaccount_owner_alter_mailrule_owner_and_more.py deleted file mode 100644 index 308ebdf15..000000000 --- a/src/paperless_mail/migrations/0025_alter_mailaccount_owner_alter_mailrule_owner_and_more.py +++ /dev/null @@ -1,52 +0,0 @@ -# Generated by Django 4.2.13 on 2024-07-09 16:39 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("paperless_mail", "0024_alter_mailrule_name_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="mailaccount", - name="owner", - field=models.ForeignKey( - blank=True, - default=None, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AlterField( - model_name="mailrule", - name="owner", - field=models.ForeignKey( - blank=True, - default=None, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - migrations.AlterField( - model_name="processedmail", - name="owner", - field=models.ForeignKey( - blank=True, - default=None, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - verbose_name="owner", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0026_mailrule_enabled.py b/src/paperless_mail/migrations/0026_mailrule_enabled.py deleted file mode 100644 index c10ee698c..000000000 --- a/src/paperless_mail/migrations/0026_mailrule_enabled.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 5.1.1 on 2024-09-30 15:17 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ( - "paperless_mail", - "0025_alter_mailaccount_owner_alter_mailrule_owner_and_more", - ), - ] - - operations = [ - migrations.AddField( - model_name="mailrule", - name="enabled", - field=models.BooleanField(default=True, verbose_name="enabled"), - ), - ] diff --git a/src/paperless_mail/migrations/0027_mailaccount_expiration_mailaccount_account_type_and_more.py b/src/paperless_mail/migrations/0027_mailaccount_expiration_mailaccount_account_type_and_more.py deleted file mode 100644 index 3fb1e6af2..000000000 --- a/src/paperless_mail/migrations/0027_mailaccount_expiration_mailaccount_account_type_and_more.py +++ /dev/null @@ -1,48 +0,0 @@ -# Generated by Django 5.1.1 on 2024-10-05 17:12 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0026_mailrule_enabled"), - ] - - operations = [ - migrations.AlterField( - model_name="mailaccount", - name="password", - field=models.CharField(max_length=3072, verbose_name="password"), - ), - migrations.AddField( - model_name="mailaccount", - name="expiration", - field=models.DateTimeField( - blank=True, - help_text="The expiration date of the refresh token. ", - null=True, - verbose_name="expiration", - ), - ), - migrations.AddField( - model_name="mailaccount", - name="account_type", - field=models.PositiveIntegerField( - choices=[(1, "IMAP"), (2, "Gmail OAuth"), (3, "Outlook OAuth")], - default=1, - verbose_name="account type", - ), - ), - migrations.AddField( - model_name="mailaccount", - name="refresh_token", - field=models.CharField( - blank=True, - help_text="The refresh token to use for token authentication e.g. with oauth2.", - max_length=3072, - null=True, - verbose_name="refresh token", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0028_alter_mailaccount_password_and_more.py b/src/paperless_mail/migrations/0028_alter_mailaccount_password_and_more.py deleted file mode 100644 index 2a0279651..000000000 --- a/src/paperless_mail/migrations/0028_alter_mailaccount_password_and_more.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by Django 5.1.1 on 2024-10-30 04:31 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ( - "paperless_mail", - "0027_mailaccount_expiration_mailaccount_account_type_and_more", - ), - ] - - operations = [ - migrations.AlterField( - model_name="mailaccount", - name="password", - field=models.TextField(verbose_name="password"), - ), - migrations.AlterField( - model_name="mailaccount", - name="refresh_token", - field=models.TextField( - blank=True, - help_text="The refresh token to use for token authentication e.g. with oauth2.", - null=True, - verbose_name="refresh token", - ), - ), - ] diff --git a/src/paperless_mail/migrations/0029_mailrule_pdf_layout.py b/src/paperless_mail/migrations/0029_mailrule_pdf_layout.py deleted file mode 100644 index fe7a93b71..000000000 --- a/src/paperless_mail/migrations/0029_mailrule_pdf_layout.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 5.1.3 on 2024-11-24 12:39 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - dependencies = [ - ("paperless_mail", "0028_alter_mailaccount_password_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="mailrule", - name="pdf_layout", - field=models.PositiveIntegerField( - choices=[ - (0, "System default"), - (1, "Text, then HTML"), - (2, "HTML, then text"), - (3, "HTML only"), - (4, "Text only"), - ], - default=0, - verbose_name="pdf layout", - ), - ), - ] diff --git a/src/paperless_mail/tests/conftest.py b/src/paperless_mail/tests/conftest.py index 01a98d57d..0742edfa3 100644 --- a/src/paperless_mail/tests/conftest.py +++ b/src/paperless_mail/tests/conftest.py @@ -1,4 +1,3 @@ -import os from collections.abc import Generator from pathlib import Path @@ -70,20 +69,31 @@ def mail_parser() -> MailDocumentParser: @pytest.fixture() -def live_mail_account() -> Generator[MailAccount, None, None]: - try: - account = MailAccount.objects.create( - name="test", - imap_server=os.environ["PAPERLESS_MAIL_TEST_HOST"], - username=os.environ["PAPERLESS_MAIL_TEST_USER"], - password=os.environ["PAPERLESS_MAIL_TEST_PASSWD"], - imap_port=993, - ) - yield account - finally: - account.delete() +def greenmail_mail_account(db: None) -> Generator[MailAccount, None, None]: + """ + Create a mail account configured for local Greenmail server. + """ + account = MailAccount.objects.create( + name="Greenmail Test", + imap_server="localhost", + imap_port=3143, + imap_security=MailAccount.ImapSecurity.NONE, + username="test@localhost", + password="test", + character_set="UTF-8", + ) + yield account + account.delete() @pytest.fixture() def mail_account_handler() -> MailAccountHandler: return MailAccountHandler() + + +@pytest.fixture(scope="session") +def nginx_base_url() -> Generator[str, None, None]: + """ + The base URL for the nginx HTTP server we expect to be alive + """ + yield "http://localhost:8080" diff --git a/src/paperless_mail/tests/samples/html.eml b/src/paperless_mail/tests/samples/html.eml index aaac68cc4..c912acc44 100644 --- a/src/paperless_mail/tests/samples/html.eml +++ b/src/paperless_mail/tests/samples/html.eml @@ -55,7 +55,7 @@ Content-Transfer-Encoding: 7bit

    Some Text

    Has to be rewritten to work.. - This image should not be shown. + This image should not be shown.

    and an embedded image.
    diff --git a/src/paperless_mail/tests/samples/sample.html b/src/paperless_mail/tests/samples/sample.html index c1fd52d43..9d535dba6 100644 --- a/src/paperless_mail/tests/samples/sample.html +++ b/src/paperless_mail/tests/samples/sample.html @@ -6,7 +6,7 @@

    Some Text

    Has to be rewritten to work.. - This image should not be shown. + This image should not be shown.

    and an embedded image.
    diff --git a/src/paperless_mail/tests/test_live_mail.py b/src/paperless_mail/tests/test_live_mail.py index ecf9f73b6..cfd7f88d0 100644 --- a/src/paperless_mail/tests/test_live_mail.py +++ b/src/paperless_mail/tests/test_live_mail.py @@ -1,6 +1,3 @@ -import os -import warnings - import pytest from paperless_mail.mail import MailAccountHandler @@ -9,53 +6,53 @@ from paperless_mail.models import MailAccount from paperless_mail.models import MailRule -# Only run if the environment is setup -# And the environment is not empty (forks, I think) -@pytest.mark.skipif( - "PAPERLESS_MAIL_TEST_HOST" not in os.environ - or not len(os.environ["PAPERLESS_MAIL_TEST_HOST"]), - reason="Live server testing not enabled", -) -@pytest.mark.django_db() -class TestMailLiveServer: - def test_process_non_gmail_server_flag( +@pytest.mark.live +@pytest.mark.greenmail +@pytest.mark.django_db +class TestMailGreenmail: + """ + Mail tests using local Greenmail server + """ + + def test_process_flag( self, mail_account_handler: MailAccountHandler, - live_mail_account: MailAccount, - ): + greenmail_mail_account: MailAccount, + ) -> None: + """ + Test processing mail with FLAG action. + """ + rule = MailRule.objects.create( + name="testrule", + account=greenmail_mail_account, + action=MailRule.MailAction.FLAG, + ) + try: - rule1 = MailRule.objects.create( - name="testrule", - account=live_mail_account, - action=MailRule.MailAction.FLAG, - ) - - mail_account_handler.handle_mail_account(live_mail_account) - - rule1.delete() - + mail_account_handler.handle_mail_account(greenmail_mail_account) except MailError as e: pytest.fail(f"Failure: {e}") - except Exception as e: - warnings.warn(f"Unhandled exception: {e}") + finally: + rule.delete() - def test_process_non_gmail_server_tag( + def test_process_tag( self, mail_account_handler: MailAccountHandler, - live_mail_account: MailAccount, - ): + greenmail_mail_account: MailAccount, + ) -> None: + """ + Test processing mail with TAG action. + """ + rule = MailRule.objects.create( + name="testrule", + account=greenmail_mail_account, + action=MailRule.MailAction.TAG, + action_parameter="TestTag", + ) + try: - rule2 = MailRule.objects.create( - name="testrule", - account=live_mail_account, - action=MailRule.MailAction.TAG, - ) - - mail_account_handler.handle_mail_account(live_mail_account) - - rule2.delete() - + mail_account_handler.handle_mail_account(greenmail_mail_account) except MailError as e: pytest.fail(f"Failure: {e}") - except Exception as e: - warnings.warn(f"Unhandled exception: {e}") + finally: + rule.delete() diff --git a/src/paperless_mail/tests/test_parsers_live.py b/src/paperless_mail/tests/test_parsers_live.py index fd052cc26..8a9487c16 100644 --- a/src/paperless_mail/tests/test_parsers_live.py +++ b/src/paperless_mail/tests/test_parsers_live.py @@ -17,7 +17,7 @@ from paperless_mail.parsers import MailDocumentParser def extract_text(pdf_path: Path) -> str: """ Using pdftotext from poppler, extracts the text of a PDF into a file, - then reads the file contents and returns it + then reads the file contents and returns it. """ with tempfile.NamedTemporaryFile( mode="w+", @@ -38,71 +38,107 @@ def extract_text(pdf_path: Path) -> str: class MailAttachmentMock: - def __init__(self, payload, content_id): + def __init__(self, payload: bytes, content_id: str) -> None: self.payload = payload self.content_id = content_id self.content_type = "image/png" +@pytest.mark.live +@pytest.mark.nginx @pytest.mark.skipif( "PAPERLESS_CI_TEST" not in os.environ, reason="No Gotenberg/Tika servers to test with", ) -class TestUrlCanary: +class TestNginxService: """ - Verify certain URLs are still available so testing is valid still + Verify the local nginx server is responding correctly. + These tests validate that the test infrastructure is working properly + before running the actual parser tests that depend on HTTP resources. """ - def test_online_image_exception_on_not_available(self): + def test_non_existent_resource_returns_404( + self, + nginx_base_url: str, + ) -> None: """ GIVEN: - - Fresh start + - Local nginx server is running WHEN: - - nonexistent image is requested + - A non-existent resource is requested THEN: - - An exception shall be thrown - """ - """ - A public image is used in the html sample file. We have no control - whether this image stays online forever, so here we check if we can detect if is not - available anymore. + - An HTTP 404 status code shall be returned """ resp = httpx.get( - "https://docs.paperless-ngx.com/assets/non-existent.png", + f"{nginx_base_url}/assets/non-existent.png", + timeout=5.0, ) with pytest.raises(httpx.HTTPStatusError) as exec_info: resp.raise_for_status() assert exec_info.value.response.status_code == httpx.codes.NOT_FOUND - def test_is_online_image_still_available(self): + def test_valid_resource_is_available( + self, + nginx_base_url: str, + ) -> None: """ GIVEN: - - Fresh start + - Local nginx server is running WHEN: - - A public image used in the html sample file is requested + - A valid test fixture resource is requested THEN: - - No exception shall be thrown + - The resource shall be returned with HTTP 200 status code + - The response shall contain the expected content type """ - """ - A public image is used in the html sample file. We have no control - whether this image stays online forever, so here we check if it is still there - """ - - # Now check the URL used in samples/sample.html resp = httpx.get( - "https://docs.paperless-ngx.com/assets/logo_full_white.svg", + f"{nginx_base_url}/assets/logo_full_white.svg", + timeout=5.0, ) resp.raise_for_status() + assert resp.status_code == httpx.codes.OK + assert "svg" in resp.headers.get("content-type", "").lower() + def test_server_connectivity( + self, + nginx_base_url: str, + ) -> None: + """ + GIVEN: + - Local test fixtures server should be running + WHEN: + - A request is made to the server root + THEN: + - The server shall respond without connection errors + """ + try: + resp = httpx.get( + nginx_base_url, + timeout=5.0, + follow_redirects=True, + ) + # We don't care about the status code, just that we can connect + assert resp.status_code in {200, 404, 403} + except httpx.ConnectError as e: + pytest.fail( + f"Cannot connect to nginx server at {nginx_base_url}. " + f"Ensure the nginx container is running via docker-compose.ci-test.yml. " + f"Error: {e}", + ) + + +@pytest.mark.live +@pytest.mark.gotenberg +@pytest.mark.tika +@pytest.mark.nginx @pytest.mark.skipif( "PAPERLESS_CI_TEST" not in os.environ, reason="No Gotenberg/Tika servers to test with", ) class TestParserLive: @staticmethod - def imagehash(file, hash_size=18): + def imagehash(file: Path, hash_size: int = 18) -> str: return f"{average_hash(Image.open(file), hash_size)}" def test_get_thumbnail( @@ -112,14 +148,15 @@ class TestParserLive: simple_txt_email_file: Path, simple_txt_email_pdf_file: Path, simple_txt_email_thumbnail_file: Path, - ): + ) -> None: """ GIVEN: - - Fresh start + - A simple text email file + - Mocked PDF generation returning a known PDF WHEN: - - The Thumbnail is requested + - The thumbnail is requested THEN: - - The returned thumbnail image file is as expected + - The returned thumbnail image file shall match the expected hash """ mock_generate_pdf = mocker.patch( "paperless_mail.parsers.MailDocumentParser.generate_pdf", @@ -134,22 +171,28 @@ class TestParserLive: assert self.imagehash(thumb) == self.imagehash( simple_txt_email_thumbnail_file, ), ( - f"Created Thumbnail {thumb} differs from expected file {simple_txt_email_thumbnail_file}" + f"Created thumbnail {thumb} differs from expected file " + f"{simple_txt_email_thumbnail_file}" ) - def test_tika_parse_successful(self, mail_parser: MailDocumentParser): + def test_tika_parse_successful(self, mail_parser: MailDocumentParser) -> None: """ GIVEN: - - Fresh start + - HTML content to parse + - Tika server is running WHEN: - - tika parsing is called + - Tika parsing is called THEN: - - a web request to tika shall be done and the reply es returned + - A web request to Tika shall be made + - The parsed text content shall be returned """ - html = '

    Some Text

    ' + html = ( + '' + "

    Some Text

    " + ) expected_text = "Some Text" - # Check successful parsing parsed = mail_parser.tika_parse(html) assert expected_text == parsed.strip() @@ -160,14 +203,17 @@ class TestParserLive: html_email_file: Path, merged_pdf_first: Path, merged_pdf_second: Path, - ): + ) -> None: """ GIVEN: - - Intermediary pdfs to be merged + - Intermediary PDFs to be merged + - An HTML email file WHEN: - - pdf generation is requested with html file requiring merging of pdfs + - PDF generation is requested with HTML file requiring merging THEN: - - gotenberg is called to merge files and the resulting file is returned + - Gotenberg shall be called to merge files + - The resulting merged PDF shall be returned + - The merged PDF shall contain text from both source PDFs """ mock_generate_pdf_from_html = mocker.patch( "paperless_mail.parsers.MailDocumentParser.generate_pdf_from_html", @@ -200,16 +246,17 @@ class TestParserLive: html_email_file: Path, html_email_pdf_file: Path, html_email_thumbnail_file: Path, - ): + ) -> None: """ GIVEN: - - Fresh start + - An HTML email file WHEN: - - pdf generation from simple eml file is requested + - PDF generation from the email file is requested THEN: - - Gotenberg is called and the resulting file is returned and look as expected. + - Gotenberg shall be called to generate the PDF + - The archive PDF shall contain the expected content + - The generated thumbnail shall match the expected image hash """ - util_call_with_backoff(mail_parser.parse, [html_email_file, "message/rfc822"]) # Check the archive PDF @@ -217,7 +264,7 @@ class TestParserLive: archive_text = extract_text(archive_path) expected_archive_text = extract_text(html_email_pdf_file) - # Archive includes the HTML content, so use in + # Archive includes the HTML content assert expected_archive_text in archive_text # Check the thumbnail @@ -227,9 +274,12 @@ class TestParserLive: ) generated_thumbnail_hash = self.imagehash(generated_thumbnail) - # The created pdf is not reproducible. But the converted image should always look the same. + # The created PDF is not reproducible, but the converted image + # should always look the same expected_hash = self.imagehash(html_email_thumbnail_file) assert generated_thumbnail_hash == expected_hash, ( - f"PDF looks different. Check if {generated_thumbnail} looks weird." + f"PDF thumbnail differs from expected. " + f"Generated: {generated_thumbnail}, " + f"Hash: {generated_thumbnail_hash} vs {expected_hash}" ) diff --git a/src/paperless_mail/tests/test_preprocessor.py b/src/paperless_mail/tests/test_preprocessor.py index 90df77ba8..2ad9410f9 100644 --- a/src/paperless_mail/tests/test_preprocessor.py +++ b/src/paperless_mail/tests/test_preprocessor.py @@ -1,5 +1,7 @@ import email import email.contentmanager +import shutil +import subprocess import tempfile from email.message import Message from email.mime.application import MIMEApplication @@ -34,6 +36,30 @@ class MessageEncryptor: ) self.gpg.gen_key(input_data) + def cleanup(self) -> None: + """ + Kill the gpg-agent process and clean up the temporary GPG home directory. + + This uses gpgconf to properly terminate the agent, which is the officially + recommended cleanup method from the GnuPG project. python-gnupg does not + provide built-in cleanup methods as it's only a wrapper around the gpg CLI. + """ + # Kill the gpg-agent using the official GnuPG cleanup tool + try: + subprocess.run( + ["gpgconf", "--kill", "gpg-agent"], + env={"GNUPGHOME": self.gpg_home}, + check=False, + capture_output=True, + timeout=5, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + # gpgconf not found or hung - agent will timeout eventually + pass + + # Clean up the temporary directory + shutil.rmtree(self.gpg_home, ignore_errors=True) + @staticmethod def get_email_body_without_headers(email_message: Message) -> bytes: """ @@ -85,8 +111,20 @@ class MessageEncryptor: class TestMailMessageGpgDecryptor(TestMail): + @classmethod + def setUpClass(cls): + """Create GPG encryptor once for all tests in this class.""" + super().setUpClass() + cls.messageEncryptor = MessageEncryptor() + + @classmethod + def tearDownClass(cls): + """Clean up GPG resources after all tests complete.""" + if hasattr(cls, "messageEncryptor"): + cls.messageEncryptor.cleanup() + super().tearDownClass() + def setUp(self): - self.messageEncryptor = MessageEncryptor() with override_settings( EMAIL_GNUPG_HOME=self.messageEncryptor.gpg_home, EMAIL_ENABLE_GPG_DECRYPTOR=True, @@ -138,13 +176,28 @@ class TestMailMessageGpgDecryptor(TestMail): def test_decrypt_fails(self): encrypted_message, _ = self.create_encrypted_unencrypted_message_pair() + # This test creates its own empty GPG home to test decryption failure empty_gpg_home = tempfile.mkdtemp() - with override_settings( - EMAIL_ENABLE_GPG_DECRYPTOR=True, - EMAIL_GNUPG_HOME=empty_gpg_home, - ): - message_decryptor = MailMessageDecryptor() - self.assertRaises(Exception, message_decryptor.run, encrypted_message) + try: + with override_settings( + EMAIL_ENABLE_GPG_DECRYPTOR=True, + EMAIL_GNUPG_HOME=empty_gpg_home, + ): + message_decryptor = MailMessageDecryptor() + self.assertRaises(Exception, message_decryptor.run, encrypted_message) + finally: + # Clean up the temporary GPG home used only by this test + try: + subprocess.run( + ["gpgconf", "--kill", "gpg-agent"], + env={"GNUPGHOME": empty_gpg_home}, + check=False, + capture_output=True, + timeout=5, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + pass + shutil.rmtree(empty_gpg_home, ignore_errors=True) def test_decrypt_encrypted_mail(self): """ diff --git a/src/paperless_tika/tests/test_live_tika.py b/src/paperless_tika/tests/test_live_tika.py index 6bb4ff021..432f7482e 100644 --- a/src/paperless_tika/tests/test_live_tika.py +++ b/src/paperless_tika/tests/test_live_tika.py @@ -12,6 +12,9 @@ from paperless_tika.parsers import TikaDocumentParser reason="No Gotenberg/Tika servers to test with", ) @pytest.mark.django_db() +@pytest.mark.live +@pytest.mark.gotenberg +@pytest.mark.tika class TestTikaParserAgainstServer: """ This test case tests the Tika parsing against a live tika server, diff --git a/src/paperless_tika/tests/test_tika_parser.py b/src/paperless_tika/tests/test_tika_parser.py index 05bc4fe2e..a84adb7b1 100644 --- a/src/paperless_tika/tests/test_tika_parser.py +++ b/src/paperless_tika/tests/test_tika_parser.py @@ -128,6 +128,8 @@ class TestTikaParser: request = httpx_mock.get_request() + assert request is not None + expected_field_name = "pdfa" content_type = request.headers["Content-Type"] diff --git a/uv.lock b/uv.lock index c621b203d..067d4cfb7 100644 --- a/uv.lock +++ b/uv.lock @@ -2,11 +2,13 @@ version = 1 revision = 3 requires-python = ">=3.10" resolution-markers = [ - "python_full_version >= '3.11' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", "python_full_version < '3.11' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'linux'", "python_full_version < '3.11' and sys_platform == 'linux'", ] supported-markers = [ @@ -14,6 +16,145 @@ supported-markers = [ "sys_platform == 'linux'", ] +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "aiosignal", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "async-timeout", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, + { name = "attrs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "frozenlist", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "multidict", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "propcache", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "yarl", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/d6/5aec9313ee6ea9c7cde8b891b69f4ff4001416867104580670a31daeba5b/aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7", size = 738950, upload-time = "2026-01-03T17:29:13.002Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/8fa90a7e6d11ff20a18837a8e2b5dd23db01aabc475aa9271c8ad33299f5/aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821", size = 496099, upload-time = "2026-01-03T17:29:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/d2/23/b81f744d402510a8366b74eb420fc0cc1170d0c43daca12d10814df85f10/aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845", size = 491072, upload-time = "2026-01-03T17:29:16.922Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e1/56d1d1c0dd334cd203dd97706ce004c1aa24b34a813b0b8daf3383039706/aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af", size = 1671588, upload-time = "2026-01-03T17:29:18.539Z" }, + { url = "https://files.pythonhosted.org/packages/5f/34/8d7f962604f4bc2b4e39eb1220dac7d4e4cba91fb9ba0474b4ecd67db165/aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940", size = 1640334, upload-time = "2026-01-03T17:29:21.028Z" }, + { url = "https://files.pythonhosted.org/packages/94/1d/fcccf2c668d87337ddeef9881537baee13c58d8f01f12ba8a24215f2b804/aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160", size = 1722656, upload-time = "2026-01-03T17:29:22.531Z" }, + { url = "https://files.pythonhosted.org/packages/aa/98/c6f3b081c4c606bc1e5f2ec102e87d6411c73a9ef3616fea6f2d5c98c062/aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7", size = 1817625, upload-time = "2026-01-03T17:29:24.276Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c0/cfcc3d2e11b477f86e1af2863f3858c8850d751ce8dc39c4058a072c9e54/aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455", size = 1672604, upload-time = "2026-01-03T17:29:26.099Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/6b4ffcbcac4c6a5d041343a756f34a6dd26174ae07f977a64fe028dda5b0/aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279", size = 1554370, upload-time = "2026-01-03T17:29:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f0/e3ddfa93f17d689dbe014ba048f18e0c9f9b456033b70e94349a2e9048be/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e", size = 1642023, upload-time = "2026-01-03T17:29:30.002Z" }, + { url = "https://files.pythonhosted.org/packages/eb/45/c14019c9ec60a8e243d06d601b33dcc4fd92379424bde3021725859d7f99/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d", size = 1649680, upload-time = "2026-01-03T17:29:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fd/09c9451dae5aa5c5ed756df95ff9ef549d45d4be663bafd1e4954fd836f0/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808", size = 1692407, upload-time = "2026-01-03T17:29:33.392Z" }, + { url = "https://files.pythonhosted.org/packages/a6/81/938bc2ec33c10efd6637ccb3d22f9f3160d08e8f3aa2587a2c2d5ab578eb/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40", size = 1543047, upload-time = "2026-01-03T17:29:34.855Z" }, + { url = "https://files.pythonhosted.org/packages/f7/23/80488ee21c8d567c83045e412e1d9b7077d27171591a4eb7822586e8c06a/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29", size = 1715264, upload-time = "2026-01-03T17:29:36.389Z" }, + { url = "https://files.pythonhosted.org/packages/e2/83/259a8da6683182768200b368120ab3deff5370bed93880fb9a3a86299f34/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11", size = 1657275, upload-time = "2026-01-03T17:29:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051, upload-time = "2026-01-03T17:29:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234, upload-time = "2026-01-03T17:29:44.822Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979, upload-time = "2026-01-03T17:29:46.405Z" }, + { url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297, upload-time = "2026-01-03T17:29:48.083Z" }, + { url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172, upload-time = "2026-01-03T17:29:49.648Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405, upload-time = "2026-01-03T17:29:51.244Z" }, + { url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449, upload-time = "2026-01-03T17:29:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444, upload-time = "2026-01-03T17:29:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038, upload-time = "2026-01-03T17:29:57.179Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156, upload-time = "2026-01-03T17:29:58.914Z" }, + { url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340, upload-time = "2026-01-03T17:30:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041, upload-time = "2026-01-03T17:30:03.609Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024, upload-time = "2026-01-03T17:30:05.132Z" }, + { url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590, upload-time = "2026-01-03T17:30:07.135Z" }, + { url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355, upload-time = "2026-01-03T17:30:09.083Z" }, + { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, + { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, + { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, + { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, + { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, + { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, + { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, + { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, + { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, + { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, + { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, + { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, + { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, + { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, + { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, + { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, + { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, + { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, + { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, + { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, + { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, + { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, + { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, + { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, + { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, + { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "aiosqlite" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/8a/64761f4005f17809769d23e518d915db74e6310474e733e3593cfc854ef1/aiosqlite-0.22.1.tar.gz", hash = "sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650", size = 14821, upload-time = "2025-12-23T19:25:43.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/b7/e3bf5133d697a08128598c8d0abc5e16377b51465a33756de24fa7dee953/aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb", size = 17405, upload-time = "2025-12-23T19:25:42.139Z" }, +] + [[package]] name = "amqp" version = "5.3.1" @@ -26,31 +167,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944, upload-time = "2024-11-12T19:55:41.782Z" }, ] +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + [[package]] name = "anyio" -version = "4.11.0" +version = "4.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, { name = "idna", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "sniffio", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform == 'linux')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, ] [[package]] name = "asgiref" -version = "3.9.2" +version = "3.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7f/bf/0f3ecda32f1cb3bf1dca480aca08a7a8a3bdc4bed2343a103f30731565c9/asgiref-3.9.2.tar.gz", hash = "sha256:a0249afacb66688ef258ffe503528360443e2b9a8d8c4581b6ebefa58c841ef1", size = 36894, upload-time = "2025-09-23T15:00:55.136Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/b9/4db2509eabd14b4a8c71d1b24c8d5734c52b8560a7b1e1a8b56c8d25568b/asgiref-3.11.0.tar.gz", hash = "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4", size = 37969, upload-time = "2025-11-19T15:32:20.106Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/d1/69d02ce34caddb0a7ae088b84c356a625a93cd4ff57b2f97644c03fad905/asgiref-3.9.2-py3-none-any.whl", hash = "sha256:0b61526596219d70396548fc003635056856dba5d0d086f86476f10b33c75960", size = 23788, upload-time = "2025-09-23T15:00:53.627Z" }, + { url = "https://files.pythonhosted.org/packages/91/be/317c2c55b8bbec407257d45f5c8d1b6867abc76d12043f2d3d58c538a4ea/asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d", size = 24096, upload-time = "2025-11-19T15:32:19.004Z" }, ] [[package]] @@ -64,28 +213,70 @@ wheels = [ [[package]] name = "attrs" -version = "25.3.0" +version = "25.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] [[package]] name = "autobahn" version = "24.4.2" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform == 'linux'", +] dependencies = [ - { name = "cryptography", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "hyperlink", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "setuptools", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "txaio", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "cryptography", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, + { name = "hyperlink", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, + { name = "setuptools", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, + { name = "txaio", version = "25.9.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/38/f2/8dffb3b709383ba5b47628b0cc4e43e8d12d59eecbddb62cfccac2e7cf6a/autobahn-24.4.2.tar.gz", hash = "sha256:a2d71ef1b0cf780b6d11f8b205fd2c7749765e65795f2ea7d823796642ee92c9", size = 482700, upload-time = "2024-08-02T09:26:48.241Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/13/ee/a6475f39ef6c6f41c33da6b193e0ffd2c6048f52e1698be6253c59301b72/autobahn-24.4.2-py2.py3-none-any.whl", hash = "sha256:c56a2abe7ac78abbfb778c02892d673a4de58fd004d088cd7ab297db25918e81", size = 666965, upload-time = "2024-08-02T09:26:44.274Z" }, ] +[[package]] +name = "autobahn" +version = "25.12.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'linux'", +] +dependencies = [ + { name = "cbor2", marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux')" }, + { name = "cffi", marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux')" }, + { name = "cryptography", marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux')" }, + { name = "hyperlink", marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux')" }, + { name = "msgpack", marker = "(python_full_version >= '3.11' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (python_full_version >= '3.11' and platform_python_implementation == 'CPython' and sys_platform == 'linux')" }, + { name = "py-ubjson", marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux')" }, + { name = "txaio", version = "25.12.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux')" }, + { name = "u-msgpack-python", marker = "(python_full_version >= '3.11' and platform_python_implementation != 'CPython' and sys_platform == 'darwin') or (python_full_version >= '3.11' and platform_python_implementation != 'CPython' and sys_platform == 'linux')" }, + { name = "ujson", marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/d5/9adf0f5b9eb244e58e898e9f3db4b00c09835ef4b6c37d491886e0376b4f/autobahn-25.12.2.tar.gz", hash = "sha256:754c06a54753aeb7e8d10c5cbf03249ad9e2a1a32bca8be02865c6f00628a98c", size = 13893652, upload-time = "2025-12-15T11:13:19.086Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/23/923e4f11dc9d12b9f5a014f36d591c479d623d54dda3bdcbd688cd12f052/autobahn-25.12.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16df879672c60f1f3fe452138c80f0fd221b3cb2ee5a14390c80f33b994104c1", size = 2053413, upload-time = "2025-12-15T11:12:58.167Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0d/3d39637a1e32f555ce5fabec4a723a035556ef918b14140faea05e7de902/autobahn-25.12.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6ffe28048ef96eb0f925f24c2569bd72332e120f4cb31cd6c40dd66718a5f85e", size = 2224850, upload-time = "2025-12-15T11:13:00.089Z" }, + { url = "https://files.pythonhosted.org/packages/64/8d/36452c06cbcad6d04587aeb87dfa987ef94be4a427b9f2155783d166bd97/autobahn-25.12.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:220748f21e91bd4a538d2d3de640cc17ee30b79f1c04a6c3dcdef321d531ee1c", size = 2225453, upload-time = "2025-12-15T11:13:02.865Z" }, + { url = "https://files.pythonhosted.org/packages/83/30/ef9c47038e4e9257319d6e1b87668b3df360a0c488d66ccff9d11aaff6ba/autobahn-25.12.2-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:bc17f6cab9438156d2701c293c76fd02a144f9be0a992c065dfee1935ce4845b", size = 1960447, upload-time = "2025-12-15T11:13:05.007Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e4/f3d5cb70bc0b9b5523d940734b2e0a251510d051a50d2e723f321e890859/autobahn-25.12.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5297a782fc7d0a26842438ef1342549ceee29496cda52672ac44635c79eeb94", size = 2053955, upload-time = "2025-12-15T11:13:06.052Z" }, + { url = "https://files.pythonhosted.org/packages/ea/49/4e592a19ae58fd9c796821a882b22598fac295ede50f899cc9d14a0282b6/autobahn-25.12.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0c3f1d5dafda52f8dc962ab583b6f3473b7b7186cab082d05372ed43a8261a5", size = 2225441, upload-time = "2025-12-15T11:13:07.527Z" }, + { url = "https://files.pythonhosted.org/packages/54/b7/0a0e3ecb2af7e452f5f359d19bdc647cbc8658f3f498bfa3bf8545cf4768/autobahn-25.12.2-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c840ee136bfaf6560467160129b0b25a0e33c9a51e2b251e98c5474f27583915", size = 1960463, upload-time = "2025-12-15T11:13:10.183Z" }, + { url = "https://files.pythonhosted.org/packages/19/8b/4215ac49d6b793b592fb08698f3a0e21a59eb3520be7f7ed288fcb52d919/autobahn-25.12.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9abda5cf817c0f8a19a55a67a031adf2fc70ed351719b5bd9e6fa0f5f4bc8f89", size = 2225590, upload-time = "2025-12-15T11:13:11.367Z" }, + { url = "https://files.pythonhosted.org/packages/d6/99/b4a3da42471d3ec36e2dca0c1a5368a079fed9f73b159ce3f049c4a4983b/autobahn-25.12.2-pp311-pypy311_pp73-macosx_15_0_arm64.whl", hash = "sha256:0c226329ddec154c6f3b491ea3e4713035f0326c96ebfd6b305bf90f27a2fba1", size = 1955357, upload-time = "2025-12-15T11:13:13.581Z" }, + { url = "https://files.pythonhosted.org/packages/89/81/67f19dd7395a9f1123a1f071314f8d1c4879c1869adeb8d99a236e756ac0/autobahn-25.12.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f079393a7626eb448c8accf21151f5f206d02f8e9cee4313d62a5ca30a3aaed", size = 623173, upload-time = "2025-12-15T11:13:14.945Z" }, + { url = "https://files.pythonhosted.org/packages/71/eb/857eab3d25e3b9cc9e7e741d6193808ad91de0befb38cf10658bd339c205/autobahn-25.12.2-pp311-pypy311_pp73-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b3a6c7d54a9f0434a435d88b86555510e5d0a84aa87042e292f29f707cab237", size = 2178008, upload-time = "2025-12-15T11:13:15.881Z" }, +] + [[package]] name = "automat" version = "25.4.16" @@ -104,23 +295,22 @@ dependencies = [ { name = "isodate", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/44/7b/8115cd713e2caa5e44def85f2b7ebd02a74ae74d7113ba20bdd41fd6dd80/azure_ai_documentintelligence-1.0.2.tar.gz", hash = "sha256:4d75a2513f2839365ebabc0e0e1772f5601b3a8c9a71e75da12440da13b63484", size = 170940 } +sdist = { url = "https://files.pythonhosted.org/packages/44/7b/8115cd713e2caa5e44def85f2b7ebd02a74ae74d7113ba20bdd41fd6dd80/azure_ai_documentintelligence-1.0.2.tar.gz", hash = "sha256:4d75a2513f2839365ebabc0e0e1772f5601b3a8c9a71e75da12440da13b63484", size = 170940, upload-time = "2025-03-27T02:46:20.606Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/75/c9ec040f23082f54ffb1977ff8f364c2d21c79a640a13d1c1809e7fd6b1a/azure_ai_documentintelligence-1.0.2-py3-none-any.whl", hash = "sha256:e1fb446abbdeccc9759d897898a0fe13141ed29f9ad11fc705f951925822ed59", size = 106005 }, + { url = "https://files.pythonhosted.org/packages/d9/75/c9ec040f23082f54ffb1977ff8f364c2d21c79a640a13d1c1809e7fd6b1a/azure_ai_documentintelligence-1.0.2-py3-none-any.whl", hash = "sha256:e1fb446abbdeccc9759d897898a0fe13141ed29f9ad11fc705f951925822ed59", size = 106005, upload-time = "2025-03-27T02:46:22.356Z" }, ] [[package]] name = "azure-core" -version = "1.33.0" +version = "1.38.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "six", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/75/aa/7c9db8edd626f1a7d99d09ef7926f6f4fb34d5f9fa00dc394afdfe8e2a80/azure_core-1.33.0.tar.gz", hash = "sha256:f367aa07b5e3005fec2c1e184b882b0b039910733907d001c20fb08ebb8c0eb9", size = 295633 } +sdist = { url = "https://files.pythonhosted.org/packages/dc/1b/e503e08e755ea94e7d3419c9242315f888fc664211c90d032e40479022bf/azure_core-1.38.0.tar.gz", hash = "sha256:8194d2682245a3e4e3151a667c686464c3786fed7918b394d035bdcd61bb5993", size = 363033, upload-time = "2026-01-12T17:03:05.535Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/b7/76b7e144aa53bd206bf1ce34fa75350472c3f69bf30e5c8c18bc9881035d/azure_core-1.33.0-py3-none-any.whl", hash = "sha256:9b5b6d0223a1d38c37500e6971118c1e0f13f54951e6893968b38910bc9cda8f", size = 207071 }, + { url = "https://files.pythonhosted.org/packages/fc/d8/b8fcba9464f02b121f39de2db2bf57f0b216fe11d014513d666e8634380d/azure_core-1.38.0-py3-none-any.whl", hash = "sha256:ab0c9b2cd71fecb1842d52c965c95285d3cfb38902f6766e4a471f1cd8905335", size = 217825, upload-time = "2026-01-12T17:03:07.291Z" }, ] [[package]] @@ -134,25 +324,41 @@ wheels = [ [[package]] name = "backrefs" -version = "5.9" +version = "6.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/eb/a7/312f673df6a79003279e1f55619abbe7daebbb87c17c976ddc0345c04c7b/backrefs-5.9.tar.gz", hash = "sha256:808548cb708d66b82ee231f962cb36faaf4f2baab032f2fbb783e9c2fdddaa59", size = 5765857, upload-time = "2025-06-22T19:34:13.97Z" } +sdist = { url = "https://files.pythonhosted.org/packages/86/e3/bb3a439d5cb255c4774724810ad8073830fac9c9dee123555820c1bcc806/backrefs-6.1.tar.gz", hash = "sha256:3bba1749aafe1db9b915f00e0dd166cba613b6f788ffd63060ac3485dc9be231", size = 7011962, upload-time = "2025-11-15T14:52:08.323Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/4d/798dc1f30468134906575156c089c492cf79b5a5fd373f07fe26c4d046bf/backrefs-5.9-py310-none-any.whl", hash = "sha256:db8e8ba0e9de81fcd635f440deab5ae5f2591b54ac1ebe0550a2ca063488cd9f", size = 380267, upload-time = "2025-06-22T19:34:05.252Z" }, - { url = "https://files.pythonhosted.org/packages/55/07/f0b3375bf0d06014e9787797e6b7cc02b38ac9ff9726ccfe834d94e9991e/backrefs-5.9-py311-none-any.whl", hash = "sha256:6907635edebbe9b2dc3de3a2befff44d74f30a4562adbb8b36f21252ea19c5cf", size = 392072, upload-time = "2025-06-22T19:34:06.743Z" }, - { url = "https://files.pythonhosted.org/packages/9d/12/4f345407259dd60a0997107758ba3f221cf89a9b5a0f8ed5b961aef97253/backrefs-5.9-py312-none-any.whl", hash = "sha256:7fdf9771f63e6028d7fee7e0c497c81abda597ea45d6b8f89e8ad76994f5befa", size = 397947, upload-time = "2025-06-22T19:34:08.172Z" }, - { url = "https://files.pythonhosted.org/packages/10/bf/fa31834dc27a7f05e5290eae47c82690edc3a7b37d58f7fb35a1bdbf355b/backrefs-5.9-py313-none-any.whl", hash = "sha256:cc37b19fa219e93ff825ed1fed8879e47b4d89aa7a1884860e2db64ccd7c676b", size = 399843, upload-time = "2025-06-22T19:34:09.68Z" }, - { url = "https://files.pythonhosted.org/packages/fc/24/b29af34b2c9c41645a9f4ff117bae860291780d73880f449e0b5d948c070/backrefs-5.9-py314-none-any.whl", hash = "sha256:df5e169836cc8acb5e440ebae9aad4bf9d15e226d3bad049cf3f6a5c20cc8dc9", size = 411762, upload-time = "2025-06-22T19:34:11.037Z" }, - { url = "https://files.pythonhosted.org/packages/41/ff/392bff89415399a979be4a65357a41d92729ae8580a66073d8ec8d810f98/backrefs-5.9-py39-none-any.whl", hash = "sha256:f48ee18f6252b8f5777a22a00a09a85de0ca931658f1dd96d4406a34f3748c60", size = 380265, upload-time = "2025-06-22T19:34:12.405Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ee/c216d52f58ea75b5e1841022bbae24438b19834a29b163cb32aa3a2a7c6e/backrefs-6.1-py310-none-any.whl", hash = "sha256:2a2ccb96302337ce61ee4717ceacfbf26ba4efb1d55af86564b8bbaeda39cac1", size = 381059, upload-time = "2025-11-15T14:51:59.758Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9a/8da246d988ded941da96c7ed945d63e94a445637eaad985a0ed88787cb89/backrefs-6.1-py311-none-any.whl", hash = "sha256:e82bba3875ee4430f4de4b6db19429a27275d95a5f3773c57e9e18abc23fd2b7", size = 392854, upload-time = "2025-11-15T14:52:01.194Z" }, + { url = "https://files.pythonhosted.org/packages/37/c9/fd117a6f9300c62bbc33bc337fd2b3c6bfe28b6e9701de336b52d7a797ad/backrefs-6.1-py312-none-any.whl", hash = "sha256:c64698c8d2269343d88947c0735cb4b78745bd3ba590e10313fbf3f78c34da5a", size = 398770, upload-time = "2025-11-15T14:52:02.584Z" }, + { url = "https://files.pythonhosted.org/packages/eb/95/7118e935b0b0bd3f94dfec2d852fd4e4f4f9757bdb49850519acd245cd3a/backrefs-6.1-py313-none-any.whl", hash = "sha256:4c9d3dc1e2e558965202c012304f33d4e0e477e1c103663fd2c3cc9bb18b0d05", size = 400726, upload-time = "2025-11-15T14:52:04.093Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/6296bad135bfafd3254ae3648cd152980a424bd6fed64a101af00cc7ba31/backrefs-6.1-py314-none-any.whl", hash = "sha256:13eafbc9ccd5222e9c1f0bec563e6d2a6d21514962f11e7fc79872fd56cbc853", size = 412584, upload-time = "2025-11-15T14:52:05.233Z" }, + { url = "https://files.pythonhosted.org/packages/02/e3/a4fa1946722c4c7b063cc25043a12d9ce9b4323777f89643be74cef2993c/backrefs-6.1-py39-none-any.whl", hash = "sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0", size = 381058, upload-time = "2025-11-15T14:52:06.698Z" }, +] + +[[package]] +name = "banks" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "griffe", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "jinja2", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "platformdirs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/64/9a4e17dfe7dc172594ffb877a287859edb40d59e0564bc930941e6c5df9d/banks-2.3.0.tar.gz", hash = "sha256:1ecb439a0b340588fcf9a8072d806540aad03c4b874ab9aff59ac8bc08c112ff", size = 182736, upload-time = "2026-01-21T10:03:15.114Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/d6/ccceb03dd5193d180e28411c9f880f2cc9a574251de94b9b8a21ebdf51ec/banks-2.3.0-py3-none-any.whl", hash = "sha256:ac6a5800d468f26a0d80e091c0c6971b69457d580ce34c0217ee2bf6c3f07271", size = 32748, upload-time = "2026-01-21T10:03:14.251Z" }, ] [[package]] name = "billiard" -version = "4.2.2" +version = "4.2.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b9/6a/1405343016bce8354b29d90aad6b0bf6485b5e60404516e4b9a3a9646cf0/billiard-4.2.2.tar.gz", hash = "sha256:e815017a062b714958463e07ba15981d802dc53d41c5b69d28c5a7c238f8ecf3", size = 155592, upload-time = "2025-09-20T14:44:40.456Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/23/b12ac0bcdfb7360d664f40a00b1bda139cbbbced012c34e375506dbd0143/billiard-4.2.4.tar.gz", hash = "sha256:55f542c371209e03cd5862299b74e52e4fbcba8250ba611ad94276b369b6a85f", size = 156537, upload-time = "2025-11-30T13:28:48.52Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/80/ef8dff49aae0e4430f81842f7403e14e0ca59db7bbaf7af41245b67c6b25/billiard-4.2.2-py3-none-any.whl", hash = "sha256:4bc05dcf0d1cc6addef470723aac2a6232f3c7ed7475b0b580473a9145829457", size = 86896, upload-time = "2025-09-20T14:44:39.157Z" }, + { url = "https://files.pythonhosted.org/packages/cb/87/8bab77b323f16d67be364031220069f79159117dd5e43eeb4be2fef1ac9b/billiard-4.2.4-py3-none-any.whl", hash = "sha256:525b42bdec68d2b983347ac312f892db930858495db601b5836ac24e6477cde5", size = 87070, upload-time = "2025-11-30T13:28:47.016Z" }, ] [[package]] @@ -169,64 +375,50 @@ wheels = [ [[package]] name = "brotli" -version = "1.1.0" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/c2/f9e977608bdf958650638c3f1e28f85a1b075f075ebbe77db8555463787b/Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724", size = 7372270, upload-time = "2023-09-07T14:05:41.643Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/16/c92ca344d646e71a43b8bb353f0a6490d7f6e06210f8554c8f874e454285/brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", size = 7388632, upload-time = "2025-11-05T18:39:42.86Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/3a/dbf4fb970c1019a57b5e492e1e0eae745d32e59ba4d6161ab5422b08eefe/Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752", size = 873045, upload-time = "2023-09-07T14:03:16.894Z" }, - { url = "https://files.pythonhosted.org/packages/dd/11/afc14026ea7f44bd6eb9316d800d439d092c8d508752055ce8d03086079a/Brotli-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9", size = 446218, upload-time = "2023-09-07T14:03:18.917Z" }, - { url = "https://files.pythonhosted.org/packages/36/83/7545a6e7729db43cb36c4287ae388d6885c85a86dd251768a47015dfde32/Brotli-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3", size = 2903872, upload-time = "2023-09-07T14:03:20.398Z" }, - { url = "https://files.pythonhosted.org/packages/32/23/35331c4d9391fcc0f29fd9bec2c76e4b4eeab769afbc4b11dd2e1098fb13/Brotli-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d", size = 2941254, upload-time = "2023-09-07T14:03:21.914Z" }, - { url = "https://files.pythonhosted.org/packages/3b/24/1671acb450c902edb64bd765d73603797c6c7280a9ada85a195f6b78c6e5/Brotli-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e", size = 2857293, upload-time = "2023-09-07T14:03:24Z" }, - { url = "https://files.pythonhosted.org/packages/d5/00/40f760cc27007912b327fe15bf6bfd8eaecbe451687f72a8abc587d503b3/Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da", size = 3002385, upload-time = "2023-09-07T14:03:26.248Z" }, - { url = "https://files.pythonhosted.org/packages/b8/cb/8aaa83f7a4caa131757668c0fb0c4b6384b09ffa77f2fba9570d87ab587d/Brotli-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80", size = 2911104, upload-time = "2023-09-07T14:03:27.849Z" }, - { url = "https://files.pythonhosted.org/packages/bc/c4/65456561d89d3c49f46b7fbeb8fe6e449f13bdc8ea7791832c5d476b2faf/Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d", size = 2809981, upload-time = "2023-09-07T14:03:29.92Z" }, - { url = "https://files.pythonhosted.org/packages/05/1b/cf49528437bae28abce5f6e059f0d0be6fecdcc1d3e33e7c54b3ca498425/Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0", size = 2935297, upload-time = "2023-09-07T14:03:32.035Z" }, - { url = "https://files.pythonhosted.org/packages/81/ff/190d4af610680bf0c5a09eb5d1eac6e99c7c8e216440f9c7cfd42b7adab5/Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e", size = 2930735, upload-time = "2023-09-07T14:03:33.801Z" }, - { url = "https://files.pythonhosted.org/packages/80/7d/f1abbc0c98f6e09abd3cad63ec34af17abc4c44f308a7a539010f79aae7a/Brotli-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c", size = 2933107, upload-time = "2024-10-18T12:32:09.016Z" }, - { url = "https://files.pythonhosted.org/packages/34/ce/5a5020ba48f2b5a4ad1c0522d095ad5847a0be508e7d7569c8630ce25062/Brotli-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1", size = 2845400, upload-time = "2024-10-18T12:32:11.134Z" }, - { url = "https://files.pythonhosted.org/packages/44/89/fa2c4355ab1eecf3994e5a0a7f5492c6ff81dfcb5f9ba7859bd534bb5c1a/Brotli-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2", size = 3031985, upload-time = "2024-10-18T12:32:12.813Z" }, - { url = "https://files.pythonhosted.org/packages/af/a4/79196b4a1674143d19dca400866b1a4d1a089040df7b93b88ebae81f3447/Brotli-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec", size = 2927099, upload-time = "2024-10-18T12:32:14.733Z" }, - { url = "https://files.pythonhosted.org/packages/96/12/ad41e7fadd5db55459c4c401842b47f7fee51068f86dd2894dd0dcfc2d2a/Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc", size = 873068, upload-time = "2023-09-07T14:03:37.779Z" }, - { url = "https://files.pythonhosted.org/packages/95/4e/5afab7b2b4b61a84e9c75b17814198ce515343a44e2ed4488fac314cd0a9/Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6", size = 446244, upload-time = "2023-09-07T14:03:39.223Z" }, - { url = "https://files.pythonhosted.org/packages/9d/e6/f305eb61fb9a8580c525478a4a34c5ae1a9bcb12c3aee619114940bc513d/Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd", size = 2906500, upload-time = "2023-09-07T14:03:40.858Z" }, - { url = "https://files.pythonhosted.org/packages/3e/4f/af6846cfbc1550a3024e5d3775ede1e00474c40882c7bf5b37a43ca35e91/Brotli-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf", size = 2943950, upload-time = "2023-09-07T14:03:42.896Z" }, - { url = "https://files.pythonhosted.org/packages/b3/e7/ca2993c7682d8629b62630ebf0d1f3bb3d579e667ce8e7ca03a0a0576a2d/Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61", size = 2918527, upload-time = "2023-09-07T14:03:44.552Z" }, - { url = "https://files.pythonhosted.org/packages/b3/96/da98e7bedc4c51104d29cc61e5f449a502dd3dbc211944546a4cc65500d3/Brotli-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327", size = 2845489, upload-time = "2023-09-07T14:03:46.594Z" }, - { url = "https://files.pythonhosted.org/packages/e8/ef/ccbc16947d6ce943a7f57e1a40596c75859eeb6d279c6994eddd69615265/Brotli-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd", size = 2914080, upload-time = "2023-09-07T14:03:48.204Z" }, - { url = "https://files.pythonhosted.org/packages/80/d6/0bd38d758d1afa62a5524172f0b18626bb2392d717ff94806f741fcd5ee9/Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9", size = 2813051, upload-time = "2023-09-07T14:03:50.348Z" }, - { url = "https://files.pythonhosted.org/packages/14/56/48859dd5d129d7519e001f06dcfbb6e2cf6db92b2702c0c2ce7d97e086c1/Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265", size = 2938172, upload-time = "2023-09-07T14:03:52.395Z" }, - { url = "https://files.pythonhosted.org/packages/3d/77/a236d5f8cd9e9f4348da5acc75ab032ab1ab2c03cc8f430d24eea2672888/Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8", size = 2933023, upload-time = "2023-09-07T14:03:53.96Z" }, - { url = "https://files.pythonhosted.org/packages/f1/87/3b283efc0f5cb35f7f84c0c240b1e1a1003a5e47141a4881bf87c86d0ce2/Brotli-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f", size = 2935871, upload-time = "2024-10-18T12:32:16.688Z" }, - { url = "https://files.pythonhosted.org/packages/f3/eb/2be4cc3e2141dc1a43ad4ca1875a72088229de38c68e842746b342667b2a/Brotli-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757", size = 2847784, upload-time = "2024-10-18T12:32:18.459Z" }, - { url = "https://files.pythonhosted.org/packages/66/13/b58ddebfd35edde572ccefe6890cf7c493f0c319aad2a5badee134b4d8ec/Brotli-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0", size = 3034905, upload-time = "2024-10-18T12:32:20.192Z" }, - { url = "https://files.pythonhosted.org/packages/84/9c/bc96b6c7db824998a49ed3b38e441a2cae9234da6fa11f6ed17e8cf4f147/Brotli-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b", size = 2929467, upload-time = "2024-10-18T12:32:21.774Z" }, - { url = "https://files.pythonhosted.org/packages/5c/d0/5373ae13b93fe00095a58efcbce837fd470ca39f703a235d2a999baadfbc/Brotli-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28", size = 815693, upload-time = "2024-10-18T12:32:23.824Z" }, - { url = "https://files.pythonhosted.org/packages/8e/48/f6e1cdf86751300c288c1459724bfa6917a80e30dbfc326f92cea5d3683a/Brotli-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f", size = 422489, upload-time = "2024-10-18T12:32:25.641Z" }, - { url = "https://files.pythonhosted.org/packages/06/88/564958cedce636d0f1bed313381dfc4b4e3d3f6015a63dae6146e1b8c65c/Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409", size = 873081, upload-time = "2023-09-07T14:03:57.967Z" }, - { url = "https://files.pythonhosted.org/packages/58/79/b7026a8bb65da9a6bb7d14329fd2bd48d2b7f86d7329d5cc8ddc6a90526f/Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2", size = 446244, upload-time = "2023-09-07T14:03:59.319Z" }, - { url = "https://files.pythonhosted.org/packages/e5/18/c18c32ecea41b6c0004e15606e274006366fe19436b6adccc1ae7b2e50c2/Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451", size = 2906505, upload-time = "2023-09-07T14:04:01.327Z" }, - { url = "https://files.pythonhosted.org/packages/08/c8/69ec0496b1ada7569b62d85893d928e865df29b90736558d6c98c2031208/Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91", size = 2944152, upload-time = "2023-09-07T14:04:03.033Z" }, - { url = "https://files.pythonhosted.org/packages/ab/fb/0517cea182219d6768113a38167ef6d4eb157a033178cc938033a552ed6d/Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408", size = 2919252, upload-time = "2023-09-07T14:04:04.675Z" }, - { url = "https://files.pythonhosted.org/packages/c7/53/73a3431662e33ae61a5c80b1b9d2d18f58dfa910ae8dd696e57d39f1a2f5/Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0", size = 2845955, upload-time = "2023-09-07T14:04:06.585Z" }, - { url = "https://files.pythonhosted.org/packages/55/ac/bd280708d9c5ebdbf9de01459e625a3e3803cce0784f47d633562cf40e83/Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc", size = 2914304, upload-time = "2023-09-07T14:04:08.668Z" }, - { url = "https://files.pythonhosted.org/packages/76/58/5c391b41ecfc4527d2cc3350719b02e87cb424ef8ba2023fb662f9bf743c/Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180", size = 2814452, upload-time = "2023-09-07T14:04:10.736Z" }, - { url = "https://files.pythonhosted.org/packages/c7/4e/91b8256dfe99c407f174924b65a01f5305e303f486cc7a2e8a5d43c8bec3/Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248", size = 2938751, upload-time = "2023-09-07T14:04:12.875Z" }, - { url = "https://files.pythonhosted.org/packages/5a/a6/e2a39a5d3b412938362bbbeba5af904092bf3f95b867b4a3eb856104074e/Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966", size = 2933757, upload-time = "2023-09-07T14:04:14.551Z" }, - { url = "https://files.pythonhosted.org/packages/13/f0/358354786280a509482e0e77c1a5459e439766597d280f28cb097642fc26/Brotli-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9", size = 2936146, upload-time = "2024-10-18T12:32:27.257Z" }, - { url = "https://files.pythonhosted.org/packages/80/f7/daf538c1060d3a88266b80ecc1d1c98b79553b3f117a485653f17070ea2a/Brotli-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb", size = 2848055, upload-time = "2024-10-18T12:32:29.376Z" }, - { url = "https://files.pythonhosted.org/packages/ad/cf/0eaa0585c4077d3c2d1edf322d8e97aabf317941d3a72d7b3ad8bce004b0/Brotli-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111", size = 3035102, upload-time = "2024-10-18T12:32:31.371Z" }, - { url = "https://files.pythonhosted.org/packages/d8/63/1c1585b2aa554fe6dbce30f0c18bdbc877fa9a1bf5ff17677d9cca0ac122/Brotli-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839", size = 2930029, upload-time = "2024-10-18T12:32:33.293Z" }, - { url = "https://files.pythonhosted.org/packages/0a/9f/fb37bb8ffc52a8da37b1c03c459a8cd55df7a57bdccd8831d500e994a0ca/Brotli-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5", size = 815681, upload-time = "2024-10-18T12:32:34.942Z" }, - { url = "https://files.pythonhosted.org/packages/06/b3/dbd332a988586fefb0aa49c779f59f47cae76855c2d00f450364bb574cac/Brotli-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8", size = 422475, upload-time = "2024-10-18T12:32:36.485Z" }, - { url = "https://files.pythonhosted.org/packages/bb/80/6aaddc2f63dbcf2d93c2d204e49c11a9ec93a8c7c63261e2b4bd35198283/Brotli-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f", size = 2906173, upload-time = "2024-10-18T12:32:37.978Z" }, - { url = "https://files.pythonhosted.org/packages/ea/1d/e6ca79c96ff5b641df6097d299347507d39a9604bde8915e76bf026d6c77/Brotli-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648", size = 2943803, upload-time = "2024-10-18T12:32:39.606Z" }, - { url = "https://files.pythonhosted.org/packages/ac/a3/d98d2472e0130b7dd3acdbb7f390d478123dbf62b7d32bda5c830a96116d/Brotli-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0", size = 2918946, upload-time = "2024-10-18T12:32:41.679Z" }, - { url = "https://files.pythonhosted.org/packages/c4/a5/c69e6d272aee3e1423ed005d8915a7eaa0384c7de503da987f2d224d0721/Brotli-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089", size = 2845707, upload-time = "2024-10-18T12:32:43.478Z" }, - { url = "https://files.pythonhosted.org/packages/58/9f/4149d38b52725afa39067350696c09526de0125ebfbaab5acc5af28b42ea/Brotli-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368", size = 2936231, upload-time = "2024-10-18T12:32:45.224Z" }, - { url = "https://files.pythonhosted.org/packages/5a/5a/145de884285611838a16bebfdb060c231c52b8f84dfbe52b852a15780386/Brotli-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c", size = 2848157, upload-time = "2024-10-18T12:32:46.894Z" }, - { url = "https://files.pythonhosted.org/packages/50/ae/408b6bfb8525dadebd3b3dd5b19d631da4f7d46420321db44cd99dcf2f2c/Brotli-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284", size = 3035122, upload-time = "2024-10-18T12:32:48.844Z" }, - { url = "https://files.pythonhosted.org/packages/af/85/a94e5cfaa0ca449d8f91c3d6f78313ebf919a0dbd55a100c711c6e9655bc/Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7", size = 2930206, upload-time = "2024-10-18T12:32:51.198Z" }, + { url = "https://files.pythonhosted.org/packages/64/10/a090475284fc4a71aed40a96f32e44a7fe5bda39687353dd977720b211b6/brotli-1.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3b90b767916ac44e93a8e28ce6adf8d551e43affb512f2377c732d486ac6514e", size = 863089, upload-time = "2025-11-05T18:38:01.181Z" }, + { url = "https://files.pythonhosted.org/packages/03/41/17416630e46c07ac21e378c3464815dd2e120b441e641bc516ac32cc51d2/brotli-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6be67c19e0b0c56365c6a76e393b932fb0e78b3b56b711d180dd7013cb1fd984", size = 445442, upload-time = "2025-11-05T18:38:02.434Z" }, + { url = "https://files.pythonhosted.org/packages/24/31/90cc06584deb5d4fcafc0985e37741fc6b9717926a78674bbb3ce018957e/brotli-1.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0bbd5b5ccd157ae7913750476d48099aaf507a79841c0d04a9db4415b14842de", size = 1532658, upload-time = "2025-11-05T18:38:03.588Z" }, + { url = "https://files.pythonhosted.org/packages/62/17/33bf0c83bcbc96756dfd712201d87342732fad70bb3472c27e833a44a4f9/brotli-1.2.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3f3c908bcc404c90c77d5a073e55271a0a498f4e0756e48127c35d91cf155947", size = 1631241, upload-time = "2025-11-05T18:38:04.582Z" }, + { url = "https://files.pythonhosted.org/packages/48/10/f47854a1917b62efe29bc98ac18e5d4f71df03f629184575b862ef2e743b/brotli-1.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1b557b29782a643420e08d75aea889462a4a8796e9a6cf5621ab05a3f7da8ef2", size = 1424307, upload-time = "2025-11-05T18:38:05.587Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b7/f88eb461719259c17483484ea8456925ee057897f8e64487d76e24e5e38d/brotli-1.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81da1b229b1889f25adadc929aeb9dbc4e922bd18561b65b08dd9343cfccca84", size = 1488208, upload-time = "2025-11-05T18:38:06.613Z" }, + { url = "https://files.pythonhosted.org/packages/26/59/41bbcb983a0c48b0b8004203e74706c6b6e99a04f3c7ca6f4f41f364db50/brotli-1.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ff09cd8c5eec3b9d02d2408db41be150d8891c5566addce57513bf546e3d6c6d", size = 1597574, upload-time = "2025-11-05T18:38:07.838Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e6/8c89c3bdabbe802febb4c5c6ca224a395e97913b5df0dff11b54f23c1788/brotli-1.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a1778532b978d2536e79c05dac2d8cd857f6c55cd0c95ace5b03740824e0e2f1", size = 1492109, upload-time = "2025-11-05T18:38:08.816Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ef/f285668811a9e1ddb47a18cb0b437d5fc2760d537a2fe8a57875ad6f8448/brotli-1.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:15b33fe93cedc4caaff8a0bd1eb7e3dab1c61bb22a0bf5bdfdfd97cd7da79744", size = 863110, upload-time = "2025-11-05T18:38:12.978Z" }, + { url = "https://files.pythonhosted.org/packages/50/62/a3b77593587010c789a9d6eaa527c79e0848b7b860402cc64bc0bc28a86c/brotli-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:898be2be399c221d2671d29eed26b6b2713a02c2119168ed914e7d00ceadb56f", size = 445438, upload-time = "2025-11-05T18:38:14.208Z" }, + { url = "https://files.pythonhosted.org/packages/cd/e1/7fadd47f40ce5549dc44493877db40292277db373da5053aff181656e16e/brotli-1.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:350c8348f0e76fff0a0fd6c26755d2653863279d086d3aa2c290a6a7251135dd", size = 1534420, upload-time = "2025-11-05T18:38:15.111Z" }, + { url = "https://files.pythonhosted.org/packages/12/8b/1ed2f64054a5a008a4ccd2f271dbba7a5fb1a3067a99f5ceadedd4c1d5a7/brotli-1.2.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1ad3fda65ae0d93fec742a128d72e145c9c7a99ee2fcd667785d99eb25a7fe", size = 1632619, upload-time = "2025-11-05T18:38:16.094Z" }, + { url = "https://files.pythonhosted.org/packages/89/5a/7071a621eb2d052d64efd5da2ef55ecdac7c3b0c6e4f9d519e9c66d987ef/brotli-1.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:40d918bce2b427a0c4ba189df7a006ac0c7277c180aee4617d99e9ccaaf59e6a", size = 1426014, upload-time = "2025-11-05T18:38:17.177Z" }, + { url = "https://files.pythonhosted.org/packages/26/6d/0971a8ea435af5156acaaccec1a505f981c9c80227633851f2810abd252a/brotli-1.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2a7f1d03727130fc875448b65b127a9ec5d06d19d0148e7554384229706f9d1b", size = 1489661, upload-time = "2025-11-05T18:38:18.41Z" }, + { url = "https://files.pythonhosted.org/packages/f3/75/c1baca8b4ec6c96a03ef8230fab2a785e35297632f402ebb1e78a1e39116/brotli-1.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9c79f57faa25d97900bfb119480806d783fba83cd09ee0b33c17623935b05fa3", size = 1599150, upload-time = "2025-11-05T18:38:19.792Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1a/23fcfee1c324fd48a63d7ebf4bac3a4115bdb1b00e600f80f727d850b1ae/brotli-1.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:844a8ceb8483fefafc412f85c14f2aae2fb69567bf2a0de53cdb88b73e7c43ae", size = 1493505, upload-time = "2025-11-05T18:38:20.913Z" }, + { url = "https://files.pythonhosted.org/packages/11/ee/b0a11ab2315c69bb9b45a2aaed022499c9c24a205c3a49c3513b541a7967/brotli-1.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:35d382625778834a7f3061b15423919aa03e4f5da34ac8e02c074e4b75ab4f84", size = 861543, upload-time = "2025-11-05T18:38:24.183Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2f/29c1459513cd35828e25531ebfcbf3e92a5e49f560b1777a9af7203eb46e/brotli-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a61c06b334bd99bc5ae84f1eeb36bfe01400264b3c352f968c6e30a10f9d08b", size = 444288, upload-time = "2025-11-05T18:38:25.139Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6f/feba03130d5fceadfa3a1bb102cb14650798c848b1df2a808356f939bb16/brotli-1.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acec55bb7c90f1dfc476126f9711a8e81c9af7fb617409a9ee2953115343f08d", size = 1528071, upload-time = "2025-11-05T18:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/2b/38/f3abb554eee089bd15471057ba85f47e53a44a462cfce265d9bf7088eb09/brotli-1.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:260d3692396e1895c5034f204f0db022c056f9e2ac841593a4cf9426e2a3faca", size = 1626913, upload-time = "2025-11-05T18:38:27.284Z" }, + { url = "https://files.pythonhosted.org/packages/03/a7/03aa61fbc3c5cbf99b44d158665f9b0dd3d8059be16c460208d9e385c837/brotli-1.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:072e7624b1fc4d601036ab3f4f27942ef772887e876beff0301d261210bca97f", size = 1419762, upload-time = "2025-11-05T18:38:28.295Z" }, + { url = "https://files.pythonhosted.org/packages/21/1b/0374a89ee27d152a5069c356c96b93afd1b94eae83f1e004b57eb6ce2f10/brotli-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adedc4a67e15327dfdd04884873c6d5a01d3e3b6f61406f99b1ed4865a2f6d28", size = 1484494, upload-time = "2025-11-05T18:38:29.29Z" }, + { url = "https://files.pythonhosted.org/packages/cf/57/69d4fe84a67aef4f524dcd075c6eee868d7850e85bf01d778a857d8dbe0a/brotli-1.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7a47ce5c2288702e09dc22a44d0ee6152f2c7eda97b3c8482d826a1f3cfc7da7", size = 1593302, upload-time = "2025-11-05T18:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/d5/3b/39e13ce78a8e9a621c5df3aeb5fd181fcc8caba8c48a194cd629771f6828/brotli-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:af43b8711a8264bb4e7d6d9a6d004c3a2019c04c01127a868709ec29962b6036", size = 1487913, upload-time = "2025-11-05T18:38:31.618Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d4/4ad5432ac98c73096159d9ce7ffeb82d151c2ac84adcc6168e476bb54674/brotli-1.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9e5825ba2c9998375530504578fd4d5d1059d09621a02065d1b6bfc41a8e05ab", size = 861523, upload-time = "2025-11-05T18:38:34.67Z" }, + { url = "https://files.pythonhosted.org/packages/91/9f/9cc5bd03ee68a85dc4bc89114f7067c056a3c14b3d95f171918c088bf88d/brotli-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0cf8c3b8ba93d496b2fae778039e2f5ecc7cff99df84df337ca31d8f2252896c", size = 444289, upload-time = "2025-11-05T18:38:35.6Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b6/fe84227c56a865d16a6614e2c4722864b380cb14b13f3e6bef441e73a85a/brotli-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8565e3cdc1808b1a34714b553b262c5de5fbda202285782173ec137fd13709f", size = 1528076, upload-time = "2025-11-05T18:38:36.639Z" }, + { url = "https://files.pythonhosted.org/packages/55/de/de4ae0aaca06c790371cf6e7ee93a024f6b4bb0568727da8c3de112e726c/brotli-1.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:26e8d3ecb0ee458a9804f47f21b74845cc823fd1bb19f02272be70774f56e2a6", size = 1626880, upload-time = "2025-11-05T18:38:37.623Z" }, + { url = "https://files.pythonhosted.org/packages/5f/16/a1b22cbea436642e071adcaf8d4b350a2ad02f5e0ad0da879a1be16188a0/brotli-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67a91c5187e1eec76a61625c77a6c8c785650f5b576ca732bd33ef58b0dff49c", size = 1419737, upload-time = "2025-11-05T18:38:38.729Z" }, + { url = "https://files.pythonhosted.org/packages/46/63/c968a97cbb3bdbf7f974ef5a6ab467a2879b82afbc5ffb65b8acbb744f95/brotli-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ecdb3b6dc36e6d6e14d3a1bdc6c1057c8cbf80db04031d566eb6080ce283a48", size = 1484440, upload-time = "2025-11-05T18:38:39.916Z" }, + { url = "https://files.pythonhosted.org/packages/06/9d/102c67ea5c9fc171f423e8399e585dabea29b5bc79b05572891e70013cdd/brotli-1.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3e1b35d56856f3ed326b140d3c6d9db91740f22e14b06e840fe4bb1923439a18", size = 1593313, upload-time = "2025-11-05T18:38:41.24Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4a/9526d14fa6b87bc827ba1755a8440e214ff90de03095cacd78a64abe2b7d/brotli-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:54a50a9dad16b32136b2241ddea9e4df159b41247b2ce6aac0b3276a66a8f1e5", size = 1487945, upload-time = "2025-11-05T18:38:42.277Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/298c2ddf786bb7347a1cd71d63a347a79e5712a7c0cba9e3c3458ebd976f/brotli-1.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6c12dad5cd04530323e723787ff762bac749a7b256a5bece32b2243dd5c27b21", size = 863080, upload-time = "2025-11-05T18:38:45.503Z" }, + { url = "https://files.pythonhosted.org/packages/84/0c/aac98e286ba66868b2b3b50338ffbd85a35c7122e9531a73a37a29763d38/brotli-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3219bd9e69868e57183316ee19c84e03e8f8b5a1d1f2667e1aa8c2f91cb061ac", size = 445453, upload-time = "2025-11-05T18:38:46.433Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f1/0ca1f3f99ae300372635ab3fe2f7a79fa335fee3d874fa7f9e68575e0e62/brotli-1.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:963a08f3bebd8b75ac57661045402da15991468a621f014be54e50f53a58d19e", size = 1528168, upload-time = "2025-11-05T18:38:47.371Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a6/2ebfc8f766d46df8d3e65b880a2e220732395e6d7dc312c1e1244b0f074a/brotli-1.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9322b9f8656782414b37e6af884146869d46ab85158201d82bab9abbcb971dc7", size = 1627098, upload-time = "2025-11-05T18:38:48.385Z" }, + { url = "https://files.pythonhosted.org/packages/f3/2f/0976d5b097ff8a22163b10617f76b2557f15f0f39d6a0fe1f02b1a53e92b/brotli-1.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cf9cba6f5b78a2071ec6fb1e7bd39acf35071d90a81231d67e92d637776a6a63", size = 1419861, upload-time = "2025-11-05T18:38:49.372Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/d76df7176a2ce7616ff94c1fb72d307c9a30d2189fe877f3dd99af00ea5a/brotli-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7547369c4392b47d30a3467fe8c3330b4f2e0f7730e45e3103d7d636678a808b", size = 1484594, upload-time = "2025-11-05T18:38:50.655Z" }, + { url = "https://files.pythonhosted.org/packages/d3/93/14cf0b1216f43df5609f5b272050b0abd219e0b54ea80b47cef9867b45e7/brotli-1.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1530af5c3c275b8524f2e24841cbe2599d74462455e9bae5109e9ff42e9361", size = 1593455, upload-time = "2025-11-05T18:38:51.624Z" }, + { url = "https://files.pythonhosted.org/packages/b3/73/3183c9e41ca755713bdf2cc1d0810df742c09484e2e1ddd693bee53877c1/brotli-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2d085ded05278d1c7f65560aae97b3160aeb2ea2c0b3e26204856beccb60888", size = 1488164, upload-time = "2025-11-05T18:38:53.079Z" }, ] [[package]] @@ -238,9 +430,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/11/0e/7d8225aab3bc1a0f5811f8e1b557aa034ac04bdf641925b30d3caf586b28/cached_property-2.0.1-py3-none-any.whl", hash = "sha256:f617d70ab1100b7bcf6e42228f9ddcb78c676ffa167278d9f730d1c2fba69ccb", size = 7428, upload-time = "2024-10-25T15:43:54.711Z" }, ] +[[package]] +name = "cbor2" +version = "5.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/8e/8b4fdde28e42ffcd741a37f4ffa9fb59cd4fe01625b544dfcfd9ccb54f01/cbor2-5.8.0.tar.gz", hash = "sha256:b19c35fcae9688ac01ef75bad5db27300c2537eb4ee00ed07e05d8456a0d4931", size = 107825, upload-time = "2025-12-30T18:44:22.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/05/486166d9e998d65d70810e63eeacc8c5f13d167d8797cf2d73a588beb335/cbor2-5.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2263c0c892194f10012ced24c322d025d9d7b11b41da1c357f3b3fe06676e6b7", size = 69882, upload-time = "2025-12-30T18:43:25.365Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d0/ee976eaaf21c211eef651e1a921c109c3c3a3785d98307d74a70d142f341/cbor2-5.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ffe4ca079f6f8ed393f5c71a8de22651cb27bd50e74e2bcd6bc9c8f853a732b", size = 260696, upload-time = "2025-12-30T18:43:27.784Z" }, + { url = "https://files.pythonhosted.org/packages/66/7f/81cabd3aee6cc54b101a5214d5c3e541d275d7c05647c7dfc266c6aacf6f/cbor2-5.8.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0427bd166230fe4c4b72965c6f2b6273bf29016d97cf08b258fa48db851ea598", size = 252135, upload-time = "2025-12-30T18:43:29.418Z" }, + { url = "https://files.pythonhosted.org/packages/c2/0b/f38e8c579e7e2d88d446549bce35bde7d845199300bc456b4123d6e6f0af/cbor2-5.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c23a04947c37964d70028ca44ea2a8709f09b8adc0090f9b5710fa957e9bc545", size = 255342, upload-time = "2025-12-30T18:43:30.966Z" }, + { url = "https://files.pythonhosted.org/packages/5d/02/8413f1bd42c8f665fb85374151599cb4957848f0f307d08334a08dee544c/cbor2-5.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:218d5c7d2e8d13c7eded01a1b3fe2a9a1e51a7a843cefb8d38cb4bbbc6ad9bf7", size = 247191, upload-time = "2025-12-30T18:43:32.555Z" }, + { url = "https://files.pythonhosted.org/packages/88/4b/623435ef9b98e86b6956a41863d39ff4fe4d67983948b5834f55499681dd/cbor2-5.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:18ac191640093e6c7fbcb174c006ffec4106c3d8ab788e70272c1c4d933cbe11", size = 69875, upload-time = "2025-12-30T18:43:35.888Z" }, + { url = "https://files.pythonhosted.org/packages/58/17/f664201080b2a7d0f57c16c8e9e5922013b92f202e294863ec7e75b7ff7f/cbor2-5.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fddee9103a17d7bed5753f0c7fc6663faa506eb953e50d8287804eccf7b048e6", size = 268316, upload-time = "2025-12-30T18:43:37.161Z" }, + { url = "https://files.pythonhosted.org/packages/d0/e1/072745b4ff01afe9df2cd627f8fc51a1acedb5d3d1253765625d2929db91/cbor2-5.8.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d2ea26fad620aba5e88d7541be8b10c5034a55db9a23809b7cb49f36803f05b", size = 258874, upload-time = "2025-12-30T18:43:38.878Z" }, + { url = "https://files.pythonhosted.org/packages/a7/10/61c262b886d22b62c56e8aac6d10fa06d0953c997879ab882a31a624952b/cbor2-5.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:de68b4b310b072b082d317adc4c5e6910173a6d9455412e6183d72c778d1f54c", size = 261971, upload-time = "2025-12-30T18:43:40.401Z" }, + { url = "https://files.pythonhosted.org/packages/7e/42/b7862f5e64364b10ad120ea53e87ec7e891fb268cb99c572348e647cf7e9/cbor2-5.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:418d2cf0e03e90160fa1474c05a40fe228bbb4a92d1628bdbbd13a48527cb34d", size = 254151, upload-time = "2025-12-30T18:43:41.938Z" }, + { url = "https://files.pythonhosted.org/packages/2f/4f/3a16e3e8fd7e5fd86751a4f1aad218a8d19a96e75ec3989c3e95a8fe1d8f/cbor2-5.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b3f91fa699a5ce22470e973601c62dd9d55dc3ca20ee446516ac075fcab27c9", size = 70270, upload-time = "2025-12-30T18:43:46.005Z" }, + { url = "https://files.pythonhosted.org/packages/38/81/0d0cf0796fe8081492a61c45278f03def21a929535a492dd97c8438f5dbe/cbor2-5.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:518c118a5e00001854adb51f3164e647aa99b6a9877d2a733a28cb5c0a4d6857", size = 286242, upload-time = "2025-12-30T18:43:47.026Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a9/fdab6c10190cfb8d639e01f2b168f2406fc847a2a6bc00e7de78c3381d0a/cbor2-5.8.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cff2a1999e49cd51c23d1b6786a012127fd8f722c5946e82bd7ab3eb307443f3", size = 285412, upload-time = "2025-12-30T18:43:48.563Z" }, + { url = "https://files.pythonhosted.org/packages/31/59/746a8e630996217a3afd523f583fcf7e3d16640d63f9a03f0f4e4f74b5b1/cbor2-5.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c4492160212374973cdc14e46f0565f2462721ef922b40f7ea11e7d613dfb2a", size = 278041, upload-time = "2025-12-30T18:43:49.92Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a3/f3bbeb6dedd45c6e0cddd627ea790dea295eaf82c83f0e2159b733365ebd/cbor2-5.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:546c7c7c4c6bcdc54a59242e0e82cea8f332b17b4465ae628718fef1fce401ca", size = 278185, upload-time = "2025-12-30T18:43:51.192Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0d/5a3f20bafaefeb2c1903d961416f051c0950f0d09e7297a3aa6941596b29/cbor2-5.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6d8d104480845e2f28c6165b4c961bbe58d08cb5638f368375cfcae051c28015", size = 70332, upload-time = "2025-12-30T18:43:54.694Z" }, + { url = "https://files.pythonhosted.org/packages/57/66/177a3f089e69db69c987453ab4934086408c3338551e4984734597be9f80/cbor2-5.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:43efee947e5ab67d406d6e0dc61b5dee9d2f5e89ae176f90677a3741a20ca2e7", size = 285985, upload-time = "2025-12-30T18:43:55.733Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/9e17b8e4ed80a2ce97e2dfa5915c169dbb31599409ddb830f514b57f96cc/cbor2-5.8.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be7ae582f50be539e09c134966d0fd63723fc4789b8dff1f6c2e3f24ae3eaf32", size = 285173, upload-time = "2025-12-30T18:43:57.321Z" }, + { url = "https://files.pythonhosted.org/packages/cc/33/9f92e107d78f88ac22723ac15d0259d220ba98c1d855e51796317f4c4114/cbor2-5.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:50f5c709561a71ea7970b4cd2bf9eda4eccacc0aac212577080fdfe64183e7f5", size = 278395, upload-time = "2025-12-30T18:43:58.497Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3f/46b80050a4a35ce5cf7903693864a9fdea7213567dc8faa6e25cb375c182/cbor2-5.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a6790ecc73aa93e76d2d9076fc42bf91a9e69f2295e5fa702e776dbe986465bd", size = 278330, upload-time = "2025-12-30T18:43:59.656Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0c/0654233d7543ac8a50f4785f172430ddc97538ba418eb305d6e529d1a120/cbor2-5.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ad72381477133046ce217617d839ea4e9454f8b77d9a6351b229e214102daeb7", size = 70710, upload-time = "2025-12-30T18:44:03.209Z" }, + { url = "https://files.pythonhosted.org/packages/84/62/4671d24e557d7f5a74a01b422c538925140c0495e57decde7e566f91d029/cbor2-5.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6da25190fad3434ce99876b11d4ca6b8828df6ca232cf7344cd14ae1166fb718", size = 285005, upload-time = "2025-12-30T18:44:05.109Z" }, + { url = "https://files.pythonhosted.org/packages/87/85/0c67d763a08e848c9a80d7e4723ba497cce676f41bc7ca1828ae90a0a872/cbor2-5.8.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c13919e3a24c5a6d286551fa288848a4cedc3e507c58a722ccd134e461217d99", size = 282435, upload-time = "2025-12-30T18:44:06.465Z" }, + { url = "https://files.pythonhosted.org/packages/b2/01/0650972b4dbfbebcfbe37cbba7fc3cd9019a8da6397ab3446e07175e342b/cbor2-5.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f8c40d32e5972047a777f9bf730870828f3cf1c43b3eb96fd0429c57a1d3b9e6", size = 277493, upload-time = "2025-12-30T18:44:07.609Z" }, + { url = "https://files.pythonhosted.org/packages/b3/6c/7704a4f32adc7f10f3b41ec067f500a4458f7606397af5e4cf2d368fd288/cbor2-5.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7627894bc0b3d5d0807f31e3107e11b996205470c4429dc2bb4ef8bfe7f64e1e", size = 276085, upload-time = "2025-12-30T18:44:09.021Z" }, + { url = "https://files.pythonhosted.org/packages/d6/4f/101071f880b4da05771128c0b89f41e334cff044dee05fb013c8f4be661c/cbor2-5.8.0-py3-none-any.whl", hash = "sha256:3727d80f539567b03a7aa11890e57798c67092c38df9e6c23abb059e0f65069c", size = 24374, upload-time = "2025-12-30T18:44:21.476Z" }, +] + [[package]] name = "celery" -version = "5.5.3" +version = "5.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "billiard", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -248,13 +474,15 @@ dependencies = [ { name = "click-didyoumean", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "click-plugins", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "click-repl", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "exceptiongroup", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, { name = "kombu", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "tzlocal", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "vine", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/7d/6c289f407d219ba36d8b384b42489ebdd0c84ce9c413875a8aae0c85f35b/celery-5.5.3.tar.gz", hash = "sha256:6c972ae7968c2b5281227f01c3a3f984037d21c5129d07bf3550cc2afc6b10a5", size = 1667144, upload-time = "2025-06-01T11:08:12.563Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/9d/3d13596519cfa7207a6f9834f4b082554845eb3cd2684b5f8535d50c7c44/celery-5.6.2.tar.gz", hash = "sha256:4a8921c3fcf2ad76317d3b29020772103581ed2454c4c042cc55dcc43585009b", size = 1718802, upload-time = "2026-01-04T12:35:58.012Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/af/0dcccc7fdcdf170f9a1585e5e96b6fb0ba1749ef6be8c89a6202284759bd/celery-5.5.3-py3-none-any.whl", hash = "sha256:0b5761a07057acee94694464ca482416b959568904c9dfa41ce8413a7d65d525", size = 438775, upload-time = "2025-06-01T11:08:09.94Z" }, + { url = "https://files.pythonhosted.org/packages/dd/bd/9ecd619e456ae4ba73b6583cc313f26152afae13e9a82ac4fe7f8856bfd1/celery-5.6.2-py3-none-any.whl", hash = "sha256:3ffafacbe056951b629c7abcf9064c4a2366de0bdfc9fdba421b97ebb68619a5", size = 445502, upload-time = "2026-01-04T12:35:55.894Z" }, ] [package.optional-dependencies] @@ -264,23 +492,23 @@ redis = [ [[package]] name = "celery-types" -version = "0.23.0" +version = "0.24.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/d1/0823e71c281e4ad0044e278cf1577d1a68e05f2809424bf94e1614925c5d/celery_types-0.23.0.tar.gz", hash = "sha256:402ed0555aea3cd5e1e6248f4632e4f18eec8edb2435173f9e6dc08449fa101e", size = 31479, upload-time = "2025-03-03T23:56:51.547Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/25/2276a1f00f8ab9fc88128c939333933a24db7df1d75aa57ecc27b7dd3a22/celery_types-0.24.0.tar.gz", hash = "sha256:c93fbcd0b04a9e9c2f55d5540aca4aa1ea4cc06a870c0c8dee5062fdd59663fe", size = 33148, upload-time = "2025-12-23T17:16:30.847Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/8b/92bb54dd74d145221c3854aa245c84f4dc04cc9366147496182cec8e88e3/celery_types-0.23.0-py3-none-any.whl", hash = "sha256:0cc495b8d7729891b7e070d0ec8d4906d2373209656a6e8b8276fe1ed306af9a", size = 50189, upload-time = "2025-03-03T23:56:50.458Z" }, + { url = "https://files.pythonhosted.org/packages/3a/7e/3252cba5f5c9a65a3f52a69734d8e51e023db8981022b503e8183cf0225e/celery_types-0.24.0-py3-none-any.whl", hash = "sha256:a21e04681e68719a208335e556a79909da4be9c5e0d6d2fd0dd4c5615954b3fd", size = 60473, upload-time = "2025-12-23T17:16:29.89Z" }, ] [[package]] name = "certifi" -version = "2025.8.3" +version = "2026.1.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, ] [[package]] @@ -350,24 +578,24 @@ wheels = [ [[package]] name = "cfgv" -version = "3.4.0" +version = "3.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, ] [[package]] name = "channels" -version = "4.3.1" +version = "4.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/12/a0/46450fcf9e56af18a6b0440ba49db6635419bb7bc84142c35f4143b1a66c/channels-4.3.1.tar.gz", hash = "sha256:97413ffd674542db08e16a9ef09cd86ec0113e5f8125fbd33cf0854adcf27cdb", size = 26896, upload-time = "2025-08-01T13:25:19.952Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/92/b18d4bb54d14986a8b35215a1c9e6a7f9f4d57ca63ac9aee8290ebb4957d/channels-4.3.2.tar.gz", hash = "sha256:f2bb6bfb73ad7fb4705041d07613c7b4e69528f01ef8cb9fb6c21d9295f15667", size = 27023, upload-time = "2025-11-20T15:13:05.102Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/1c/eae1c2a8c195760376e7f65d0bdcc3e966695d29cfbe5c54841ce5c71408/channels-4.3.1-py3-none-any.whl", hash = "sha256:b091d4b26f91d807de3e84aead7ba785314f27eaf5bac31dd51b1c956b883859", size = 31286, upload-time = "2025-08-01T13:25:18.845Z" }, + { url = "https://files.pythonhosted.org/packages/16/34/c32915288b7ef482377b6adc401192f98c6a99b3a145423d3b8aed807898/channels-4.3.2-py3-none-any.whl", hash = "sha256:fef47e9055a603900cf16cef85f050d522d9ac4b3daccf24835bd9580705c176", size = 31313, upload-time = "2025-11-20T15:13:02.357Z" }, ] [[package]] @@ -387,65 +615,85 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.3" +version = "3.4.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695, upload-time = "2025-08-09T07:55:36.452Z" }, - { url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153, upload-time = "2025-08-09T07:55:38.467Z" }, - { url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428, upload-time = "2025-08-09T07:55:40.072Z" }, - { url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627, upload-time = "2025-08-09T07:55:41.706Z" }, - { url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388, upload-time = "2025-08-09T07:55:43.262Z" }, - { url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077, upload-time = "2025-08-09T07:55:44.903Z" }, - { url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631, upload-time = "2025-08-09T07:55:46.346Z" }, - { url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210, upload-time = "2025-08-09T07:55:47.539Z" }, - { url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739, upload-time = "2025-08-09T07:55:48.744Z" }, - { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" }, - { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" }, - { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" }, - { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" }, - { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" }, - { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" }, - { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" }, - { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, - { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, - { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, - { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, - { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, - { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, - { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, - { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, - { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, - { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, - { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, - { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, - { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, - { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, - { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, - { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, - { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, - { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, - { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, - { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, - { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, - { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, - { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, - { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, - { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, - { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] [[package]] name = "click" -version = "8.2.1" +version = "8.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] [[package]] @@ -517,81 +765,81 @@ wheels = [ [[package]] name = "coverage" -version = "7.10.7" +version = "7.13.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/49/349848445b0e53660e258acbcc9b0d014895b6739237920886672240f84b/coverage-7.13.2.tar.gz", hash = "sha256:044c6951ec37146b72a50cc81ef02217d27d4c3640efd2640311393cbbf143d3", size = 826523, upload-time = "2026-01-25T13:00:04.889Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" }, - { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" }, - { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" }, - { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" }, - { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" }, - { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" }, - { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" }, - { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" }, - { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" }, - { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" }, - { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" }, - { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" }, - { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" }, - { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" }, - { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" }, - { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" }, - { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" }, - { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" }, - { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" }, - { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" }, - { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" }, - { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" }, - { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" }, - { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" }, - { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" }, - { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" }, - { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" }, - { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, - { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, - { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, - { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, - { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, - { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, - { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, - { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, - { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, - { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, - { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, - { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, - { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, - { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, - { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, - { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, - { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, - { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, - { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, - { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, - { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, - { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, - { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, - { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, - { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, - { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, - { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, - { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, - { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, - { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, - { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, - { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, - { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, - { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, - { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, - { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, - { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, - { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, - { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, - { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, - { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, + { url = "https://files.pythonhosted.org/packages/a4/2d/63e37369c8e81a643afe54f76073b020f7b97ddbe698c5c944b51b0a2bc5/coverage-7.13.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4af3b01763909f477ea17c962e2cca8f39b350a4e46e3a30838b2c12e31b81b", size = 218842, upload-time = "2026-01-25T12:57:15.3Z" }, + { url = "https://files.pythonhosted.org/packages/57/06/86ce882a8d58cbcb3030e298788988e618da35420d16a8c66dac34f138d0/coverage-7.13.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:36393bd2841fa0b59498f75466ee9bdec4f770d3254f031f23e8fd8e140ffdd2", size = 219360, upload-time = "2026-01-25T12:57:17.572Z" }, + { url = "https://files.pythonhosted.org/packages/cd/84/70b0eb1ee19ca4ef559c559054c59e5b2ae4ec9af61398670189e5d276e9/coverage-7.13.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9cc7573518b7e2186bd229b1a0fe24a807273798832c27032c4510f47ffdb896", size = 246123, upload-time = "2026-01-25T12:57:19.087Z" }, + { url = "https://files.pythonhosted.org/packages/35/fb/05b9830c2e8275ebc031e0019387cda99113e62bb500ab328bb72578183b/coverage-7.13.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca9566769b69a5e216a4e176d54b9df88f29d750c5b78dbb899e379b4e14b30c", size = 247930, upload-time = "2026-01-25T12:57:20.929Z" }, + { url = "https://files.pythonhosted.org/packages/81/aa/3f37858ca2eed4f09b10ca3c6ddc9041be0a475626cd7fd2712f4a2d526f/coverage-7.13.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c9bdea644e94fd66d75a6f7e9a97bb822371e1fe7eadae2cacd50fcbc28e4dc", size = 249804, upload-time = "2026-01-25T12:57:22.904Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b3/c904f40c56e60a2d9678a5ee8df3d906d297d15fb8bec5756c3b0a67e2df/coverage-7.13.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5bd447332ec4f45838c1ad42268ce21ca87c40deb86eabd59888859b66be22a5", size = 246815, upload-time = "2026-01-25T12:57:24.314Z" }, + { url = "https://files.pythonhosted.org/packages/41/91/ddc1c5394ca7fd086342486440bfdd6b9e9bda512bf774599c7c7a0081e0/coverage-7.13.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7c79ad5c28a16a1277e1187cf83ea8dafdcc689a784228a7d390f19776db7c31", size = 247843, upload-time = "2026-01-25T12:57:26.544Z" }, + { url = "https://files.pythonhosted.org/packages/87/d2/cdff8f4cd33697883c224ea8e003e9c77c0f1a837dc41d95a94dd26aad67/coverage-7.13.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:76e06ccacd1fb6ada5d076ed98a8c6f66e2e6acd3df02819e2ee29fd637b76ad", size = 245850, upload-time = "2026-01-25T12:57:28.507Z" }, + { url = "https://files.pythonhosted.org/packages/f5/42/e837febb7866bf2553ab53dd62ed52f9bb36d60c7e017c55376ad21fbb05/coverage-7.13.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:49d49e9a5e9f4dc3d3dac95278a020afa6d6bdd41f63608a76fa05a719d5b66f", size = 246116, upload-time = "2026-01-25T12:57:30.16Z" }, + { url = "https://files.pythonhosted.org/packages/09/b1/4a3f935d7df154df02ff4f71af8d61298d713a7ba305d050ae475bfbdde2/coverage-7.13.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed2bce0e7bfa53f7b0b01c722da289ef6ad4c18ebd52b1f93704c21f116360c8", size = 246720, upload-time = "2026-01-25T12:57:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/6c/01/abca50583a8975bb6e1c59eff67ed8e48bb127c07dad5c28d9e96ccc09ec/coverage-7.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:060ebf6f2c51aff5ba38e1f43a2095e087389b1c69d559fde6049a4b0001320e", size = 218971, upload-time = "2026-01-25T12:57:36.953Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0e/b6489f344d99cd1e5b4d5e1be52dfd3f8a3dc5112aa6c33948da8cabad4e/coverage-7.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1ea8ca9db5e7469cd364552985e15911548ea5b69c48a17291f0cac70484b2e", size = 219473, upload-time = "2026-01-25T12:57:38.934Z" }, + { url = "https://files.pythonhosted.org/packages/17/11/db2f414915a8e4ec53f60b17956c27f21fb68fcf20f8a455ce7c2ccec638/coverage-7.13.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b780090d15fd58f07cf2011943e25a5f0c1c894384b13a216b6c86c8a8a7c508", size = 249896, upload-time = "2026-01-25T12:57:40.365Z" }, + { url = "https://files.pythonhosted.org/packages/80/06/0823fe93913663c017e508e8810c998c8ebd3ec2a5a85d2c3754297bdede/coverage-7.13.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:88a800258d83acb803c38175b4495d293656d5fac48659c953c18e5f539a274b", size = 251810, upload-time = "2026-01-25T12:57:42.045Z" }, + { url = "https://files.pythonhosted.org/packages/61/dc/b151c3cc41b28cdf7f0166c5fa1271cbc305a8ec0124cce4b04f74791a18/coverage-7.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6326e18e9a553e674d948536a04a80d850a5eeefe2aae2e6d7cf05d54046c01b", size = 253920, upload-time = "2026-01-25T12:57:44.026Z" }, + { url = "https://files.pythonhosted.org/packages/2d/35/e83de0556e54a4729a2b94ea816f74ce08732e81945024adee46851c2264/coverage-7.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:59562de3f797979e1ff07c587e2ac36ba60ca59d16c211eceaa579c266c5022f", size = 250025, upload-time = "2026-01-25T12:57:45.624Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/af2eb9c3926ce3ea0d58a0d2516fcbdacf7a9fc9559fe63076beaf3f2596/coverage-7.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:27ba1ed6f66b0e2d61bfa78874dffd4f8c3a12f8e2b5410e515ab345ba7bc9c3", size = 251612, upload-time = "2026-01-25T12:57:47.713Z" }, + { url = "https://files.pythonhosted.org/packages/26/62/5be2e25f3d6c711d23b71296f8b44c978d4c8b4e5b26871abfc164297502/coverage-7.13.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8be48da4d47cc68754ce643ea50b3234557cbefe47c2f120495e7bd0a2756f2b", size = 249670, upload-time = "2026-01-25T12:57:49.378Z" }, + { url = "https://files.pythonhosted.org/packages/b3/51/400d1b09a8344199f9b6a6fc1868005d766b7ea95e7882e494fa862ca69c/coverage-7.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2a47a4223d3361b91176aedd9d4e05844ca67d7188456227b6bf5e436630c9a1", size = 249395, upload-time = "2026-01-25T12:57:50.86Z" }, + { url = "https://files.pythonhosted.org/packages/e0/36/f02234bc6e5230e2f0a63fd125d0a2093c73ef20fdf681c7af62a140e4e7/coverage-7.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6f141b468740197d6bd38f2b26ade124363228cc3f9858bd9924ab059e00059", size = 250298, upload-time = "2026-01-25T12:57:52.287Z" }, + { url = "https://files.pythonhosted.org/packages/46/39/e92a35f7800222d3f7b2cbb7bbc3b65672ae8d501cb31801b2d2bd7acdf1/coverage-7.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f106b2af193f965d0d3234f3f83fc35278c7fb935dfbde56ae2da3dd2c03b84d", size = 219142, upload-time = "2026-01-25T12:58:00.448Z" }, + { url = "https://files.pythonhosted.org/packages/45/7a/8bf9e9309c4c996e65c52a7c5a112707ecdd9fbaf49e10b5a705a402bbb4/coverage-7.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f45d21dc4d5d6bd29323f0320089ef7eae16e4bef712dff79d184fa7330af3", size = 219503, upload-time = "2026-01-25T12:58:02.451Z" }, + { url = "https://files.pythonhosted.org/packages/87/93/17661e06b7b37580923f3f12406ac91d78aeed293fb6da0b69cc7957582f/coverage-7.13.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fae91dfecd816444c74531a9c3d6ded17a504767e97aa674d44f638107265b99", size = 251006, upload-time = "2026-01-25T12:58:04.059Z" }, + { url = "https://files.pythonhosted.org/packages/12/f0/f9e59fb8c310171497f379e25db060abef9fa605e09d63157eebec102676/coverage-7.13.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:264657171406c114787b441484de620e03d8f7202f113d62fcd3d9688baa3e6f", size = 253750, upload-time = "2026-01-25T12:58:05.574Z" }, + { url = "https://files.pythonhosted.org/packages/e5/b1/1935e31add2232663cf7edd8269548b122a7d100047ff93475dbaaae673e/coverage-7.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae47d8dcd3ded0155afbb59c62bd8ab07ea0fd4902e1c40567439e6db9dcaf2f", size = 254862, upload-time = "2026-01-25T12:58:07.647Z" }, + { url = "https://files.pythonhosted.org/packages/af/59/b5e97071ec13df5f45da2b3391b6cdbec78ba20757bc92580a5b3d5fa53c/coverage-7.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a0b33e9fd838220b007ce8f299114d406c1e8edb21336af4c97a26ecfd185aa", size = 251420, upload-time = "2026-01-25T12:58:09.309Z" }, + { url = "https://files.pythonhosted.org/packages/3f/75/9495932f87469d013dc515fb0ce1aac5fa97766f38f6b1a1deb1ee7b7f3a/coverage-7.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3becbea7f3ce9a2d4d430f223ec15888e4deb31395840a79e916368d6004cce", size = 252786, upload-time = "2026-01-25T12:58:10.909Z" }, + { url = "https://files.pythonhosted.org/packages/6a/59/af550721f0eb62f46f7b8cb7e6f1860592189267b1c411a4e3a057caacee/coverage-7.13.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f819c727a6e6eeb8711e4ce63d78c620f69630a2e9d53bc95ca5379f57b6ba94", size = 250928, upload-time = "2026-01-25T12:58:12.449Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b1/21b4445709aae500be4ab43bbcfb4e53dc0811c3396dcb11bf9f23fd0226/coverage-7.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4f7b71757a3ab19f7ba286e04c181004c1d61be921795ee8ba6970fd0ec91da5", size = 250496, upload-time = "2026-01-25T12:58:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b1/0f5d89dfe0392990e4f3980adbde3eb34885bc1effb2dc369e0bf385e389/coverage-7.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b7fc50d2afd2e6b4f6f2f403b70103d280a8e0cb35320cbbe6debcda02a1030b", size = 252373, upload-time = "2026-01-25T12:58:15.976Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f0/3d3eac7568ab6096ff23791a526b0048a1ff3f49d0e236b2af6fb6558e88/coverage-7.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed75de7d1217cf3b99365d110975f83af0528c849ef5180a12fd91b5064df9d6", size = 219168, upload-time = "2026-01-25T12:58:23.376Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a6/f8b5cfeddbab95fdef4dcd682d82e5dcff7a112ced57a959f89537ee9995/coverage-7.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97e596de8fa9bada4d88fde64a3f4d37f1b6131e4faa32bad7808abc79887ddc", size = 219537, upload-time = "2026-01-25T12:58:24.932Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e6/8d8e6e0c516c838229d1e41cadcec91745f4b1031d4db17ce0043a0423b4/coverage-7.13.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:68c86173562ed4413345410c9480a8d64864ac5e54a5cda236748031e094229f", size = 250528, upload-time = "2026-01-25T12:58:26.567Z" }, + { url = "https://files.pythonhosted.org/packages/8e/78/befa6640f74092b86961f957f26504c8fba3d7da57cc2ab7407391870495/coverage-7.13.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7be4d613638d678b2b3773b8f687537b284d7074695a43fe2fbbfc0e31ceaed1", size = 253132, upload-time = "2026-01-25T12:58:28.251Z" }, + { url = "https://files.pythonhosted.org/packages/9d/10/1630db1edd8ce675124a2ee0f7becc603d2bb7b345c2387b4b95c6907094/coverage-7.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7f63ce526a96acd0e16c4af8b50b64334239550402fb1607ce6a584a6d62ce9", size = 254374, upload-time = "2026-01-25T12:58:30.294Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1d/0d9381647b1e8e6d310ac4140be9c428a0277330991e0c35bdd751e338a4/coverage-7.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:406821f37f864f968e29ac14c3fccae0fec9fdeba48327f0341decf4daf92d7c", size = 250762, upload-time = "2026-01-25T12:58:32.036Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5636dfc9a7c871ee8776af83ee33b4c26bc508ad6cee1e89b6419a366582/coverage-7.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ee68e5a4e3e5443623406b905db447dceddffee0dceb39f4e0cd9ec2a35004b5", size = 252502, upload-time = "2026-01-25T12:58:33.961Z" }, + { url = "https://files.pythonhosted.org/packages/02/2a/7ff2884d79d420cbb2d12fed6fff727b6d0ef27253140d3cdbbd03187ee0/coverage-7.13.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2ee0e58cca0c17dd9c6c1cdde02bb705c7b3fbfa5f3b0b5afeda20d4ebff8ef4", size = 250463, upload-time = "2026-01-25T12:58:35.529Z" }, + { url = "https://files.pythonhosted.org/packages/91/c0/ba51087db645b6c7261570400fc62c89a16278763f36ba618dc8657a187b/coverage-7.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e5bbb5018bf76a56aabdb64246b5288d5ae1b7d0dd4d0534fe86df2c2992d1c", size = 250288, upload-time = "2026-01-25T12:58:37.226Z" }, + { url = "https://files.pythonhosted.org/packages/03/07/44e6f428551c4d9faf63ebcefe49b30e5c89d1be96f6a3abd86a52da9d15/coverage-7.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a55516c68ef3e08e134e818d5e308ffa6b1337cc8b092b69b24287bf07d38e31", size = 252063, upload-time = "2026-01-25T12:58:38.821Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f7/080376c029c8f76fadfe43911d0daffa0cbdc9f9418a0eead70c56fb7f4b/coverage-7.13.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5a1e41ce5df6b547cbc3d3699381c9e2c2c369c67837e716ed0f549d48e", size = 219861, upload-time = "2026-01-25T12:58:46.586Z" }, + { url = "https://files.pythonhosted.org/packages/42/11/0b5e315af5ab35f4c4a70e64d3314e4eec25eefc6dec13be3a7d5ffe8ac5/coverage-7.13.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b01899e82a04085b6561eb233fd688474f57455e8ad35cd82286463ba06332b7", size = 220207, upload-time = "2026-01-25T12:58:48.277Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0c/0874d0318fb1062117acbef06a09cf8b63f3060c22265adaad24b36306b7/coverage-7.13.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:838943bea48be0e2768b0cf7819544cdedc1bbb2f28427eabb6eb8c9eb2285d3", size = 261504, upload-time = "2026-01-25T12:58:49.904Z" }, + { url = "https://files.pythonhosted.org/packages/83/5e/1cd72c22ecb30751e43a72f40ba50fcef1b7e93e3ea823bd9feda8e51f9a/coverage-7.13.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:93d1d25ec2b27e90bcfef7012992d1f5121b51161b8bffcda756a816cf13c2c3", size = 263582, upload-time = "2026-01-25T12:58:51.582Z" }, + { url = "https://files.pythonhosted.org/packages/9b/da/8acf356707c7a42df4d0657020308e23e5a07397e81492640c186268497c/coverage-7.13.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93b57142f9621b0d12349c43fc7741fe578e4bc914c1e5a54142856cfc0bf421", size = 266008, upload-time = "2026-01-25T12:58:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/41/41/ea1730af99960309423c6ea8d6a4f1fa5564b2d97bd1d29dda4b42611f04/coverage-7.13.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f06799ae1bdfff7ccb8665d75f8291c69110ba9585253de254688aa8a1ccc6c5", size = 260762, upload-time = "2026-01-25T12:58:55.372Z" }, + { url = "https://files.pythonhosted.org/packages/22/fa/02884d2080ba71db64fdc127b311db60e01fe6ba797d9c8363725e39f4d5/coverage-7.13.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f9405ab4f81d490811b1d91c7a20361135a2df4c170e7f0b747a794da5b7f23", size = 263571, upload-time = "2026-01-25T12:58:57.52Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6b/4083aaaeba9b3112f55ac57c2ce7001dc4d8fa3fcc228a39f09cc84ede27/coverage-7.13.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f9ab1d5b86f8fbc97a5b3cd6280a3fd85fef3b028689d8a2c00918f0d82c728c", size = 261200, upload-time = "2026-01-25T12:58:59.255Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d2/aea92fa36d61955e8c416ede9cf9bf142aa196f3aea214bb67f85235a050/coverage-7.13.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:f674f59712d67e841525b99e5e2b595250e39b529c3bda14764e4f625a3fa01f", size = 260095, upload-time = "2026-01-25T12:59:01.066Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ae/04ffe96a80f107ea21b22b2367175c621da920063260a1c22f9452fd7866/coverage-7.13.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c6cadac7b8ace1ba9144feb1ae3cb787a6065ba6d23ffc59a934b16406c26573", size = 262284, upload-time = "2026-01-25T12:59:02.802Z" }, + { url = "https://files.pythonhosted.org/packages/55/53/1da9e51a0775634b04fcc11eb25c002fc58ee4f92ce2e8512f94ac5fc5bf/coverage-7.13.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:387a825f43d680e7310e6f325b2167dd093bc8ffd933b83e9aa0983cf6e0a2ef", size = 219213, upload-time = "2026-01-25T12:59:11.909Z" }, + { url = "https://files.pythonhosted.org/packages/46/35/b3caac3ebbd10230fea5a33012b27d19e999a17c9285c4228b4b2e35b7da/coverage-7.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f0d7fea9d8e5d778cd5a9e8fc38308ad688f02040e883cdc13311ef2748cb40f", size = 219549, upload-time = "2026-01-25T12:59:13.638Z" }, + { url = "https://files.pythonhosted.org/packages/76/9c/e1cf7def1bdc72c1907e60703983a588f9558434a2ff94615747bd73c192/coverage-7.13.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e080afb413be106c95c4ee96b4fffdc9e2fa56a8bbf90b5c0918e5c4449412f5", size = 250586, upload-time = "2026-01-25T12:59:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ba/49/f54ec02ed12be66c8d8897270505759e057b0c68564a65c429ccdd1f139e/coverage-7.13.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7fc042ba3c7ce25b8a9f097eb0f32a5ce1ccdb639d9eec114e26def98e1f8a4", size = 253093, upload-time = "2026-01-25T12:59:17.491Z" }, + { url = "https://files.pythonhosted.org/packages/fb/5e/aaf86be3e181d907e23c0f61fccaeb38de8e6f6b47aed92bf57d8fc9c034/coverage-7.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0ba505e021557f7f8173ee8cd6b926373d8653e5ff7581ae2efce1b11ef4c27", size = 254446, upload-time = "2026-01-25T12:59:19.752Z" }, + { url = "https://files.pythonhosted.org/packages/28/c8/a5fa01460e2d75b0c853b392080d6829d3ca8b5ab31e158fa0501bc7c708/coverage-7.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7de326f80e3451bd5cc7239ab46c73ddb658fe0b7649476bc7413572d36cd548", size = 250615, upload-time = "2026-01-25T12:59:21.928Z" }, + { url = "https://files.pythonhosted.org/packages/86/0b/6d56315a55f7062bb66410732c24879ccb2ec527ab6630246de5fe45a1df/coverage-7.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:abaea04f1e7e34841d4a7b343904a3f59481f62f9df39e2cd399d69a187a9660", size = 252452, upload-time = "2026-01-25T12:59:23.592Z" }, + { url = "https://files.pythonhosted.org/packages/30/19/9bc550363ebc6b0ea121977ee44d05ecd1e8bf79018b8444f1028701c563/coverage-7.13.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9f93959ee0c604bccd8e0697be21de0887b1f73efcc3aa73a3ec0fd13feace92", size = 250418, upload-time = "2026-01-25T12:59:25.392Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/580530a31ca2f0cc6f07a8f2ab5460785b02bb11bdf815d4c4d37a4c5169/coverage-7.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:13fe81ead04e34e105bf1b3c9f9cdf32ce31736ee5d90a8d2de02b9d3e1bcb82", size = 250231, upload-time = "2026-01-25T12:59:27.888Z" }, + { url = "https://files.pythonhosted.org/packages/e2/42/dd9093f919dc3088cb472893651884bd675e3df3d38a43f9053656dca9a2/coverage-7.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d6d16b0f71120e365741bca2cb473ca6fe38930bc5431c5e850ba949f708f892", size = 251888, upload-time = "2026-01-25T12:59:29.636Z" }, + { url = "https://files.pythonhosted.org/packages/b6/60/a3820c7232db63be060e4019017cd3426751c2699dab3c62819cdbcea387/coverage-7.13.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f61d349f5b7cd95c34017f1927ee379bfbe9884300d74e07cf630ccf7a610c1b", size = 219950, upload-time = "2026-01-25T12:59:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/fd/37/e4ef5975fdeb86b1e56db9a82f41b032e3d93a840ebaf4064f39e770d5c5/coverage-7.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a43d34ce714f4ca674c0d90beb760eb05aad906f2c47580ccee9da8fe8bfb417", size = 220209, upload-time = "2026-01-25T12:59:38.339Z" }, + { url = "https://files.pythonhosted.org/packages/54/df/d40e091d00c51adca1e251d3b60a8b464112efa3004949e96a74d7c19a64/coverage-7.13.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bff1b04cb9d4900ce5c56c4942f047dc7efe57e2608cb7c3c8936e9970ccdbee", size = 261576, upload-time = "2026-01-25T12:59:40.446Z" }, + { url = "https://files.pythonhosted.org/packages/c5/44/5259c4bed54e3392e5c176121af9f71919d96dde853386e7730e705f3520/coverage-7.13.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6ae99e4560963ad8e163e819e5d77d413d331fd00566c1e0856aa252303552c1", size = 263704, upload-time = "2026-01-25T12:59:42.346Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/ae9f005827abcbe2c70157459ae86053971c9fa14617b63903abbdce26d9/coverage-7.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e79a8c7d461820257d9aa43716c4efc55366d7b292e46b5b37165be1d377405d", size = 266109, upload-time = "2026-01-25T12:59:44.073Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c0/8e279c1c0f5b1eaa3ad9b0fb7a5637fc0379ea7d85a781c0fe0bb3cfc2ab/coverage-7.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:060ee84f6a769d40c492711911a76811b4befb6fba50abb450371abb720f5bd6", size = 260686, upload-time = "2026-01-25T12:59:45.804Z" }, + { url = "https://files.pythonhosted.org/packages/b2/47/3a8112627e9d863e7cddd72894171c929e94491a597811725befdcd76bce/coverage-7.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bca209d001fd03ea2d978f8a4985093240a355c93078aee3f799852c23f561a", size = 263568, upload-time = "2026-01-25T12:59:47.929Z" }, + { url = "https://files.pythonhosted.org/packages/92/bc/7ea367d84afa3120afc3ce6de294fd2dcd33b51e2e7fbe4bbfd200f2cb8c/coverage-7.13.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6b8092aa38d72f091db61ef83cb66076f18f02da3e1a75039a4f218629600e04", size = 261174, upload-time = "2026-01-25T12:59:49.717Z" }, + { url = "https://files.pythonhosted.org/packages/33/b7/f1092dcecb6637e31cc2db099581ee5c61a17647849bae6b8261a2b78430/coverage-7.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4a3158dc2dcce5200d91ec28cd315c999eebff355437d2765840555d765a6e5f", size = 260017, upload-time = "2026-01-25T12:59:51.463Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cd/f3d07d4b95fbe1a2ef0958c15da614f7e4f557720132de34d2dc3aa7e911/coverage-7.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3973f353b2d70bd9796cc12f532a05945232ccae966456c8ed7034cb96bbfd6f", size = 262337, upload-time = "2026-01-25T12:59:53.407Z" }, + { url = "https://files.pythonhosted.org/packages/d2/db/d291e30fdf7ea617a335531e72294e0c723356d7fdde8fba00610a76bda9/coverage-7.13.2-py3-none-any.whl", hash = "sha256:40ce1ea1e25125556d8e76bd0b61500839a07944cc287ac21d5626f3e620cad5", size = 210943, upload-time = "2026-01-25T13:00:02.388Z" }, ] [package.optional-dependencies] @@ -601,43 +849,56 @@ toml = [ [[package]] name = "cryptography" -version = "44.0.3" +version = "46.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "(platform_python_implementation != 'PyPy' and sys_platform == 'darwin') or (platform_python_implementation != 'PyPy' and sys_platform == 'linux')" }, + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/d6/1411ab4d6108ab167d06254c5be517681f1e331f90edf1379895bcb87020/cryptography-44.0.3.tar.gz", hash = "sha256:fe19d8bc5536a91a24a8133328880a41831b6c5df54599a8417b62fe015d3053", size = 711096, upload-time = "2025-05-02T19:36:04.667Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/53/c776d80e9d26441bb3868457909b4e74dd9ccabd182e10b2b0ae7a07e265/cryptography-44.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:962bc30480a08d133e631e8dfd4783ab71cc9e33d5d7c1e192f0b7c06397bb88", size = 6670281, upload-time = "2025-05-02T19:34:50.665Z" }, - { url = "https://files.pythonhosted.org/packages/6a/06/af2cf8d56ef87c77319e9086601bef621bedf40f6f59069e1b6d1ec498c5/cryptography-44.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffc61e8f3bf5b60346d89cd3d37231019c17a081208dfbbd6e1605ba03fa137", size = 3959305, upload-time = "2025-05-02T19:34:53.042Z" }, - { url = "https://files.pythonhosted.org/packages/ae/01/80de3bec64627207d030f47bf3536889efee8913cd363e78ca9a09b13c8e/cryptography-44.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58968d331425a6f9eedcee087f77fd3c927c88f55368f43ff7e0a19891f2642c", size = 4171040, upload-time = "2025-05-02T19:34:54.675Z" }, - { url = "https://files.pythonhosted.org/packages/bd/48/bb16b7541d207a19d9ae8b541c70037a05e473ddc72ccb1386524d4f023c/cryptography-44.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e28d62e59a4dbd1d22e747f57d4f00c459af22181f0b2f787ea83f5a876d7c76", size = 3963411, upload-time = "2025-05-02T19:34:56.61Z" }, - { url = "https://files.pythonhosted.org/packages/42/b2/7d31f2af5591d217d71d37d044ef5412945a8a8e98d5a2a8ae4fd9cd4489/cryptography-44.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af653022a0c25ef2e3ffb2c673a50e5a0d02fecc41608f4954176f1933b12359", size = 3689263, upload-time = "2025-05-02T19:34:58.591Z" }, - { url = "https://files.pythonhosted.org/packages/25/50/c0dfb9d87ae88ccc01aad8eb93e23cfbcea6a6a106a9b63a7b14c1f93c75/cryptography-44.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:157f1f3b8d941c2bd8f3ffee0af9b049c9665c39d3da9db2dc338feca5e98a43", size = 4196198, upload-time = "2025-05-02T19:35:00.988Z" }, - { url = "https://files.pythonhosted.org/packages/66/c9/55c6b8794a74da652690c898cb43906310a3e4e4f6ee0b5f8b3b3e70c441/cryptography-44.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:c6cd67722619e4d55fdb42ead64ed8843d64638e9c07f4011163e46bc512cf01", size = 3966502, upload-time = "2025-05-02T19:35:03.091Z" }, - { url = "https://files.pythonhosted.org/packages/b6/f7/7cb5488c682ca59a02a32ec5f975074084db4c983f849d47b7b67cc8697a/cryptography-44.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b424563394c369a804ecbee9b06dfb34997f19d00b3518e39f83a5642618397d", size = 4196173, upload-time = "2025-05-02T19:35:05.018Z" }, - { url = "https://files.pythonhosted.org/packages/d2/0b/2f789a8403ae089b0b121f8f54f4a3e5228df756e2146efdf4a09a3d5083/cryptography-44.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c91fc8e8fd78af553f98bc7f2a1d8db977334e4eea302a4bfd75b9461c2d8904", size = 4087713, upload-time = "2025-05-02T19:35:07.187Z" }, - { url = "https://files.pythonhosted.org/packages/1d/aa/330c13655f1af398fc154089295cf259252f0ba5df93b4bc9d9c7d7f843e/cryptography-44.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:25cd194c39fa5a0aa4169125ee27d1172097857b27109a45fadc59653ec06f44", size = 4299064, upload-time = "2025-05-02T19:35:08.879Z" }, - { url = "https://files.pythonhosted.org/packages/34/a3/ad08e0bcc34ad436013458d7528e83ac29910943cea42ad7dd4141a27bbb/cryptography-44.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:5639c2b16764c6f76eedf722dbad9a0914960d3489c0cc38694ddf9464f1bb2f", size = 6673501, upload-time = "2025-05-02T19:35:13.775Z" }, - { url = "https://files.pythonhosted.org/packages/b1/f0/7491d44bba8d28b464a5bc8cc709f25a51e3eac54c0a4444cf2473a57c37/cryptography-44.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ffef566ac88f75967d7abd852ed5f182da252d23fac11b4766da3957766759", size = 3960307, upload-time = "2025-05-02T19:35:15.917Z" }, - { url = "https://files.pythonhosted.org/packages/f7/c8/e5c5d0e1364d3346a5747cdcd7ecbb23ca87e6dea4f942a44e88be349f06/cryptography-44.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:192ed30fac1728f7587c6f4613c29c584abdc565d7417c13904708db10206645", size = 4170876, upload-time = "2025-05-02T19:35:18.138Z" }, - { url = "https://files.pythonhosted.org/packages/73/96/025cb26fc351d8c7d3a1c44e20cf9a01e9f7cf740353c9c7a17072e4b264/cryptography-44.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7d5fe7195c27c32a64955740b949070f21cba664604291c298518d2e255931d2", size = 3964127, upload-time = "2025-05-02T19:35:19.864Z" }, - { url = "https://files.pythonhosted.org/packages/01/44/eb6522db7d9f84e8833ba3bf63313f8e257729cf3a8917379473fcfd6601/cryptography-44.0.3-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3f07943aa4d7dad689e3bb1638ddc4944cc5e0921e3c227486daae0e31a05e54", size = 3689164, upload-time = "2025-05-02T19:35:21.449Z" }, - { url = "https://files.pythonhosted.org/packages/68/fb/d61a4defd0d6cee20b1b8a1ea8f5e25007e26aeb413ca53835f0cae2bcd1/cryptography-44.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb90f60e03d563ca2445099edf605c16ed1d5b15182d21831f58460c48bffb93", size = 4198081, upload-time = "2025-05-02T19:35:23.187Z" }, - { url = "https://files.pythonhosted.org/packages/1b/50/457f6911d36432a8811c3ab8bd5a6090e8d18ce655c22820994913dd06ea/cryptography-44.0.3-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ab0b005721cc0039e885ac3503825661bd9810b15d4f374e473f8c89b7d5460c", size = 3967716, upload-time = "2025-05-02T19:35:25.426Z" }, - { url = "https://files.pythonhosted.org/packages/35/6e/dca39d553075980ccb631955c47b93d87d27f3596da8d48b1ae81463d915/cryptography-44.0.3-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3bb0847e6363c037df8f6ede57d88eaf3410ca2267fb12275370a76f85786a6f", size = 4197398, upload-time = "2025-05-02T19:35:27.678Z" }, - { url = "https://files.pythonhosted.org/packages/9b/9d/d1f2fe681eabc682067c66a74addd46c887ebacf39038ba01f8860338d3d/cryptography-44.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0cc66c74c797e1db750aaa842ad5b8b78e14805a9b5d1348dc603612d3e3ff5", size = 4087900, upload-time = "2025-05-02T19:35:29.312Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f5/3599e48c5464580b73b236aafb20973b953cd2e7b44c7c2533de1d888446/cryptography-44.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6866df152b581f9429020320e5eb9794c8780e90f7ccb021940d7f50ee00ae0b", size = 4301067, upload-time = "2025-05-02T19:35:31.547Z" }, - { url = "https://files.pythonhosted.org/packages/7f/10/abcf7418536df1eaba70e2cfc5c8a0ab07aa7aa02a5cbc6a78b9d8b4f121/cryptography-44.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:cad399780053fb383dc067475135e41c9fe7d901a97dd5d9c5dfb5611afc0d7d", size = 3393192, upload-time = "2025-05-02T19:35:37.468Z" }, - { url = "https://files.pythonhosted.org/packages/06/59/ecb3ef380f5891978f92a7f9120e2852b1df6f0a849c277b8ea45b865db2/cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:21a83f6f35b9cc656d71b5de8d519f566df01e660ac2578805ab245ffd8523f8", size = 3898419, upload-time = "2025-05-02T19:35:39.065Z" }, - { url = "https://files.pythonhosted.org/packages/bb/d0/35e2313dbb38cf793aa242182ad5bc5ef5c8fd4e5dbdc380b936c7d51169/cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fc3c9babc1e1faefd62704bb46a69f359a9819eb0292e40df3fb6e3574715cd4", size = 4117892, upload-time = "2025-05-02T19:35:40.839Z" }, - { url = "https://files.pythonhosted.org/packages/dc/c8/31fb6e33b56c2c2100d76de3fd820afaa9d4d0b6aea1ccaf9aaf35dc7ce3/cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:e909df4053064a97f1e6565153ff8bb389af12c5c8d29c343308760890560aff", size = 3900855, upload-time = "2025-05-02T19:35:42.599Z" }, - { url = "https://files.pythonhosted.org/packages/43/2a/08cc2ec19e77f2a3cfa2337b429676406d4bb78ddd130a05c458e7b91d73/cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:dad80b45c22e05b259e33ddd458e9e2ba099c86ccf4e88db7bbab4b747b18d06", size = 4117619, upload-time = "2025-05-02T19:35:44.774Z" }, - { url = "https://files.pythonhosted.org/packages/8d/4b/c11ad0b6c061902de5223892d680e89c06c7c4d606305eb8de56c5427ae6/cryptography-44.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:896530bc9107b226f265effa7ef3f21270f18a2026bc09fed1ebd7b66ddf6375", size = 3390230, upload-time = "2025-05-02T19:35:49.062Z" }, - { url = "https://files.pythonhosted.org/packages/58/11/0a6bf45d53b9b2290ea3cec30e78b78e6ca29dc101e2e296872a0ffe1335/cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9b4d4a5dbee05a2c390bf212e78b99434efec37b17a4bff42f50285c5c8c9647", size = 3895216, upload-time = "2025-05-02T19:35:51.351Z" }, - { url = "https://files.pythonhosted.org/packages/0a/27/b28cdeb7270e957f0077a2c2bfad1b38f72f1f6d699679f97b816ca33642/cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02f55fb4f8b79c1221b0961488eaae21015b69b210e18c386b69de182ebb1259", size = 4115044, upload-time = "2025-05-02T19:35:53.044Z" }, - { url = "https://files.pythonhosted.org/packages/35/b0/ec4082d3793f03cb248881fecefc26015813199b88f33e3e990a43f79835/cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dd3db61b8fe5be220eee484a17233287d0be6932d056cf5738225b9c05ef4fff", size = 3898034, upload-time = "2025-05-02T19:35:54.72Z" }, - { url = "https://files.pythonhosted.org/packages/0b/7f/adf62e0b8e8d04d50c9a91282a57628c00c54d4ae75e2b02a223bd1f2613/cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:978631ec51a6bbc0b7e58f23b68a8ce9e5f09721940933e9c217068388789fe5", size = 4114449, upload-time = "2025-05-02T19:35:57.139Z" }, + { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, + { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, + { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, + { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, + { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, + { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, + { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cd/1a8633802d766a0fa46f382a77e096d7e209e0817892929655fe0586ae32/cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32", size = 3689163, upload-time = "2025-10-15T23:18:13.821Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" }, + { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" }, + { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" }, + { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" }, + { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" }, ] [[package]] @@ -646,7 +907,8 @@ version = "4.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "autobahn", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "autobahn", version = "24.4.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, + { name = "autobahn", version = "25.12.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux')" }, { name = "twisted", extra = ["tls"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/9d/322b605fdc03b963cf2d33943321c8f4405e8d82e698bf49d1eed1ca40c4/daphne-4.2.1.tar.gz", hash = "sha256:5f898e700a1fda7addf1541d7c328606415e96a7bd768405f0463c312fcb31b3", size = 45600, upload-time = "2025-07-02T12:57:04.935Z" } @@ -654,6 +916,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/34/6171ab34715ed210bcd6c2b38839cc792993cff4fe2493f50bc92b0086a0/daphne-4.2.1-py3-none-any.whl", hash = "sha256:881e96b387b95b35ad85acd855f229d7f5b79073d6649089c8a33f661885e055", size = 29015, upload-time = "2025-07-02T12:57:03.793Z" }, ] +[[package]] +name = "dataclasses-json" +version = "0.6.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "marshmallow", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-inspect", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227, upload-time = "2024-06-09T16:20:19.103Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686, upload-time = "2024-06-09T16:20:16.715Z" }, +] + [[package]] name = "dateparser" version = "1.2.2" @@ -671,14 +946,14 @@ wheels = [ [[package]] name = "deprecated" -version = "1.2.18" +version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wrapt", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744, upload-time = "2025-01-27T10:46:25.7Z" } +sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998, upload-time = "2025-01-27T10:46:09.186Z" }, + { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, ] [[package]] @@ -693,6 +968,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178, upload-time = "2020-04-20T14:23:36.581Z" }, ] +[[package]] +name = "dirtyjson" +version = "1.0.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/04/d24f6e645ad82ba0ef092fa17d9ef7a21953781663648a01c9371d9e8e98/dirtyjson-1.0.8.tar.gz", hash = "sha256:90ca4a18f3ff30ce849d100dcf4a003953c79d3a2348ef056f1d9c22231a25fd", size = 30782, upload-time = "2022-11-28T23:32:33.319Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/69/1bcf70f81de1b4a9f21b3a62ec0c83bdff991c88d6cc2267d02408457e88/dirtyjson-1.0.8-py3-none-any.whl", hash = "sha256:125e27248435a58acace26d5c2c4c11a1c0de0a9c5124c5a94ba78e517d74f53", size = 25197, upload-time = "2022-11-28T23:32:31.219Z" }, +] + [[package]] name = "distlib" version = "0.4.0" @@ -702,28 +986,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, ] +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + [[package]] name = "django" -version = "5.2.7" +version = "5.2.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "sqlparse", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/96/bd84e2bb997994de8bcda47ae4560991084e86536541d7214393880f01a8/django-5.2.7.tar.gz", hash = "sha256:e0f6f12e2551b1716a95a63a1366ca91bbcd7be059862c1b18f989b1da356cdd", size = 10865812, upload-time = "2025-10-01T14:22:12.081Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/e5/2671df24bf0ded831768ef79532e5a7922485411a5696f6d979568591a37/django-5.2.10.tar.gz", hash = "sha256:74df100784c288c50a2b5cad59631d71214f40f72051d5af3fdf220c20bdbbbe", size = 10880754, upload-time = "2026-01-06T18:55:26.817Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/ef/81f3372b5dd35d8d354321155d1a38894b2b766f576d0abffac4d8ae78d9/django-5.2.7-py3-none-any.whl", hash = "sha256:59a13a6515f787dec9d97a0438cd2efac78c8aca1c80025244b0fe507fe0754b", size = 8307145, upload-time = "2025-10-01T14:22:49.476Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/f1a7cd896daec85832136ab509d9b2a6daed4939dbe26313af3e95fc5f5e/django-5.2.10-py3-none-any.whl", hash = "sha256:cf85067a64250c95d5f9067b056c5eaa80591929f7e16fbcd997746e40d6c45c", size = 8290820, upload-time = "2026-01-06T18:55:20.009Z" }, ] [[package]] name = "django-allauth" -version = "65.12.1" +version = "65.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/94/75d7f8c59e061d1b66a6d917b287817fe02d2671c9e6376a4ddfb3954989/django_allauth-65.12.1.tar.gz", hash = "sha256:662666ff2d5c71766f66b1629ac7345c30796813221184e13e11ed7460940c6a", size = 1967971, upload-time = "2025-10-16T16:39:58.342Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/42a048ba1dedbb6b553f5376a6126b1c753c10c70d1edab8f94c560c8066/django_allauth-65.13.1.tar.gz", hash = "sha256:2af0d07812f8c1a8e3732feaabe6a9db5ecf3fad6b45b6a0f7fd825f656c5a15", size = 1983857, upload-time = "2025-11-20T16:34:40.811Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/98/9d44ae1468abfdb521d651fb67f914165c7812dfdd97be16190c9b1cc246/django_allauth-65.13.1-py3-none-any.whl", hash = "sha256:2887294beedfd108b4b52ebd182e0ed373deaeb927fc5a22f77bbde3174704a6", size = 1787349, upload-time = "2025-11-20T16:34:37.354Z" }, +] [package.optional-dependencies] mfa = [ @@ -738,15 +1034,15 @@ socialaccount = [ [[package]] name = "django-auditlog" -version = "3.3.0" +version = "3.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/37/d8/ddd1c653ffb7ed1984596420982e32a0b163a0be316721a801a54dcbf016/django_auditlog-3.3.0.tar.gz", hash = "sha256:01331a0e7bb1a8ff7573311b486c88f3d0c431c388f5a1e4a9b6b26911dd79b8", size = 85941, upload-time = "2025-10-02T17:16:27.591Z" } +sdist = { url = "https://files.pythonhosted.org/packages/70/e5/2beb2b256775c4fc041ed60cb44f5d77acb6cde307f01567dcf2756721a7/django_auditlog-3.4.1.tar.gz", hash = "sha256:ad07b9db452d5fa8303822cccd78cd3fcb2c2863aeb6abe039ec45739b4d7e33", size = 91611, upload-time = "2025-12-18T08:56:35.378Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/bc/6e1b503d1755ab09cff6480cb088def073f1303165ab59b1a09247a2e756/django_auditlog-3.3.0-py3-none-any.whl", hash = "sha256:ab0f0f556a7107ac01c8fa87137bdfbb2b6f0debf70f7753169d9a40673d2636", size = 39676, upload-time = "2025-10-02T17:15:42.922Z" }, + { url = "https://files.pythonhosted.org/packages/91/37/3239143dddb34a3f64582c43f56587a80c9ac136cbd34fc215077f83beb9/django_auditlog-3.4.1-py3-none-any.whl", hash = "sha256:29958ecacfee00144127214f3ccef3f0c203c3659bcb6dd404a0f3d5551a10a5", size = 49541, upload-time = "2025-12-18T08:56:23.483Z" }, ] [[package]] @@ -867,19 +1163,19 @@ wheels = [ [[package]] name = "django-soft-delete" -version = "1.0.21" +version = "1.0.22" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/bf/13996c18bffee3bbcf294830c1737bfb5564164b8319c51e6714b6bdf783/django_soft_delete-1.0.21.tar.gz", hash = "sha256:542bd4650d2769105a4363ea7bb7fbdb3c28429dbaa66417160f8f4b5dc689d5", size = 21153, upload-time = "2025-09-17T08:46:30.476Z" } +sdist = { url = "https://files.pythonhosted.org/packages/98/d1/c990b731676f93bd4594dee4b5133df52f5d0eee1eb8a969b4030014ac54/django_soft_delete-1.0.22.tar.gz", hash = "sha256:32d0bb95f180c28a40163e78a558acc18901fd56011f91f8ee735c171a6d4244", size = 21982, upload-time = "2025-10-25T13:11:46.199Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/e6/8f4fed14499c63e35ca33cf9f424ad2e14e963ec5545594d7c7dc2f710f4/django_soft_delete-1.0.21-py3-none-any.whl", hash = "sha256:dd91e671d9d431ff96f4db727ce03e7fbb4008ae4541b1d162d5d06cc9becd2a", size = 18681, upload-time = "2025-09-17T08:46:29.272Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c2/fca2bf69b7ca7e18aed9ac059e89f1043663e207a514e8fb652450e49631/django_soft_delete-1.0.22-py3-none-any.whl", hash = "sha256:81973c541d21452d249151085d617ebbfb5ec463899f47cd6b1306677481e94c", size = 19221, upload-time = "2025-10-25T13:11:44.755Z" }, ] [[package]] name = "django-stubs" -version = "5.2.5" +version = "5.2.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -888,9 +1184,9 @@ dependencies = [ { name = "types-pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/8e/286150f593481c33f54d14efb58d72178f159d57d31043529d38bbc98e2f/django_stubs-5.2.5.tar.gz", hash = "sha256:fc78384e28d8c5292d60983075a5934f644f7c304c25ae2793fc57aa66d5018b", size = 247794, upload-time = "2025-09-12T19:29:49.636Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/01/86c921e0e19c9fa7e705bf795998dbf55eb183e7be0342a3027dc1bcbc9f/django_stubs-5.2.9.tar.gz", hash = "sha256:c192257120b08785cfe6f2f1c91f1797aceae8e9daa689c336e52c91e8f6a493", size = 257970, upload-time = "2026-01-20T23:59:27.018Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/02/cdbf7652ef2c9a7a1fed7c279484b7f3806f15b1bb34aec9fef8e8cfacbf/django_stubs-5.2.5-py3-none-any.whl", hash = "sha256:223c1a3324cd4873b7629dec6e9adbe224a94508284c1926b25fddff7a92252b", size = 490196, upload-time = "2025-09-12T19:29:47.954Z" }, + { url = "https://files.pythonhosted.org/packages/0d/05/4c9c419b7051eb4b350100b086be6df487f968ab672d3d370f8ccf7c3746/django_stubs-5.2.9-py3-none-any.whl", hash = "sha256:2317a7130afdaa76f6ff7f623650d7f3bf1b6c86a60f95840e14e6ec6de1a7cd", size = 508656, upload-time = "2026-01-20T23:59:25.12Z" }, ] [package.optional-dependencies] @@ -900,24 +1196,24 @@ compatible-mypy = [ [[package]] name = "django-stubs-ext" -version = "5.2.5" +version = "5.2.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/16/94/c9b8f4c47084a0fa666da9066c36771098101932688adf2c17a40fab79c2/django_stubs_ext-5.2.5.tar.gz", hash = "sha256:ecc628df29d36cede638567c4e33ff485dd7a99f1552ad0cece8c60e9c3a8872", size = 6489, upload-time = "2025-09-12T19:29:06.008Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/03/9c2be939490d2282328db4611bc5956899f5ff7eabc3e88bd4b964a87373/django_stubs_ext-5.2.9.tar.gz", hash = "sha256:6db4054d1580657b979b7d391474719f1a978773e66c7070a5e246cd445a25a9", size = 6497, upload-time = "2026-01-20T23:58:59.462Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/fe/a85a105fddffadb4a8d50e500caeee87d836b679d51a19d52dfa0cc6c660/django_stubs_ext-5.2.5-py3-none-any.whl", hash = "sha256:9b4b8ac9d32f7e6c304fd05477f8688fae6ed57f6a0f9f4d074f9e55b5a3da14", size = 9310, upload-time = "2025-09-12T19:29:04.62Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f7/0d5f7d7e76fe972d9f560f687fdc0cab4db9e1624fd90728ca29b4ed7a63/django_stubs_ext-5.2.9-py3-none-any.whl", hash = "sha256:230c51575551b0165be40177f0f6805f1e3ebf799b835c85f5d64c371ca6cf71", size = 9974, upload-time = "2026-01-20T23:58:58.438Z" }, ] [[package]] name = "django-treenode" -version = "0.23.2" +version = "0.23.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/75/f3/274b84607fd64c0844e98659985f964190a46c2460f2523a446c4a946216/django_treenode-0.23.2.tar.gz", hash = "sha256:3c5a6ff5e0c83e34da88749f602b3013dd1ab0527f51952c616e3c21bf265d52", size = 26700, upload-time = "2025-09-04T21:16:53.497Z" } +sdist = { url = "https://files.pythonhosted.org/packages/25/58/86edbbd1075bb8bc0962c6feb13bc06822405a10fea8352ad73ab2babdd9/django_treenode-0.23.3.tar.gz", hash = "sha256:714c825d5b925a3d2848d0709f29973941ea41a606b8e2b64cbec46010a8cce3", size = 27812, upload-time = "2025-12-01T23:01:24.847Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/61/e17d3dee5c6bb24b8faf0c101e17f9a8cafeba6384166176e066c80e8cbb/django_treenode-0.23.2-py3-none-any.whl", hash = "sha256:9363cb50f753654a9acfad6ec4df2a664a5f89dfdf8b55ffd964f27461bef85e", size = 21879, upload-time = "2025-09-04T21:16:51.811Z" }, + { url = "https://files.pythonhosted.org/packages/bc/52/696db237167483324ef38d8d090fb0fcc33dbb70ebe66c75868005fb7c75/django_treenode-0.23.3-py3-none-any.whl", hash = "sha256:8072e1ac688c1ed3ab95a98a797c5e965380de5228a389d60a4ef8b9a6449387", size = 22014, upload-time = "2025-12-01T23:01:23.266Z" }, ] [[package]] @@ -948,18 +1244,17 @@ wheels = [ [[package]] name = "djangorestframework-stubs" -version = "3.16.4" +version = "3.16.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django-stubs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "types-pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "types-requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/77/33/79c0dd2a02ead2702a1b3d25579b56ffdc1bc6d1271c31a0979ce9ad10fa/djangorestframework_stubs-3.16.4.tar.gz", hash = "sha256:f43136bfbef568dd0e10848427c01bd9ef759dd328195949f6f7f9a2292a34f6", size = 31960, upload-time = "2025-09-29T20:11:20.57Z" } +sdist = { url = "https://files.pythonhosted.org/packages/35/50/889b1121dc0831aa9f6ece8409d41a5f4667da2a963172516841f343fd35/djangorestframework_stubs-3.16.7.tar.gz", hash = "sha256:e53bc346e9950ebdd1bb2bbc19d7e5c8b7acc894e381df55da69248f47ab78ff", size = 32296, upload-time = "2026-01-13T11:42:48.3Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/45/63fac5c34313986acc005529fdba638822bae973f2b61a0542a6c8494847/djangorestframework_stubs-3.16.4-py3-none-any.whl", hash = "sha256:3b27353fa797876f55da87eceafe4c2f265a93924fa7763d257e509d865df1b2", size = 56503, upload-time = "2025-09-29T20:11:19.317Z" }, + { url = "https://files.pythonhosted.org/packages/9e/99/7c969728d66388e22fdaba94e1a9c56490954e2f12f598416e380a53b26d/djangorestframework_stubs-3.16.7-py3-none-any.whl", hash = "sha256:70f80050144875f80ce8ac823ff8628f6e3eb7336495394bb9803251721d9358", size = 56522, upload-time = "2026-01-13T11:42:46.118Z" }, ] [package.optional-dependencies] @@ -970,7 +1265,7 @@ compatible-mypy = [ [[package]] name = "drf-spectacular" -version = "0.28.0" +version = "0.29.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -980,9 +1275,9 @@ dependencies = [ { name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "uritemplate", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/b9/741056455aed00fa51a1df41fad5ad27c8e0d433b6bf490d4e60e2808bc6/drf_spectacular-0.28.0.tar.gz", hash = "sha256:2c778a47a40ab2f5078a7c42e82baba07397bb35b074ae4680721b2805943061", size = 237849, upload-time = "2024-11-30T08:49:02.355Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/0e/a4f50d83e76cbe797eda88fc0083c8ca970cfa362b5586359ef06ec6f70a/drf_spectacular-0.29.0.tar.gz", hash = "sha256:0a069339ea390ce7f14a75e8b5af4a0860a46e833fd4af027411a3e94fc1a0cc", size = 241722, upload-time = "2025-11-02T03:40:26.348Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/66/c2929871393b1515c3767a670ff7d980a6882964a31a4ca2680b30d7212a/drf_spectacular-0.28.0-py3-none-any.whl", hash = "sha256:856e7edf1056e49a4245e87a61e8da4baff46c83dbc25be1da2df77f354c7cb4", size = 103928, upload-time = "2024-11-30T08:48:57.288Z" }, + { url = "https://files.pythonhosted.org/packages/32/d9/502c56fc3ca960075d00956283f1c44e8cafe433dada03f9ed2821f3073b/drf_spectacular-0.29.0-py3-none-any.whl", hash = "sha256:d1ee7c9535d89848affb4427347f7c4a22c5d22530b8842ef133d7b72e19b41a", size = 105433, upload-time = "2025-11-02T03:40:24.823Z" }, ] [[package]] @@ -1007,23 +1302,23 @@ wheels = [ [[package]] name = "exceptiongroup" -version = "1.3.0" +version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, + { name = "typing-extensions", marker = "(python_full_version < '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.13' and sys_platform == 'darwin')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] [[package]] name = "execnet" -version = "2.1.1" +version = "2.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bb/ff/b4c0dc78fbe20c3e59c0c7334de0c27eb4001a2b2017999af398bf730817/execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3", size = 166524, upload-time = "2024-04-08T09:04:19.245Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", size = 40612, upload-time = "2024-04-08T09:04:17.414Z" }, + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, ] [[package]] @@ -1039,36 +1334,60 @@ wheels = [ ] [[package]] -name = "faker" -version = "37.8.0" +name = "faiss-cpu" +version = "1.13.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "tzdata", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, + { name = "numpy", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux')" }, + { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3a/da/1336008d39e5d4076dddb4e0f3a52ada41429274bf558a3cc28030d324a3/faker-37.8.0.tar.gz", hash = "sha256:090bb5abbec2b30949a95ce1ba6b20d1d0ed222883d63483a0d4be4a970d6fb8", size = 1912113, upload-time = "2025-09-15T20:24:13.592Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/11/02ebebb09ff2104b690457cb7bc6ed700c9e0ce88cf581486bb0a5d3c88b/faker-37.8.0-py3-none-any.whl", hash = "sha256:b08233118824423b5fc239f7dd51f145e7018082b4164f8da6a9994e1f1ae793", size = 1953940, upload-time = "2025-09-15T20:24:11.482Z" }, + { url = "https://files.pythonhosted.org/packages/07/c9/671f66f6b31ec48e5825d36435f0cb91189fa8bb6b50724029dbff4ca83c/faiss_cpu-1.13.2-cp310-abi3-macosx_14_0_arm64.whl", hash = "sha256:a9064eb34f8f64438dd5b95c8f03a780b1a3f0b99c46eeacb1f0b5d15fc02dc1", size = 3452776, upload-time = "2025-12-24T10:27:01.419Z" }, + { url = "https://files.pythonhosted.org/packages/5a/4a/97150aa1582fb9c2bca95bd8fc37f27d3b470acec6f0a6833844b21e4b40/faiss_cpu-1.13.2-cp310-abi3-macosx_14_0_x86_64.whl", hash = "sha256:c8d097884521e1ecaea6467aeebbf1aa56ee4a36350b48b2ca6b39366565c317", size = 7896434, upload-time = "2025-12-24T10:27:03.592Z" }, + { url = "https://files.pythonhosted.org/packages/0b/d0/0940575f059591ca31b63a881058adb16a387020af1709dcb7669460115c/faiss_cpu-1.13.2-cp310-abi3-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ee330a284042c2480f2e90450a10378fd95655d62220159b1408f59ee83ebf1", size = 11485825, upload-time = "2025-12-24T10:27:05.681Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e1/a5acac02aa593809f0123539afe7b4aff61d1db149e7093239888c9053e1/faiss_cpu-1.13.2-cp310-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ab88ee287c25a119213153d033f7dd64c3ccec466ace267395872f554b648cd7", size = 23845772, upload-time = "2025-12-24T10:27:08.194Z" }, + { url = "https://files.pythonhosted.org/packages/9c/7b/49dcaf354834ec457e85ca769d50bc9b5f3003fab7c94a9dcf08cf742793/faiss_cpu-1.13.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:85511129b34f890d19c98b82a0cd5ffb27d89d1cec2ee41d2621ee9f9ef8cf3f", size = 13477567, upload-time = "2025-12-24T10:27:10.822Z" }, + { url = "https://files.pythonhosted.org/packages/f7/6b/12bb4037921c38bb2c0b4cfc213ca7e04bbbebbfea89b0b5746248ce446e/faiss_cpu-1.13.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b32eb4065bac352b52a9f5ae07223567fab0a976c7d05017c01c45a1c24264f", size = 25102239, upload-time = "2025-12-24T10:27:13.476Z" }, +] + +[[package]] +name = "faker" +version = "40.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/77/1c3ff07b6739b9a1d23ca01ec0a90a309a33b78e345a3eb52f9ce9240e36/faker-40.1.2.tar.gz", hash = "sha256:b76a68163aa5f171d260fc24827a8349bc1db672f6a665359e8d0095e8135d30", size = 1949802, upload-time = "2026-01-13T20:51:49.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/ec/91a434c8a53d40c3598966621dea9c50512bec6ce8e76fa1751015e74cef/faker-40.1.2-py3-none-any.whl", hash = "sha256:93503165c165d330260e4379fd6dc07c94da90c611ed3191a0174d2ab9966a42", size = 1985633, upload-time = "2026-01-13T20:51:47.982Z" }, ] [[package]] name = "fido2" -version = "1.2.0" +version = "2.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/cc/4529123364d41f342145f2fd775307eaed817cd22810895dea10e15a4d06/fido2-1.2.0.tar.gz", hash = "sha256:e39f95920122d64283fda5e5581d95a206e704fa42846bfa4662f86aa0d3333b", size = 266369, upload-time = "2024-11-27T09:08:21.071Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/3c/c65377e48c144afca6b02c69f10c0fe936db556096a4e2c9798e2aa72db6/fido2-2.1.1.tar.gz", hash = "sha256:f1379f845870cc7fc64c7f07323c3ce41e8c96c37054e79e0acd5630b3fec5ac", size = 4455940, upload-time = "2026-01-19T11:08:34.683Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/48/e9b99d66f27d3416a619324568739fd6603e093b2f79138d6f47ccf727b6/fido2-1.2.0-py3-none-any.whl", hash = "sha256:f7c8ee62e359aa980a45773f9493965bb29ede1b237a9218169dbfe60c80e130", size = 219418, upload-time = "2024-11-27T09:08:18.932Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ab/d0fa89cc4b982800dd88daa799612f11642bf9393851715d9eaeba3cfcac/fido2-2.1.1-py3-none-any.whl", hash = "sha256:f85c16c8084abf6530b6c6ec3a0cf8575943321842e06916686943a8b784182c", size = 226945, upload-time = "2026-01-19T11:08:29.675Z" }, ] [[package]] name = "filelock" -version = "3.20.0" +version = "3.20.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, + { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, +] + +[[package]] +name = "filetype" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/29/745f7d30d47fe0f251d3ad3dc2978a23141917661998763bebb6da007eb1/filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb", size = 998020, upload-time = "2022-11-02T17:34:04.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970, upload-time = "2022-11-02T17:34:01.425Z" }, ] [[package]] @@ -1087,6 +1406,115 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a6/ff/ee2f67c0ff146ec98b5df1df637b2bc2d17beeb05df9f427a67bd7a7d79c/flower-2.0.1-py2.py3-none-any.whl", hash = "sha256:9db2c621eeefbc844c8dd88be64aef61e84e2deb29b271e02ab2b5b9f01068e2", size = 383553, upload-time = "2023-08-13T14:37:41.552Z" }, ] +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", size = 86230, upload-time = "2025-10-06T05:35:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", size = 49621, upload-time = "2025-10-06T05:35:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", size = 49889, upload-time = "2025-10-06T05:35:26.797Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", size = 219464, upload-time = "2025-10-06T05:35:28.254Z" }, + { url = "https://files.pythonhosted.org/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", size = 221649, upload-time = "2025-10-06T05:35:29.454Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", size = 219188, upload-time = "2025-10-06T05:35:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", size = 231748, upload-time = "2025-10-06T05:35:32.101Z" }, + { url = "https://files.pythonhosted.org/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", size = 236351, upload-time = "2025-10-06T05:35:33.834Z" }, + { url = "https://files.pythonhosted.org/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", size = 218767, upload-time = "2025-10-06T05:35:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", size = 235887, upload-time = "2025-10-06T05:35:36.354Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785, upload-time = "2025-10-06T05:35:37.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312, upload-time = "2025-10-06T05:35:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650, upload-time = "2025-10-06T05:35:40.377Z" }, + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/7d/5df2650c57d47c57232af5ef4b4fdbff182070421e405e0d62c6cdbfaa87/fsspec-2026.1.0.tar.gz", hash = "sha256:e987cb0496a0d81bba3a9d1cee62922fb395e7d4c3b575e57f547953334fe07b", size = 310496, upload-time = "2026-01-09T15:21:35.562Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/c9/97cc5aae1648dcb851958a3ddf73ccd7dbe5650d95203ecb4d7720b4cdbf/fsspec-2026.1.0-py3-none-any.whl", hash = "sha256:cb76aa913c2285a3b49bdd5fc55b1d7c708d7208126b60f2eb8194fe1b4cbdcc", size = 201838, upload-time = "2026-01-09T15:21:34.041Z" }, +] + [[package]] name = "ghp-import" version = "2.1.0" @@ -1101,99 +1529,96 @@ wheels = [ [[package]] name = "gotenberg-client" -version = "0.12.0" +version = "0.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx", extra = ["http2"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/6d/07ea213c146bbe91dffebff2d8f4dc61e7076d3dd34d4fd1467f9163e752/gotenberg_client-0.12.0.tar.gz", hash = "sha256:1ab50878024469fc003c414ee9810ceeb00d4d7d7c36bd2fb75318fbff139e9b", size = 1210884, upload-time = "2025-10-15T15:32:37.669Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/6c/aaadd6657ca42fbd148b1c00604b98c1ead5a22552f4e5365ce5f0632430/gotenberg_client-0.13.1.tar.gz", hash = "sha256:cdd6bbb535cd739b87446cd1b4f6347ed7f9af6a0d4b19baf7c064b75528ee54", size = 1211143, upload-time = "2025-12-04T20:45:24.151Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/39/fcb24ff053b1be7e5124f56c3d358706a23a328f685c6db33bc9dbc5472d/gotenberg_client-0.12.0-py3-none-any.whl", hash = "sha256:a540b35ac518e902c2860a88fbe448c15fe5a56fe8ec8604e6a2c8c2228fd0cb", size = 51051, upload-time = "2025-10-15T15:32:36.32Z" }, + { url = "https://files.pythonhosted.org/packages/79/f6/7a6e6785295332d2538f729ae19516cef712273a5ab8b90d015f08e37a45/gotenberg_client-0.13.1-py3-none-any.whl", hash = "sha256:613f7083a5e8a81699dd8d715c97e5806a424ac48920aad25d7c11b600cdfaf3", size = 51058, upload-time = "2025-12-04T20:45:22.603Z" }, ] [[package]] name = "granian" -version = "2.5.4" +version = "2.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/78/9b/6ac903de211e5874824e7349387c9e0467459dc1ad0cd960cb4196f38ae6/granian-2.5.4.tar.gz", hash = "sha256:85989a08052f1bbb174fd73759e1ae505e50b4c0690af366ca6ba844203dd463", size = 112016, upload-time = "2025-09-18T11:52:16.004Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/22/93016f4f9e9115ba981f51fc17c7c369a34772f11a93043320a2a3d5c6ea/granian-2.6.1.tar.gz", hash = "sha256:d209065b12f18b6d7e78f1c16ff9444e5367dddeb41e3225c2cf024762740590", size = 115480, upload-time = "2026-01-07T11:08:55.927Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/a0/b6782563716dfd178f094fe7fe6d28fc6c13857926bb9efac6ddc73dec54/granian-2.5.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:907d17f94a039b1047a82386b4979a6a7db7f4c37598225c6184a2b89f0ae12d", size = 2860831, upload-time = "2025-09-18T11:49:41.521Z" }, - { url = "https://files.pythonhosted.org/packages/a9/a5/6ae10379f21415255dd36b4d26a69a0a8ec80d4ba4fe26ca563e46a1ca62/granian-2.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a009e99d3c4a2a70a15a97391566753045a81641e5a3e651ff346d8bb7fe7450", size = 2550345, upload-time = "2025-09-18T11:49:43.015Z" }, - { url = "https://files.pythonhosted.org/packages/7c/ca/1cdbd669ee4bf85208b96e0bcaf5b51cba67907b71679c18a1da6bea61e6/granian-2.5.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cb602ac3ea567476c339e8683a0fa2ffe7fd8432798bd63c371d5b32502bdb9", size = 3048013, upload-time = "2025-09-18T11:49:44.538Z" }, - { url = "https://files.pythonhosted.org/packages/cf/9e/aba367c3c372d641e78aaaaa4ec8a4452bb8a2259bdb8b7484d537969864/granian-2.5.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52aee85459304f1e74ff4cb5bb60d23db267b671b1199ff589b1a5a65f5638f", size = 2862464, upload-time = "2025-09-18T11:49:45.976Z" }, - { url = "https://files.pythonhosted.org/packages/3e/aa/542ef36aee53a21ff868a38c4d567eb253b1338501091e36b6ad8090c862/granian-2.5.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8478d777971d6c1601b479c70a5c1aaaba7b656fa5044b1c38b4ba5e172f0fc7", size = 3147423, upload-time = "2025-09-18T11:49:47.266Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a3/1ba8d0d534ab993e1f84eab3320b4e3071e9bec166131e663c60160e5192/granian-2.5.4-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:1da588e951b3e0bce94f2743158750c9733efcbe5c27b31f50e9bda6af8aac1f", size = 2914051, upload-time = "2025-09-18T11:49:48.616Z" }, - { url = "https://files.pythonhosted.org/packages/4b/1e/0e43ee8a4a97c4b2a413964448917cabe154aabc99be46ae0f487ce094e8/granian-2.5.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:79db7d0eac7445a383e22b6d3e9323882bc9a9c1d2fd62097c0452822c4de526", size = 2919482, upload-time = "2025-09-18T11:49:49.789Z" }, - { url = "https://files.pythonhosted.org/packages/c7/74/6857f59e1ae9a556b7293c60258963063d74ad154c0411f94dc235aa02ab/granian-2.5.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:cc75f15876415054c094e9ef941cf49c315ee5f0f20701fdfb3ffc698054c727", size = 3157058, upload-time = "2025-09-18T11:49:51.406Z" }, - { url = "https://files.pythonhosted.org/packages/02/e9/05eaa62200693b31a75e3767f5716a55aeb07572f1a41e2d31b6f8bb115d/granian-2.5.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2caeee9d12144c9c285d3074c7979cdf1ad3d84a86204dec9035ca6cec5d713f", size = 3194238, upload-time = "2025-09-18T11:49:52.575Z" }, - { url = "https://files.pythonhosted.org/packages/a9/a3/89471ae2ff6d3111964ef9e0b8ac00c5a68046aca93965048b94ec0ec952/granian-2.5.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a404bff75dc29c01566a4e896237f6cb8eda49a71b90770b8316ebe1b08a3d46", size = 2861005, upload-time = "2025-09-18T11:49:55.442Z" }, - { url = "https://files.pythonhosted.org/packages/85/90/81706bbe0f23737c3c1cf8a4e76a6e2c9ec9c5a950a023aea2aed6ef74c4/granian-2.5.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d91b4642283ea8169aad64b74b242c364a3ce24d6aeed9b5a4358f99a5ab4d84", size = 2550393, upload-time = "2025-09-18T11:49:57.24Z" }, - { url = "https://files.pythonhosted.org/packages/66/67/7bdd9b1b63c811439ac7b8b4e52112abb5a38b575f033b9c7672d0355a70/granian-2.5.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aa6b4ad4d479fe3e7d42ca4321ae7febad9cdae5c269032234b8b4ac8dbd017", size = 3048134, upload-time = "2025-09-18T11:49:59.263Z" }, - { url = "https://files.pythonhosted.org/packages/39/94/9bd7e8248c438ac7861653ea4fb071a2ed2dcb4b7a1ec5cf034282d4079a/granian-2.5.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2466523c14724d2d68497cd081ffd2aa913381be199e7eb71347847a3651224c", size = 2862817, upload-time = "2025-09-18T11:50:00.932Z" }, - { url = "https://files.pythonhosted.org/packages/c1/f7/c98302718f58a3ee47ab1db83e8c0834b91016fc3c83f3a23f7b256a02a7/granian-2.5.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce9ec6baebb83ba7d1ed507dc7d301f7f29725f9b7a8c9c974f96479dea3a090", size = 3147500, upload-time = "2025-09-18T11:50:02.569Z" }, - { url = "https://files.pythonhosted.org/packages/4d/c0/eb3b94d2eb40d5b94bfad94da018f5daf539e09e8fc33742a8330707913a/granian-2.5.4-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:8b3faa2eec6dbbb072aae575d6a6a5e5577aef13c93d38d454a6a9fffc954ce7", size = 2914076, upload-time = "2025-09-18T11:50:04.074Z" }, - { url = "https://files.pythonhosted.org/packages/0f/77/42e06595c441c14f934858351acacc28fb2552798ab7eefed2e0e3920d15/granian-2.5.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:25a1d03fc93184009d5e76a5bfb5b29222e7debacfc225dd5d3732f6f6f99c10", size = 2919154, upload-time = "2025-09-18T11:50:05.497Z" }, - { url = "https://files.pythonhosted.org/packages/c4/af/1f87d4bfabf09d16dcf3a355cc356daa33185c561a5f3c5904f6fd0f0e5f/granian-2.5.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:1e580f5fa30ed04e724c71de099dcacc4722613f8a53e41454bac86242887da7", size = 3157119, upload-time = "2025-09-18T11:50:07.088Z" }, - { url = "https://files.pythonhosted.org/packages/ab/34/18b038e4b67a97eb776bf84307a0d5c8bff79290024973def614d8052596/granian-2.5.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5a4e74bf6d91dd7df6ffc7edb74e74147057fc947c04684d2d9af03e5e71ad71", size = 3193906, upload-time = "2025-09-18T11:50:08.305Z" }, - { url = "https://files.pythonhosted.org/packages/ea/9f/2a419461f2696bd95ba6b4d2a1c09b7372b79e66ac3b7dd4a985bf35f7d6/granian-2.5.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:c4387cca4e38ec7579cac71a2da27fd177ced682b1de8bf917b9610f2ac0ba5e", size = 2846208, upload-time = "2025-09-18T11:50:10.934Z" }, - { url = "https://files.pythonhosted.org/packages/0a/bd/f9b9f57e14f778665e5b56a5b98d20187136517188a39ac404b13812bb34/granian-2.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a126b75927583292a9d2cfae627cd4b1da3e68d04dd87ba5a53b4860768d9e04", size = 2537995, upload-time = "2025-09-18T11:50:12.461Z" }, - { url = "https://files.pythonhosted.org/packages/55/eb/4df4fd10fb0ca0aa7ccbbe6b805e8019dc83d3a7861a8e0ec73a4f671bcf/granian-2.5.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b44dc391bf9bc1303bcb2cb344bbb5c35de92f43a3e8584f2a984dfda2fea8e3", size = 3033917, upload-time = "2025-09-18T11:50:16.535Z" }, - { url = "https://files.pythonhosted.org/packages/1a/ad/a3b8a773ee347f1a9b52d37caeb373eff590363c478cd6d9d20422a842de/granian-2.5.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07c47847163a1bcce0b7c323093b20be8a8ec9d4f4eba596b4d27f85ddbe669f", size = 2860524, upload-time = "2025-09-18T11:50:17.811Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ae/188342234ed4f842ad63fd6a0328a05a8e2b991293496527b30654b6710c/granian-2.5.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6c50539f654ce5f8fadd9b360fac0361d812c39c7a5f1e525889c51899a10f0", size = 3139768, upload-time = "2025-09-18T11:50:19.006Z" }, - { url = "https://files.pythonhosted.org/packages/28/40/babaaf6b95bf690cf3af1ff0c3a1d9c33b23f2c18dabb373c805653834bd/granian-2.5.4-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e52f65acd4da0a3af7a5d2d6a6445d530e84fe52057ee39f52ce92a6598fe37b", size = 2915038, upload-time = "2025-09-18T11:50:20.194Z" }, - { url = "https://files.pythonhosted.org/packages/a2/01/3d2eda00cbaa09f5a734d57fb4f52f68a1a48137a262e46d59ffecb54bd6/granian-2.5.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b78ab23495e384521085c33fecb3987779e1b1e43f34acd5b25e864b699933f9", size = 2915368, upload-time = "2025-09-18T11:50:21.335Z" }, - { url = "https://files.pythonhosted.org/packages/8a/3c/51670c8d83334ea1ce1b54aae93f6066ff101ca81ccd6ab01832309b2156/granian-2.5.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:6a477b204fca30218b3cc16721df38f1e159c5ee27252b305c78982af1995974", size = 3142893, upload-time = "2025-09-18T11:50:22.843Z" }, - { url = "https://files.pythonhosted.org/packages/68/42/81f848d9cf6cd77f24d0625d5c49caf6471477c97a760827d41cbb90a214/granian-2.5.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7f58116ab1300ca744a861482ce3194e9be5f1adad9ac4adda89d47b1ba3fa50", size = 3206519, upload-time = "2025-09-18T11:50:24.124Z" }, - { url = "https://files.pythonhosted.org/packages/50/aa/ed7cee53c0663fbb4d64b9a143d176f76000100f8a5ccef8a166df2bb9a9/granian-2.5.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:533bf842b56c8531705048211e3152fb1234d7611f83257a71cbf7e734c0f4a1", size = 2845936, upload-time = "2025-09-18T11:50:27.394Z" }, - { url = "https://files.pythonhosted.org/packages/04/a3/a63b04b67fb44578ce5d2d6c2b669932d6adb9a4844d86dc0f0cd0adb409/granian-2.5.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1efb111f84236a72d5864d64a0198e04e699014119c33d957fac34a0efb2474", size = 2537660, upload-time = "2025-09-18T11:50:28.953Z" }, - { url = "https://files.pythonhosted.org/packages/a6/99/4c630712f95ce6105f070631242fbafe4c045a00cd5e00f437a03085a9cc/granian-2.5.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b0341a553fe913b4a741c10f532f5315d57deaa34877494d4c4b09c666f5266c", size = 3033766, upload-time = "2025-09-18T11:50:30.515Z" }, - { url = "https://files.pythonhosted.org/packages/44/73/d05f0cd49764feedfc91b418e060c212a35077154209e22654891c08d2dc/granian-2.5.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8b3b24b7620df45752bbf34f93250f733a6996a7409879efbea6ab38f57eff69", size = 2860740, upload-time = "2025-09-18T11:50:31.799Z" }, - { url = "https://files.pythonhosted.org/packages/6e/85/061748715e5c213c8f5e58c9a7f95741fc5787a0c45c7b066b1df0f59453/granian-2.5.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bb902636271f9e61f6653625670982f7a0e19cbc7ae56fc65cd26bf330c612f", size = 3139932, upload-time = "2025-09-18T11:50:33.07Z" }, - { url = "https://files.pythonhosted.org/packages/7e/5b/f361708fd275763f1436bc1584bf3171ea5b6b12255e7cf3d5a2d7280546/granian-2.5.4-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:23b2e86ea97320bbe80866a94e6855752f0c73c0ec07772e0241e8409384cde5", size = 2914531, upload-time = "2025-09-18T11:50:34.364Z" }, - { url = "https://files.pythonhosted.org/packages/0d/84/9f1dbcb6a2228fd9bbf548d81f17225ae26dbbcef33f61d7e14971d78d2f/granian-2.5.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:328ed82315ccbd5dedc8f5318a1f80d49e07eb34ebc0753bc2930d2f30070a34", size = 2915108, upload-time = "2025-09-18T11:50:35.667Z" }, - { url = "https://files.pythonhosted.org/packages/77/10/13e04a2ef44f711474ca68a095e65f72fb6ced28f5c0930980a012386f8b/granian-2.5.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:9bd438bb41cbac25f5f3155924947f0e2594b69f8a5f78e43c453c35fa28a1f0", size = 3142589, upload-time = "2025-09-18T11:50:37.349Z" }, - { url = "https://files.pythonhosted.org/packages/43/a5/826dab4703261d0f500b5d1d0cc8a965157856b0266ca3679185d316d5f7/granian-2.5.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d6d1b462ccb57af3051def8eae13f1df81dc902e9deff3cc6dfbb692c40a5a1f", size = 3206072, upload-time = "2025-09-18T11:50:38.602Z" }, - { url = "https://files.pythonhosted.org/packages/d5/40/a9d9e976bfbd6274d1fe052676fb02c055c1476a9936961218a211785cef/granian-2.5.4-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:d04a1432ed98b7e4b4e5cff188819f34bd680785352237477d7886eb9305f692", size = 2763126, upload-time = "2025-09-18T11:50:41.404Z" }, - { url = "https://files.pythonhosted.org/packages/ad/84/0a302005e3c1c254c592d29aaa11a281ef4a84dfab45ccfe3072223f9c5b/granian-2.5.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c6309d729f1b022b09fd051a277096a732bd8ed39986ac4b9849f6e79b660880", size = 2479158, upload-time = "2025-09-18T11:50:42.767Z" }, - { url = "https://files.pythonhosted.org/packages/74/9f/a889cc30f2ee67acdcfccd10c3da239c828c584cb9e7f04e54717ff0c42b/granian-2.5.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a067c27e733b0851e6f66175c1aac8badda60b698457181881c08a4e17baecf", size = 3016245, upload-time = "2025-09-18T11:50:44.3Z" }, - { url = "https://files.pythonhosted.org/packages/b7/37/22a86b369b140b3684f8aecfd9c80ed2765421c09cb3d3ef06245a6aaaf8/granian-2.5.4-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:54bd0604db172a964b1bc4b8709329b7f4e9cff6b8f468104ca7603a5e61d529", size = 2795566, upload-time = "2025-09-18T11:50:45.626Z" }, - { url = "https://files.pythonhosted.org/packages/c3/cc/65073d6f08d9251ceaa5eb9a485e1a8fda9cfb4bac3c03c9f28e01abf416/granian-2.5.4-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:487bdc40b234ef84510eac1d63f0720ca92daca08feb7d2d98a1f0a84cc54b0e", size = 2909489, upload-time = "2025-09-18T11:50:48.98Z" }, - { url = "https://files.pythonhosted.org/packages/d9/76/c52fdfcd536584ed01a878e13139e338083fac4fde32d6fbfb0f3b29f380/granian-2.5.4-cp313-cp313t-musllinux_1_1_armv7l.whl", hash = "sha256:76cd55ab521cc501a977f50ace9d72a6a4f9a6849e6490b14af2e9acc614ce55", size = 3124827, upload-time = "2025-09-18T11:50:50.45Z" }, - { url = "https://files.pythonhosted.org/packages/f3/fc/4d93870feebc547bbc68dcbfa3a6c491107a6ac634636c7e332441ef9077/granian-2.5.4-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:27b3e7906916ad6e84a8b16d89517d8652bece62888cb9421597eb14767a9d92", size = 3193471, upload-time = "2025-09-18T11:50:52.493Z" }, - { url = "https://files.pythonhosted.org/packages/aa/32/d1776652352df81c420e61a1d79711c9992ba6dcd1a419b1c9df83c925ce/granian-2.5.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c237db56e5ff3fdad6539a3fbfcb9b57ce71463db55a016ba08296828043112f", size = 2827836, upload-time = "2025-09-18T11:50:55.444Z" }, - { url = "https://files.pythonhosted.org/packages/73/60/0f7b994feaca68c2585fa96338369fc8f928281cb1f7da2a377ec698f6a2/granian-2.5.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a76d7033a8d68c8353293fae8365e3b649bb155ab39af14387f3e9e870d503fb", size = 2525297, upload-time = "2025-09-18T11:50:56.765Z" }, - { url = "https://files.pythonhosted.org/packages/ff/06/73580eef85ac4ac4da7205d320e449a46db0e613b6df706a5feb46809e96/granian-2.5.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6778b9f7ecef8a423dd203aa5b0644a18d53eb749e830b2fe33abecad5d7e84", size = 3028201, upload-time = "2025-09-18T11:50:58.047Z" }, - { url = "https://files.pythonhosted.org/packages/19/78/5c0b43af33b18bd528b10ff770aaeb158a4fbeb0ac5cc0371921759b6b59/granian-2.5.4-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3accc02c981436e783772b12ea8cead35e8e644437881d7da842ff474f8e30f9", size = 2852319, upload-time = "2025-09-18T11:50:59.573Z" }, - { url = "https://files.pythonhosted.org/packages/ff/ae/ad9cc1729d12b9699318c66015999efef14c7fd17836393a09a1bbbf5185/granian-2.5.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3eaaf38851308af616ad5fdc35f333857f128493072ea208c1bb2fb557dcf2e", size = 3135211, upload-time = "2025-09-18T11:51:01.022Z" }, - { url = "https://files.pythonhosted.org/packages/7d/41/cbda436041e46116647d804d2eada3fab144883b0e6ce75f3d967a116c6c/granian-2.5.4-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:3cad64e8e41d6f3daf3e7a3eea88023aa7e64ee81450443ac9f4e6cae005079d", size = 2914810, upload-time = "2025-09-18T11:51:02.36Z" }, - { url = "https://files.pythonhosted.org/packages/9f/28/ab53dcab1d55636eca417a4263114d24872958609ceb853b84887b12cc38/granian-2.5.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4bb60b122971326d0d1baf10336c67bdecdd7adc708cf0b09bf1cde5563e8f5", size = 2914603, upload-time = "2025-09-18T11:51:03.701Z" }, - { url = "https://files.pythonhosted.org/packages/a0/33/e8810b11004e9139b6dd71dfa4a1a56ae60331e8d72dfa8bc7121158abff/granian-2.5.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:14129339f0ed9bbd2d129f82ed16e0c330edca7300482bd53cef466cc7b3ec6d", size = 3137585, upload-time = "2025-09-18T11:51:04.921Z" }, - { url = "https://files.pythonhosted.org/packages/17/4c/78650e1d54a9a8460add62643a57a0042880592e5a6f5574370954d7e91d/granian-2.5.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:294c574fcd8005587671476b751e5853b262ecb1b1011e634ac160d6a0259abd", size = 3195738, upload-time = "2025-09-18T11:51:06.328Z" }, - { url = "https://files.pythonhosted.org/packages/d8/d0/cdab820b2f5c692dc4c67879f937fb223eae1599784755362a53872f512c/granian-2.5.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:3f0940963f6c6e50de19195014902d9a565a612aa0583082c9082bd83f259446", size = 2747894, upload-time = "2025-09-18T11:51:09.927Z" }, - { url = "https://files.pythonhosted.org/packages/71/15/1d27cd429a4fb0cb066848ca7ba432e5887b2506873832136444a6aa24d2/granian-2.5.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bc27bff6ea5a80fd5bf28297ac53fa31771cbdfa35650a6eb4f2c4efc751097d", size = 2463984, upload-time = "2025-09-18T11:51:11.294Z" }, - { url = "https://files.pythonhosted.org/packages/18/c0/2f3594892055c465c0035d5fc174da95c01a4902f9c63e7ea05d49fad892/granian-2.5.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73c136bac23cd1d2c969a52374972ec7da6e0920768bf0bcce65e00cabb4ebb9", size = 3009713, upload-time = "2025-09-18T11:51:12.713Z" }, - { url = "https://files.pythonhosted.org/packages/31/e7/09e44ece7ebcd5a70cd313ca3a0fa5b21632697c20412ddf169c51f16894/granian-2.5.4-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2dc03e2f375354b95d477c9455fb2fb427a922740f45e036cdf60da660adbf13", size = 2794570, upload-time = "2025-09-18T11:51:14.015Z" }, - { url = "https://files.pythonhosted.org/packages/e4/91/e76301bf0411ede4739b273c972f959c4675702418b00228ed7c278aac05/granian-2.5.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:539ee12b02281929358349e01a0c42c0594ebcf4f44033c8a4d7a446f837e034", size = 2909702, upload-time = "2025-09-18T11:51:16.252Z" }, - { url = "https://files.pythonhosted.org/packages/4a/6e/928b37a9a8a863339f5e37d4154de6faf1b8c58c8684799e117caf66b3a4/granian-2.5.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:97735bdbc2877583ea1c8dbfca31bcaf118a6e818afe6000eb8a9d09fd9d07e0", size = 3118746, upload-time = "2025-09-18T11:51:17.928Z" }, - { url = "https://files.pythonhosted.org/packages/94/7c/5c24c3d17ae24eee4af34bc46ecf6d9fc5d3a23b7b974c7ef3ff0f51cea9/granian-2.5.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:5f642a4fa1d41943d288db714bd1e0d218537bfa8bc6355d7063e8959b84c32b", size = 3187333, upload-time = "2025-09-18T11:51:19.427Z" }, - { url = "https://files.pythonhosted.org/packages/7b/7f/37896fc4180fd0f59146e3bd88e21fb71ce28c705d482540cfc8fc532cc9/granian-2.5.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9b970a50230ae437615d754e1bc4aaa740fbe3f1418cc0c8933b260a691bb8f5", size = 2853053, upload-time = "2025-09-18T11:51:37.959Z" }, - { url = "https://files.pythonhosted.org/packages/a2/f4/fb23d1958b52a1546f25da03c459e11ea634d166dc7ef6024c7662c949fc/granian-2.5.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a09d2bef7805f10093aa356d976fdb3607d274252ef9429c6c1a24d239032c29", size = 2553878, upload-time = "2025-09-18T11:51:39.251Z" }, - { url = "https://files.pythonhosted.org/packages/54/43/e105894f0ae2711080c827f71d04d63ec7b4b881e0c4ae8a22a41500c800/granian-2.5.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6d5bd05c6e833d54b77c2ee19130cfa5d54ae4eb301ffca56744f712c4a9d03", size = 3139712, upload-time = "2025-09-18T11:51:40.999Z" }, - { url = "https://files.pythonhosted.org/packages/b1/ae/d1df545edeafbfd787c73863e47b269ff5646ec68b025db692a1c6aafbd5/granian-2.5.4-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a7782a25ab78a55b61cb9b06f8aac471e9fafa3e1c20d6cdf970e93c834f6ddf", size = 2919335, upload-time = "2025-09-18T11:51:42.616Z" }, - { url = "https://files.pythonhosted.org/packages/6f/95/c55889554ef4471b786853707c3fa68f8ef4fafa3257054bd6990042db1f/granian-2.5.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4403600ac0273d4169c4c73773f57c5a3b44cc8aa8384a2f468c98c4294a3f27", size = 2927449, upload-time = "2025-09-18T11:51:44.273Z" }, - { url = "https://files.pythonhosted.org/packages/0b/52/34d3e17753c20ea56b88e48778e8c2b6fa01593e6febc0123bb6edfae7d7/granian-2.5.4-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c711335619a6936728b7773052fb0ec9d612b19abb2c786972ce3efee836df9d", size = 3172721, upload-time = "2025-09-18T11:51:45.76Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b0/0323368449a05b2693ebbbbd4f61bcaef858a3df0255b6ee3dd460112549/granian-2.5.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ff9996929a16a72a61fb1f03a9e79e830bf7a6a4e9eb0470c6ef03f67d5ea5c0", size = 3183851, upload-time = "2025-09-18T11:51:47.651Z" }, - { url = "https://files.pythonhosted.org/packages/6a/69/e4c77826239bf3d612dced6a3a0007ef378bb454d602d5751a16c8b74d2f/granian-2.5.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f3fcf2e6c8a150f48e4b77271b33ebfc7c2d8381e692b5177d4bd7fcefbb691d", size = 2853059, upload-time = "2025-09-18T11:51:52.136Z" }, - { url = "https://files.pythonhosted.org/packages/6f/74/4ac398a54f718db5fd13e92ed1198a76c7c332dee4ab7af9c06ded51c884/granian-2.5.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:c5550d533a61053fc4d95044bdc80ba00118ca312ed85867ed63163fa5878f85", size = 2553706, upload-time = "2025-09-18T11:51:53.714Z" }, - { url = "https://files.pythonhosted.org/packages/32/10/871e8d09eae976613b3b0812eb927d39e7960fecf1ac885e21962bbc8ff3/granian-2.5.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48356114113ac3d48f70ea05cf42e560384c93318f5ef8f5638cb666f8243f2b", size = 3139539, upload-time = "2025-09-18T11:51:55.133Z" }, - { url = "https://files.pythonhosted.org/packages/ec/f6/dd0ef6a7f3f4fea28b57014bc69b925995c7c6006451653b115440bf432b/granian-2.5.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:cbadc8e49f90716b8f8aa2c2cee7a2e82c5a37dab5f6fbd449e76185ce205715", size = 2919295, upload-time = "2025-09-18T11:51:56.496Z" }, - { url = "https://files.pythonhosted.org/packages/27/dd/3a8f99363b05d33049ea8cd94e0d40792aebddfa39f6f14120b71613fdca/granian-2.5.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d1712a8ede152c2648557de6a23dbeb05ed499bfd83c42dad0689d9f2ba0621d", size = 2927578, upload-time = "2025-09-18T11:51:58.081Z" }, - { url = "https://files.pythonhosted.org/packages/49/42/ecb762dbf7a1447a89b4884907d07fa505fd89f904c1e2d3bd4f30aeb9d1/granian-2.5.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:b78b8a6f30d0534b2db3f9cb99702d06be110b6e91f5639837a6f52f4891fc1d", size = 3172980, upload-time = "2025-09-18T11:51:59.428Z" }, - { url = "https://files.pythonhosted.org/packages/4c/2f/2abc969b206033d789c87d0ed7ee17a8831b0740e30590348abb544dba13/granian-2.5.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:10ae5ce26b1048888ac5aa3747415d8be8bbb014106b27ef0e77d23d2e00c51d", size = 3183479, upload-time = "2025-09-18T11:52:01.133Z" }, + { url = "https://files.pythonhosted.org/packages/54/3f/9a78d70beaa2dafc54c2147dd03ce1b75a97d12b61e9319eacb6ad536e30/granian-2.6.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b8a9f6006142ed64082ec726a9de40f4eb2ebe65f842c199f254b253362b8ab4", size = 3073952, upload-time = "2026-01-07T11:06:51.277Z" }, + { url = "https://files.pythonhosted.org/packages/dd/40/52e01a382d58ba416d12e165a2ac1e3270367267ced12ddfe1dd1fb03b65/granian-2.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:811a8eec0f96c7a1c9f991a2922f7c21561ecb2c52263666e14474aeea37ff6b", size = 2827739, upload-time = "2026-01-07T11:06:52.637Z" }, + { url = "https://files.pythonhosted.org/packages/7f/65/439a866076b378611eeebc3ecb285cc606aad4046b3f3b26035cc82d8c8b/granian-2.6.1-cp310-cp310-manylinux_2_24_armv7l.whl", hash = "sha256:3b78caa06c44d73551038aba9918d03d59f4d37e2bf39623e5572800d110e121", size = 3327232, upload-time = "2026-01-07T11:06:54.813Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/e9611920b7a4c3fe4881fa204322eb3944decd57ce3b38db2a3da00bb876/granian-2.6.1-cp310-cp310-manylinux_2_24_i686.whl", hash = "sha256:7cce9865bab5c2ca29d80224caad390b15c385ec23903bf0781f4cc6cee006c6", size = 3140534, upload-time = "2026-01-07T11:06:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/46/a7/3840c2a9ed7fcf1c606dc8796aa3c4e43bcc4169ba04d2f840d576d8a238/granian-2.6.1-cp310-cp310-manylinux_2_24_x86_64.whl", hash = "sha256:b6df9ffdcbd85e4151661e878127186a6a54a8ba4048cf5f883d9093ae628b99", size = 3372279, upload-time = "2026-01-07T11:06:57.483Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e8/4b78ed5fab45e83c69f6e1ea8805dbc638e7694d7946e97495c507026e6b/granian-2.6.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:66e5e25bc901c4bd54ab0f6449318c678e49ef6dcfd4fd62f15188680ed9a24d", size = 3239371, upload-time = "2026-01-07T11:06:59.49Z" }, + { url = "https://files.pythonhosted.org/packages/76/7d/a9e70763e9c99158edcdaca74c4b4fd4d57a46d2ea39a0ef32df4e8262f3/granian-2.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5429fb374accfa48f6c6459b3e0278f3ad58bba701127b62163086b70c076d11", size = 3309145, upload-time = "2026-01-07T11:07:01.166Z" }, + { url = "https://files.pythonhosted.org/packages/95/07/c899ba39d1be5810e25981d7c72a53437e2d099393fad9c8e6c273690f05/granian-2.6.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:8276b24da71771bc282057f9d144452695d54a99522adcf3a02175e613929d09", size = 3492752, upload-time = "2026-01-07T11:07:03.018Z" }, + { url = "https://files.pythonhosted.org/packages/1c/99/590ad3ad2f97c0e2f32558f0cf630ab72aa7c71f94e865084f3fb89b7cc5/granian-2.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6a3248c965f6ca71dca1285c7f5e16e4209a779a218fc2f64fe2f6cbe5ace709", size = 3498818, upload-time = "2026-01-07T11:07:04.536Z" }, + { url = "https://files.pythonhosted.org/packages/66/a3/12e30c4a16761f6db3cff71a93351678dca535c6348d3c1f65f6461c8848/granian-2.6.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:66cb53293a721bf2da651150cb5ba501d5536c3bec284dcbcb10a9f044a4b23e", size = 3073572, upload-time = "2026-01-07T11:07:07.447Z" }, + { url = "https://files.pythonhosted.org/packages/11/c0/0582a42e5b3e3c9e34eb9060585ed6cd11807852d645c19a0a79953be571/granian-2.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fef1c4b75d3827101a103b134abf43076703b6143b788f022e821dc4180b602", size = 2827569, upload-time = "2026-01-07T11:07:08.964Z" }, + { url = "https://files.pythonhosted.org/packages/db/b8/306dad81288330c5c1043434ac57a246a7cd3a70cd5877570efcd7888879/granian-2.6.1-cp311-cp311-manylinux_2_24_armv7l.whl", hash = "sha256:2375c346cafd2afd944a8b014f7dd882b416161ffe321c126d6d87984499396c", size = 3326925, upload-time = "2026-01-07T11:07:10.288Z" }, + { url = "https://files.pythonhosted.org/packages/ad/bb/f76654e4e5679d000335101922653c809adacaa675f861646aef95e9673c/granian-2.6.1-cp311-cp311-manylinux_2_24_i686.whl", hash = "sha256:6c0e9367956c1cdd23b41d571159e59b5530c8f44ff4c340fe69065ffd1bfe70", size = 3140557, upload-time = "2026-01-07T11:07:11.764Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0d/2e6ab1ce28fbb45f8e747d33db06ea870c1eee580c584592a8ceb49c0a59/granian-2.6.1-cp311-cp311-manylinux_2_24_x86_64.whl", hash = "sha256:4eacfe0bf355a88486933e6f846c2ecc0c2b0cf020a989750765294da4216b0c", size = 3372055, upload-time = "2026-01-07T11:07:13.584Z" }, + { url = "https://files.pythonhosted.org/packages/5d/05/9f104225ef0ceef6770e12d476077656c7930cde84474797c4a9807a4d3d/granian-2.6.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:c6ac45432028c7799a083917cda567e377cf42dbcad45c24b66dd03b72b1e1d6", size = 3239306, upload-time = "2026-01-07T11:07:15.01Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ec/73ead13fe326ac548fda5f85f471e16015629672e8acc3d4ccc07e9b313a/granian-2.6.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0aaac0304c7c68e6b798d15dd96a7b6ae477ab89d519c398d470da460f7ddda0", size = 3309025, upload-time = "2026-01-07T11:07:16.445Z" }, + { url = "https://files.pythonhosted.org/packages/f4/fb/0f474c6d437464d440924f3261295c046365dc9514cdd898d152b5a6c0bd/granian-2.6.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:a356685207987c07fb19f76a04d7bac6da0322584ede37adb1af7a607f8c8e35", size = 3492393, upload-time = "2026-01-07T11:07:18.202Z" }, + { url = "https://files.pythonhosted.org/packages/30/92/dbd3793e3b02d0a09422dacd456d739039eba4147d2c716e601f87287fde/granian-2.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c2928057de92ef90c2d87e3de5e34af4e50d746c5adfb035a6bbef490ec465af", size = 3498644, upload-time = "2026-01-07T11:07:19.943Z" }, + { url = "https://files.pythonhosted.org/packages/50/d1/9d191ea0b4f01a0d2437600b32a025e687189bae072878ec161f358eb465/granian-2.6.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:801bcf7efc3fdd12a08016ed94b1a386480c9a5185eb8e017fd83db1b2d210b4", size = 3070339, upload-time = "2026-01-07T11:07:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1e/be0ba55a2b21aeadeb8774721964740130fdd3dd7337d8a5ec130a0c48c0/granian-2.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:853fb869a50d742576bb4f974f321242a71a4d8eed918939397b317ab32c6a2d", size = 2819049, upload-time = "2026-01-07T11:07:23.877Z" }, + { url = "https://files.pythonhosted.org/packages/78/c7/d8adb472dc71b212281a82d3ea00858809f2844a79b45e63bbb3a09921b7/granian-2.6.1-cp312-cp312-manylinux_2_24_armv7l.whl", hash = "sha256:327a6090496c1deebd9e315f973bdbfc5c927e5574588bba918bfe2127bbd578", size = 3322325, upload-time = "2026-01-07T11:07:25.304Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/c3ce9e4f19163f35c5c57c45af2ad353abcc6091a44625caec56e065ca4a/granian-2.6.1-cp312-cp312-manylinux_2_24_i686.whl", hash = "sha256:4c91f0eefc34d809773762a9b81c1c48e20ff74c0f1be876d1132d82c0f74609", size = 3136460, upload-time = "2026-01-07T11:07:26.682Z" }, + { url = "https://files.pythonhosted.org/packages/3d/87/91b57eb5407a12bfe779acfa3fbb2be329aec14e6d88acf293fe910c19e5/granian-2.6.1-cp312-cp312-manylinux_2_24_x86_64.whl", hash = "sha256:c5754de57b56597d5998b7bb40aa9d0dc4e1dbeb5aea3309945126ed71b41c6d", size = 3386850, upload-time = "2026-01-07T11:07:27.989Z" }, + { url = "https://files.pythonhosted.org/packages/f0/43/b61a6f3bfc2f35e504e42789776a269cbdc0cdafdb10597bd6534e93ba3d/granian-2.6.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e849d6467ebe77d0a75eb4175f7cc06b1150dbfce0259932a4270c765b4de6c4", size = 3240693, upload-time = "2026-01-07T11:07:29.52Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1d/c40bd8dd99b855190d67127e0610f082cfbc7898dbd41f1ade015c2041f7/granian-2.6.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5a265867203e30d3c54d9d99783346040681ba2aaec70fcbe63de0e295e7882f", size = 3312703, upload-time = "2026-01-07T11:07:31.128Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ca/589c042afc3287b36dfeed6df56074cc831a94e5217bcbd7c1af20812fe2/granian-2.6.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:03f0a1505e7862183203d7d7c1e2b29349bd63a18858ced49aec4d7aadb98fc8", size = 3483737, upload-time = "2026-01-07T11:07:32.726Z" }, + { url = "https://files.pythonhosted.org/packages/6f/51/72eb037bac01db9623fa5fb128739bfb5679fb90e6da2645c5a3d8a4168d/granian-2.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:703ed57ba134ab16f15d49f7d644329db1cb0f7f8114ec3f08fb8039850e308a", size = 3514745, upload-time = "2026-01-07T11:07:34.706Z" }, + { url = "https://files.pythonhosted.org/packages/4b/3f/40975a573dc9a80382121694d71379fffab568012f411038043ed454cdd0/granian-2.6.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:8af4c75ffa2c8c77a3f5558b5ff71a9d97a57e08387ef954f560f2412a0b3db9", size = 3069408, upload-time = "2026-01-07T11:07:38.4Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2f/64b0d58344eedfb7f27c48d7a40e840cd714a8898bcaf3289cecad02f030/granian-2.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b473cdaf3f19ddc16e511b2c2d1e98b9ce7c13fd105e9095ecb268a6a5286a32", size = 2818749, upload-time = "2026-01-07T11:07:39.946Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1e/e33ae736adbef0633307f13092490021688df33885c9a320b50b83dbc076/granian-2.6.1-cp313-cp313-manylinux_2_24_armv7l.whl", hash = "sha256:b8ca2ac7261bcb8e57a35f8e7202aa891807420d51e3e61fd0913088d379e0fd", size = 3321824, upload-time = "2026-01-07T11:07:41.379Z" }, + { url = "https://files.pythonhosted.org/packages/59/3c/ef25189d251d14c81b11514e8d0a9a3cd8f9a467df423fb14362d95c7d6a/granian-2.6.1-cp313-cp313-manylinux_2_24_i686.whl", hash = "sha256:1eca9cfcf05dc54ffb21b14797ed7718707f7d26e7c5274722212e491eb8a4a6", size = 3136201, upload-time = "2026-01-07T11:07:42.703Z" }, + { url = "https://files.pythonhosted.org/packages/4f/31/a5621235cd26e7cb78d57ce9a3baeb837885dc7ebf5c2c907f2bf319002a/granian-2.6.1-cp313-cp313-manylinux_2_24_x86_64.whl", hash = "sha256:7722373629ab423835eb25015223f59788aa077036ea9ac3a4bddce43b0eb9c9", size = 3386378, upload-time = "2026-01-07T11:07:44.289Z" }, + { url = "https://files.pythonhosted.org/packages/22/ce/134a494be1c4d8a602cc03298e3f961d66e4a2b97c974403ffce50c09965/granian-2.6.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:7de143cf76934cfc63cc8cf296af69f750e4e3799ec0700d5da8254202aad12a", size = 3240009, upload-time = "2026-01-07T11:07:46.18Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f5/48f2fff5effee50dce30d726874936d582e83a690ccdc87cc2ca15b9accf/granian-2.6.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b0e7d6ee92962544a16039e1d5f36c5f43cd0a538541e7a3e5337147c701539", size = 3312533, upload-time = "2026-01-07T11:07:48.176Z" }, + { url = "https://files.pythonhosted.org/packages/a2/90/0590100351bf2b99ff95b0ac34c8c14f61c13f8417c16b93860fe8b1619a/granian-2.6.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:d9ca799472c72cb378192d49004abe98e387c1719378b01d7dc85ab293fa680e", size = 3482213, upload-time = "2026-01-07T11:07:49.471Z" }, + { url = "https://files.pythonhosted.org/packages/22/12/0f8f1367ebc4bbd3e17bed842ad21cf9691d828b8c89029dfcf9f1448fcd/granian-2.6.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:616c3213d2ffe638e49a3578185cbe9d0009635949e7ac083275942a2cbbee0c", size = 3513942, upload-time = "2026-01-07T11:07:51.011Z" }, + { url = "https://files.pythonhosted.org/packages/64/ec/49ada0ed6861db9ba127c26058cc1a8451af891922cb2673b9e88e847cf6/granian-2.6.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:fd3151933d867352b6e240b0e57d97b96cd6e0fa010c9e3503f4cb83e6815f6b", size = 3030683, upload-time = "2026-01-07T11:07:53.803Z" }, + { url = "https://files.pythonhosted.org/packages/71/7e/4c8b9eb66555e95798b8cf5e18be910519daf6cdf3c8cac90333ffe8e031/granian-2.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:1d11bb744878fa73197a1af99b324b6ccd9368f2c73b82a6c4cfcc696c14dcde", size = 2772412, upload-time = "2026-01-07T11:07:55.605Z" }, + { url = "https://files.pythonhosted.org/packages/15/20/013495365505da26d45721b670114855a8969f1d3784ecdc60a124330075/granian-2.6.1-cp313-cp313t-manylinux_2_24_armv7l.whl", hash = "sha256:fe1103a49cdb75bbac47005f0a70353aa575b6ac052e9dc932b39b644358c43a", size = 3317657, upload-time = "2026-01-07T11:07:57.472Z" }, + { url = "https://files.pythonhosted.org/packages/63/1e/4070bb26876c12b1304da9d27d6309e3dc53bbdf5928d732ba9a87fe561a/granian-2.6.1-cp313-cp313t-manylinux_2_24_i686.whl", hash = "sha256:e965d85634e02fbf97960e4ba8ef5290d37c094ad089a89fb19b68958888297a", size = 2969120, upload-time = "2026-01-07T11:07:58.778Z" }, + { url = "https://files.pythonhosted.org/packages/08/3e/dd29f04737a554acd3309372c8d2c6901a99cac3f9fcdee73bbe5a9bf0aa/granian-2.6.1-cp313-cp313t-manylinux_2_24_x86_64.whl", hash = "sha256:570c509cf608d77f0a32d66a51c9e4e9aba064a0a388776c41398392cc5e58d3", size = 3263555, upload-time = "2026-01-07T11:08:00.133Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b6/ad4c439a8a20bebbed5066d051150ed0ad32160aee89c0cbdcf49fb1b092/granian-2.6.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:3a105aa2f9b6dba037f81bc36b49a61c1648b60a82020e5c34394ce0940cdaef", size = 3112547, upload-time = "2026-01-07T11:08:01.453Z" }, + { url = "https://files.pythonhosted.org/packages/b8/92/58b5c7ca48540232034f2fa8be839e2ec997ec723489ff704a02c5a19945/granian-2.6.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:a4d90804f8d7c61e01741d9ccd400257763670f0e52a3cb59829fe2465b2b4a1", size = 3304759, upload-time = "2026-01-07T11:08:02.967Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e0/fdcf7d91937acf6bfbe6c459820ffc847840d0375624daf1fcd3e2acea50/granian-2.6.1-cp313-cp313t-musllinux_1_1_armv7l.whl", hash = "sha256:647a818814c6c2a4bcd5757509596d9d5a0e10fbe96d52acb4c992f56134ae27", size = 3479270, upload-time = "2026-01-07T11:08:04.337Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/d40f9e8699c9bb6ebf923e6baee6c9f90b9ff997c075173f67ffdee9aefc/granian-2.6.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:20639cec0106f047147c6b873bce6aaa275fb71456810ce3c0124f35b149e408", size = 3509699, upload-time = "2026-01-07T11:08:06.215Z" }, + { url = "https://files.pythonhosted.org/packages/55/49/2a3f993a2ee5e9876811a9d5fc41f47582da9a6873b02f214271e68f539b/granian-2.6.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:021ed3abb6805be450136f73ddc15283fe83714135d4481f5c3363c12109ef43", size = 3067322, upload-time = "2026-01-07T11:08:09.136Z" }, + { url = "https://files.pythonhosted.org/packages/04/86/ad139301cc61c97ef8d8ec9a96e796350f108fa434c088ea14faa5e43e81/granian-2.6.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:df10c8fb56e00fd51aaccb639a34d067ec43cfbf1241d57d93ac67f0dbebd33d", size = 2818936, upload-time = "2026-01-07T11:08:11.091Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a7/5223956aed93671dd83094cb4883e6431701e07a7641382b8cde6f4328d3/granian-2.6.1-cp314-cp314-manylinux_2_24_armv7l.whl", hash = "sha256:8f0eaacf8c6f1be67b38022d9839b35040e5a23df6117a08be5fc661ca404200", size = 3319401, upload-time = "2026-01-07T11:08:12.817Z" }, + { url = "https://files.pythonhosted.org/packages/90/ab/cbd1e9d9d0419b8d55043c821ea5b431d9cd43b35928c4c9f2034f9abf34/granian-2.6.1-cp314-cp314-manylinux_2_24_i686.whl", hash = "sha256:2f987bdb76a78a78dee56e9b4e44862471dcdbc3549152e35c64b19ba09c4dd0", size = 3132180, upload-time = "2026-01-07T11:08:14.215Z" }, + { url = "https://files.pythonhosted.org/packages/c4/cb/4e4aa44e696c10a701c1366b9882561d6ba5caa2e3c00e3716a57da40c18/granian-2.6.1-cp314-cp314-manylinux_2_24_x86_64.whl", hash = "sha256:ffb814d4f9df8f04759c4c0fc1c903b607d363496e9d5fcb29596ab7f82bfae0", size = 3383751, upload-time = "2026-01-07T11:08:15.775Z" }, + { url = "https://files.pythonhosted.org/packages/72/10/f37497b41c8f2a49fbc7f17dafdb979c84084ea127e27bfe92fe28dcd229/granian-2.6.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:450d45418116766cce8d2ef138eeea59e74d15da3759ae87af9732a8896ca3ef", size = 3239746, upload-time = "2026-01-07T11:08:17.75Z" }, + { url = "https://files.pythonhosted.org/packages/33/c5/842eb481cba2dc49374ffabb5a0ffeb8e61017da10a5084764369b1ac452/granian-2.6.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6e85ca2e6c83b5d70801068fbdca8acf733e12e35cee782ee94554f417971aff", size = 3311896, upload-time = "2026-01-07T11:08:19.3Z" }, + { url = "https://files.pythonhosted.org/packages/9b/6e/db253ae3ab220a394ffcd0cc7c3d7752ba20757bf37270af8f7b6813cd59/granian-2.6.1-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:aa4d187fd52e2873ef714c1d8aee097d88c141ac850f5bf04dbd37b513a3d67b", size = 3480487, upload-time = "2026-01-07T11:08:21.037Z" }, + { url = "https://files.pythonhosted.org/packages/4d/1d/0a78570d2c1fcccd5df93ff18e2cf00a708a75b9e935c038d973554f5f87/granian-2.6.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:53e2380f2b522082d5587c5dc05ad80ae0463dc751655d7cfb19d7ed3dbc48d6", size = 3509926, upload-time = "2026-01-07T11:08:23.067Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ed/f1130abe166f5a9bb4c032bae94b4cbc1b04179703a98906493e3b329974/granian-2.6.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:997a2a45b14e583504b920abfbcaf605d3ee9a4e7e0d3599b41f2091b388bd81", size = 3029240, upload-time = "2026-01-07T11:08:26.17Z" }, + { url = "https://files.pythonhosted.org/packages/cb/40/f5ec53011f3d91c14d6be604b02df930069cdb2dddbeb6aabda0ce265fc4/granian-2.6.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ee9ac64109dd54ff6773a82e9dc302843079edd7d9fcca4a72d9286918ae2c03", size = 2771824, upload-time = "2026-01-07T11:08:28.267Z" }, + { url = "https://files.pythonhosted.org/packages/82/98/a75d6d3b47d0a44e7af0ee7733ea295c01fb0070a636cb93290a6075da35/granian-2.6.1-cp314-cp314t-manylinux_2_24_armv7l.whl", hash = "sha256:4a76bbfcfea1b5114bda720fd9dbc11e584e513c351c2ec399cd3e2dcb1f8fd2", size = 3313753, upload-time = "2026-01-07T11:08:30.556Z" }, + { url = "https://files.pythonhosted.org/packages/3d/33/c37f0c5dd649ff97bed0b70d36b8f02fd488d926c30b272f6d09618ccde7/granian-2.6.1-cp314-cp314t-manylinux_2_24_i686.whl", hash = "sha256:5f3c6bab1e443b2277923b0f836141584b0f56b6db74904d4436d6031118a3fa", size = 2966697, upload-time = "2026-01-07T11:08:32.52Z" }, + { url = "https://files.pythonhosted.org/packages/5a/71/92dd02f5b53d28478906fc374a91337e65efeae04be9f392bdcb4a313906/granian-2.6.1-cp314-cp314t-manylinux_2_24_x86_64.whl", hash = "sha256:bbc0cac57a8719ae0d96ee250c539872becdd941795ab4590dca49cba240c809", size = 3261860, upload-time = "2026-01-07T11:08:33.946Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ca/abab95b8b3541a5e540a3b89a0b2805cdd1b064c9083cd4ada71c7d1ac76/granian-2.6.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dac537c663ec9b2f1f8bd3c0c6fd0c3f749d91b4f35f918df9ea1357528360dd", size = 3111002, upload-time = "2026-01-07T11:08:35.355Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3f/b872cca9f39b98aaeccf95f4c5b10fca21562c623bb3f7115de4506e9e1b/granian-2.6.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:9d6a3ac7234708fb2cce1fec6dd0941458c45e26b98b377b4a08d715fe9a9bd2", size = 3303389, upload-time = "2026-01-07T11:08:36.767Z" }, + { url = "https://files.pythonhosted.org/packages/5d/39/2224e99dbd2c61aed585550f5a624b653a5c90b6ea7821c7a1e1c187d4cb/granian-2.6.1-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:75bb8af9306aaceade90ec91cd9f5d4bdb5537b221b31e5a358b1eadb57279ad", size = 3475366, upload-time = "2026-01-07T11:08:38.29Z" }, + { url = "https://files.pythonhosted.org/packages/72/01/5077972a90cee7ef28c03a02b35c15d111416b25ccf8f8fda8df9972b6ce/granian-2.6.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:b7c1a0f24bd079bab8f6550fbb277e09c187dd5fe15e24ea3f2ace78453e5b7b", size = 3507600, upload-time = "2026-01-07T11:08:39.691Z" }, + { url = "https://files.pythonhosted.org/packages/e1/eb/8965002085f93cc1a9887414f43aed0d23025a7d4a4965c27d23d2d9c3c6/granian-2.6.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3c14a8708066e396a56673cc23acff8174fff613c433f1da0746c903341e4c22", size = 3077208, upload-time = "2026-01-07T11:08:43.752Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8b/5e08ad10d2cfde71cc438bc3887f168f7845e195f67656d726c36bfbfa0f/granian-2.6.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4e3041bc47b6add143e3944c5bb0d14cd94b5b9722812319d73c24d24816b039", size = 2813151, upload-time = "2026-01-07T11:08:45.094Z" }, + { url = "https://files.pythonhosted.org/packages/82/1b/dcb6c44a059b0a6571da1d4abe329a30c7e40c49e7a108e963a7b8c61a4c/granian-2.6.1-pp311-pypy311_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:9635f707119a8bdc22ebfd70b50944a72a7e791905b544ac64938f4e87a8060f", size = 3357183, upload-time = "2026-01-07T11:08:46.952Z" }, + { url = "https://files.pythonhosted.org/packages/a3/18/b8e976b0bec47edb5d469a3c4e44d8cad3383ffb6b8202eba35249a23845/granian-2.6.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:566941333bed75345583c4ff0a72783812c68c5f87df3f9974e847bfcfb78d3e", size = 3233117, upload-time = "2026-01-07T11:08:48.354Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e7/939eb934e4de6faa3b60448bf233610aec39e6186b0da212179cedce3baf/granian-2.6.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4a85fa1f42fd54feda9c70a7ef349259da6c5d81f9d5918633c677b7be8238ba", size = 3306125, upload-time = "2026-01-07T11:08:49.848Z" }, + { url = "https://files.pythonhosted.org/packages/0f/30/b99516161444c2528b9ab9e2633060c226f7f6ddf2d24464fb0d3b3f86ce/granian-2.6.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:7787cfc9b79a298289ff728d26937eac95482fcb468ef912d9178e707889c276", size = 3485546, upload-time = "2026-01-07T11:08:51.479Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/30b0db6729767eac2249856b563a9f71a04b7eb8ce44321b7472834dcb19/granian-2.6.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d3fd613953ea080f911d66bbebd46c8c4b3e66fbb293f78b13c86fb3ec0202ae", size = 3497427, upload-time = "2026-01-07T11:08:53.065Z" }, ] [package.optional-dependencies] @@ -1201,6 +1626,68 @@ uvloop = [ { name = "uvloop", marker = "(platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_python_implementation == 'CPython' and sys_platform == 'linux')" }, ] +[[package]] +name = "greenlet" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/99/1cd3411c56a410994669062bd73dd58270c00cc074cac15f385a1fd91f8a/greenlet-3.3.1.tar.gz", hash = "sha256:41848f3230b58c08bb43dee542e74a2a2e34d3c59dc3076cec9151aeeedcae98", size = 184690, upload-time = "2026-01-23T15:31:02.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/65/5b235b40581ad75ab97dcd8b4218022ae8e3ab77c13c919f1a1dfe9171fd/greenlet-3.3.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:04bee4775f40ecefcdaa9d115ab44736cd4b9c5fba733575bfe9379419582e13", size = 273723, upload-time = "2026-01-23T15:30:37.521Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ad/eb4729b85cba2d29499e0a04ca6fbdd8f540afd7be142fd571eea43d712f/greenlet-3.3.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e1457f4fed12a50e427988a07f0f9df53cf0ee8da23fab16e6732c2ec909d4", size = 574874, upload-time = "2026-01-23T16:00:54.551Z" }, + { url = "https://files.pythonhosted.org/packages/87/32/57cad7fe4c8b82fdaa098c89498ef85ad92dfbb09d5eb713adedfc2ae1f5/greenlet-3.3.1-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:070472cd156f0656f86f92e954591644e158fd65aa415ffbe2d44ca77656a8f5", size = 586309, upload-time = "2026-01-23T16:05:25.18Z" }, + { url = "https://files.pythonhosted.org/packages/66/66/f041005cb87055e62b0d68680e88ec1a57f4688523d5e2fb305841bc8307/greenlet-3.3.1-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1108b61b06b5224656121c3c8ee8876161c491cbe74e5c519e0634c837cf93d5", size = 597461, upload-time = "2026-01-23T16:15:51.943Z" }, + { url = "https://files.pythonhosted.org/packages/87/eb/8a1ec2da4d55824f160594a75a9d8354a5fe0a300fb1c48e7944265217e1/greenlet-3.3.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a300354f27dd86bae5fbf7002e6dd2b3255cd372e9242c933faf5e859b703fe", size = 586985, upload-time = "2026-01-23T15:32:47.968Z" }, + { url = "https://files.pythonhosted.org/packages/15/1c/0621dd4321dd8c351372ee8f9308136acb628600658a49be1b7504208738/greenlet-3.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e84b51cbebf9ae573b5fbd15df88887815e3253fc000a7d0ff95170e8f7e9729", size = 1547271, upload-time = "2026-01-23T16:04:18.977Z" }, + { url = "https://files.pythonhosted.org/packages/9d/53/24047f8924c83bea7a59c8678d9571209c6bfe5f4c17c94a78c06024e9f2/greenlet-3.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0093bd1a06d899892427217f0ff2a3c8f306182b8c754336d32e2d587c131b4", size = 1613427, upload-time = "2026-01-23T15:33:44.428Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e8/2e1462c8fdbe0f210feb5ac7ad2d9029af8be3bf45bd9fa39765f821642f/greenlet-3.3.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5fd23b9bc6d37b563211c6abbb1b3cab27db385a4449af5c32e932f93017080c", size = 274974, upload-time = "2026-01-23T15:31:02.891Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a8/530a401419a6b302af59f67aaf0b9ba1015855ea7e56c036b5928793c5bd/greenlet-3.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f51496a0bfbaa9d74d36a52d2580d1ef5ed4fdfcff0a73730abfbbbe1403dd", size = 577175, upload-time = "2026-01-23T16:00:56.213Z" }, + { url = "https://files.pythonhosted.org/packages/8e/89/7e812bb9c05e1aaef9b597ac1d0962b9021d2c6269354966451e885c4e6b/greenlet-3.3.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb0feb07fe6e6a74615ee62a880007d976cf739b6669cce95daa7373d4fc69c5", size = 590401, upload-time = "2026-01-23T16:05:26.365Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/e2d5f0e59b94a2269b68a629173263fa40b63da32f5c231307c349315871/greenlet-3.3.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67ea3fc73c8cd92f42467a72b75e8f05ed51a0e9b1d15398c913416f2dafd49f", size = 601161, upload-time = "2026-01-23T16:15:53.456Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ae/8d472e1f5ac5efe55c563f3eabb38c98a44b832602e12910750a7c025802/greenlet-3.3.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39eda9ba259cc9801da05351eaa8576e9aa83eb9411e8f0c299e05d712a210f2", size = 590272, upload-time = "2026-01-23T15:32:49.411Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/0fde34bebfcadc833550717eade64e35ec8738e6b097d5d248274a01258b/greenlet-3.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e2e7e882f83149f0a71ac822ebf156d902e7a5d22c9045e3e0d1daf59cee2cc9", size = 1550729, upload-time = "2026-01-23T16:04:20.867Z" }, + { url = "https://files.pythonhosted.org/packages/16/c9/2fb47bee83b25b119d5a35d580807bb8b92480a54b68fef009a02945629f/greenlet-3.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80aa4d79eb5564f2e0a6144fcc744b5a37c56c4a92d60920720e99210d88db0f", size = 1615552, upload-time = "2026-01-23T15:33:45.743Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c8/9d76a66421d1ae24340dfae7e79c313957f6e3195c144d2c73333b5bfe34/greenlet-3.3.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7e806ca53acf6d15a888405880766ec84721aa4181261cd11a457dfe9a7a4975", size = 276443, upload-time = "2026-01-23T15:30:10.066Z" }, + { url = "https://files.pythonhosted.org/packages/81/99/401ff34bb3c032d1f10477d199724f5e5f6fbfb59816ad1455c79c1eb8e7/greenlet-3.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d842c94b9155f1c9b3058036c24ffb8ff78b428414a19792b2380be9cecf4f36", size = 597359, upload-time = "2026-01-23T16:00:57.394Z" }, + { url = "https://files.pythonhosted.org/packages/2b/bc/4dcc0871ed557792d304f50be0f7487a14e017952ec689effe2180a6ff35/greenlet-3.3.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20fedaadd422fa02695f82093f9a98bad3dab5fcda793c658b945fcde2ab27ba", size = 607805, upload-time = "2026-01-23T16:05:28.068Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cd/7a7ca57588dac3389e97f7c9521cb6641fd8b6602faf1eaa4188384757df/greenlet-3.3.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c620051669fd04ac6b60ebc70478210119c56e2d5d5df848baec4312e260e4ca", size = 622363, upload-time = "2026-01-23T16:15:54.754Z" }, + { url = "https://files.pythonhosted.org/packages/cf/05/821587cf19e2ce1f2b24945d890b164401e5085f9d09cbd969b0c193cd20/greenlet-3.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14194f5f4305800ff329cbf02c5fcc88f01886cadd29941b807668a45f0d2336", size = 609947, upload-time = "2026-01-23T15:32:51.004Z" }, + { url = "https://files.pythonhosted.org/packages/a4/52/ee8c46ed9f8babaa93a19e577f26e3d28a519feac6350ed6f25f1afee7e9/greenlet-3.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7b2fe4150a0cf59f847a67db8c155ac36aed89080a6a639e9f16df5d6c6096f1", size = 1567487, upload-time = "2026-01-23T16:04:22.125Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7c/456a74f07029597626f3a6db71b273a3632aecb9afafeeca452cfa633197/greenlet-3.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49f4ad195d45f4a66a0eb9c1ba4832bb380570d361912fa3554746830d332149", size = 1636087, upload-time = "2026-01-23T15:33:47.486Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ab/d26750f2b7242c2b90ea2ad71de70cfcd73a948a49513188a0fc0d6fc15a/greenlet-3.3.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:7ab327905cabb0622adca5971e488064e35115430cec2c35a50fd36e72a315b3", size = 275205, upload-time = "2026-01-23T15:30:24.556Z" }, + { url = "https://files.pythonhosted.org/packages/10/d3/be7d19e8fad7c5a78eeefb2d896a08cd4643e1e90c605c4be3b46264998f/greenlet-3.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65be2f026ca6a176f88fb935ee23c18333ccea97048076aef4db1ef5bc0713ac", size = 599284, upload-time = "2026-01-23T16:00:58.584Z" }, + { url = "https://files.pythonhosted.org/packages/ae/21/fe703aaa056fdb0f17e5afd4b5c80195bbdab701208918938bd15b00d39b/greenlet-3.3.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a3ae05b3d225b4155bda56b072ceb09d05e974bc74be6c3fc15463cf69f33fd", size = 610274, upload-time = "2026-01-23T16:05:29.312Z" }, + { url = "https://files.pythonhosted.org/packages/06/00/95df0b6a935103c0452dad2203f5be8377e551b8466a29650c4c5a5af6cc/greenlet-3.3.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:12184c61e5d64268a160226fb4818af4df02cfead8379d7f8b99a56c3a54ff3e", size = 624375, upload-time = "2026-01-23T16:15:55.915Z" }, + { url = "https://files.pythonhosted.org/packages/cb/86/5c6ab23bb3c28c21ed6bebad006515cfe08b04613eb105ca0041fecca852/greenlet-3.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6423481193bbbe871313de5fd06a082f2649e7ce6e08015d2a76c1e9186ca5b3", size = 612904, upload-time = "2026-01-23T15:32:52.317Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/7949994264e22639e40718c2daf6f6df5169bf48fb038c008a489ec53a50/greenlet-3.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:33a956fe78bbbda82bfc95e128d61129b32d66bcf0a20a1f0c08aa4839ffa951", size = 1567316, upload-time = "2026-01-23T16:04:23.316Z" }, + { url = "https://files.pythonhosted.org/packages/8d/6e/d73c94d13b6465e9f7cd6231c68abde838bb22408596c05d9059830b7872/greenlet-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b065d3284be43728dd280f6f9a13990b56470b81be20375a207cdc814a983f2", size = 1636549, upload-time = "2026-01-23T15:33:48.643Z" }, + { url = "https://files.pythonhosted.org/packages/ae/fb/011c7c717213182caf78084a9bea51c8590b0afda98001f69d9f853a495b/greenlet-3.3.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:bd59acd8529b372775cd0fcbc5f420ae20681c5b045ce25bd453ed8455ab99b5", size = 275737, upload-time = "2026-01-23T15:32:16.889Z" }, + { url = "https://files.pythonhosted.org/packages/41/2e/a3a417d620363fdbb08a48b1dd582956a46a61bf8fd27ee8164f9dfe87c2/greenlet-3.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b31c05dd84ef6871dd47120386aed35323c944d86c3d91a17c4b8d23df62f15b", size = 646422, upload-time = "2026-01-23T16:01:00.354Z" }, + { url = "https://files.pythonhosted.org/packages/b4/09/c6c4a0db47defafd2d6bab8ddfe47ad19963b4e30f5bed84d75328059f8c/greenlet-3.3.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02925a0bfffc41e542c70aa14c7eda3593e4d7e274bfcccca1827e6c0875902e", size = 658219, upload-time = "2026-01-23T16:05:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/e2/89/b95f2ddcc5f3c2bc09c8ee8d77be312df7f9e7175703ab780f2014a0e781/greenlet-3.3.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3e0f3878ca3a3ff63ab4ea478585942b53df66ddde327b59ecb191b19dbbd62d", size = 671455, upload-time = "2026-01-23T16:15:57.232Z" }, + { url = "https://files.pythonhosted.org/packages/80/38/9d42d60dffb04b45f03dbab9430898352dba277758640751dc5cc316c521/greenlet-3.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34a729e2e4e4ffe9ae2408d5ecaf12f944853f40ad724929b7585bca808a9d6f", size = 660237, upload-time = "2026-01-23T15:32:53.967Z" }, + { url = "https://files.pythonhosted.org/packages/96/61/373c30b7197f9e756e4c81ae90a8d55dc3598c17673f91f4d31c3c689c3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aec9ab04e82918e623415947921dea15851b152b822661cce3f8e4393c3df683", size = 1615261, upload-time = "2026-01-23T16:04:25.066Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d3/ca534310343f5945316f9451e953dcd89b36fe7a19de652a1dc5a0eeef3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71c767cf281a80d02b6c1bdc41c9468e1f5a494fb11bc8688c360524e273d7b1", size = 1683719, upload-time = "2026-01-23T15:33:50.61Z" }, + { url = "https://files.pythonhosted.org/packages/28/24/cbbec49bacdcc9ec652a81d3efef7b59f326697e7edf6ed775a5e08e54c2/greenlet-3.3.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:3e63252943c921b90abb035ebe9de832c436401d9c45f262d80e2d06cc659242", size = 282706, upload-time = "2026-01-23T15:33:05.525Z" }, + { url = "https://files.pythonhosted.org/packages/86/2e/4f2b9323c144c4fe8842a4e0d92121465485c3c2c5b9e9b30a52e80f523f/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76e39058e68eb125de10c92524573924e827927df5d3891fbc97bd55764a8774", size = 651209, upload-time = "2026-01-23T16:01:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/d9/87/50ca60e515f5bb55a2fbc5f0c9b5b156de7d2fc51a0a69abc9d23914a237/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9f9d5e7a9310b7a2f416dd13d2e3fd8b42d803968ea580b7c0f322ccb389b97", size = 654300, upload-time = "2026-01-23T16:05:32.199Z" }, + { url = "https://files.pythonhosted.org/packages/7c/25/c51a63f3f463171e09cb586eb64db0861eb06667ab01a7968371a24c4f3b/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b9721549a95db96689458a1e0ae32412ca18776ed004463df3a9299c1b257ab", size = 662574, upload-time = "2026-01-23T16:15:58.364Z" }, + { url = "https://files.pythonhosted.org/packages/1d/94/74310866dfa2b73dd08659a3d18762f83985ad3281901ba0ee9a815194fb/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92497c78adf3ac703b57f1e3813c2d874f27f71a178f9ea5887855da413cd6d2", size = 653842, upload-time = "2026-01-23T15:32:55.671Z" }, + { url = "https://files.pythonhosted.org/packages/97/43/8bf0ffa3d498eeee4c58c212a3905dd6146c01c8dc0b0a046481ca29b18c/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ed6b402bc74d6557a705e197d47f9063733091ed6357b3de33619d8a8d93ac53", size = 1614917, upload-time = "2026-01-23T16:04:26.276Z" }, + { url = "https://files.pythonhosted.org/packages/89/90/a3be7a5f378fc6e84abe4dcfb2ba32b07786861172e502388b4c90000d1b/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:59913f1e5ada20fde795ba906916aea25d442abcc0593fba7e26c92b7ad76249", size = 1676092, upload-time = "2026-01-23T15:33:52.176Z" }, +] + +[[package]] +name = "griffe" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112, upload-time = "2025-11-10T15:03:15.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705, upload-time = "2025-11-10T15:03:13.549Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -1224,68 +1711,103 @@ wheels = [ ] [[package]] -name = "hiredis" -version = "3.2.1" +name = "hf-xet" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f7/08/24b72f425b75e1de7442fb1740f69ca66d5820b9f9c0e2511ff9aadab3b7/hiredis-3.2.1.tar.gz", hash = "sha256:5a5f64479bf04dd829fe7029fad0ea043eac4023abc6e946668cbbec3493a78d", size = 89096, upload-time = "2025-05-23T11:41:57.227Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020, upload-time = "2025-10-24T19:04:32.129Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/12/e797b676d65b86d9ad56f434cb4548b1bd0ebf531cd2e36ef74c5cd46dcd/hiredis-3.2.1-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:add17efcbae46c5a6a13b244ff0b4a8fa079602ceb62290095c941b42e9d5dec", size = 82441, upload-time = "2025-05-23T11:39:36.142Z" }, - { url = "https://files.pythonhosted.org/packages/d3/04/45783d5cf6e7430b1c67d64a7919ee45381e8b98d6d4578516579c5a4420/hiredis-3.2.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:5fe955cc4f66c57df1ae8e5caf4de2925d43b5efab4e40859662311d1bcc5f54", size = 45235, upload-time = "2025-05-23T11:39:37.49Z" }, - { url = "https://files.pythonhosted.org/packages/d5/97/7f50bad0b8213a3ee7780e295cd3d5e3db2839de2a6342b3c0ceeaf8e0af/hiredis-3.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f9ad63cd9065820a43fb1efb8ed5ae85bb78f03ef5eb53f6bde47914708f5718", size = 43250, upload-time = "2025-05-23T11:39:38.518Z" }, - { url = "https://files.pythonhosted.org/packages/51/d0/38d4b5bf36bfd010fdfd460c53efc0aaef7c81d6c20f4041ca35e26a1e12/hiredis-3.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e7f9e5fdba08841d78d4e1450cae03a4dbed2eda8a4084673cafa5615ce24a", size = 168996, upload-time = "2025-05-23T11:39:39.563Z" }, - { url = "https://files.pythonhosted.org/packages/99/22/4e2e9fde2b2efcf9847a2442a21f404c4112c57cccd6a09e564524dd70f3/hiredis-3.2.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1dce2508eca5d4e47ef38bc7c0724cb45abcdb0089f95a2ef49baf52882979a8", size = 165508, upload-time = "2025-05-23T11:39:40.723Z" }, - { url = "https://files.pythonhosted.org/packages/98/d0/b05bc8d4f339abaa455a9e677fc5223e25cd97630e66a2da0ad25e67b131/hiredis-3.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:186428bf353e4819abae15aa2ad64c3f40499d596ede280fe328abb9e98e72ce", size = 180109, upload-time = "2025-05-23T11:39:41.865Z" }, - { url = "https://files.pythonhosted.org/packages/e3/ca/6df2cf488792ace30ee525a5444e12f432cc1da4acb47756ea5de265ea80/hiredis-3.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:74f2500d90a0494843aba7abcdc3e77f859c502e0892112d708c02e1dcae8f90", size = 169161, upload-time = "2025-05-23T11:39:43.432Z" }, - { url = "https://files.pythonhosted.org/packages/15/8b/afcef7a30bf5b94936264edb7daaf12a165f2b57007e384a57ac48411886/hiredis-3.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32822a94d2fdd1da96c05b22fdeef6d145d8fdbd865ba2f273f45eb949e4a805", size = 169485, upload-time = "2025-05-23T11:39:45.008Z" }, - { url = "https://files.pythonhosted.org/packages/43/14/3443dee27bd20f2ac88a759b67b29e7f3756a9a38bbe8084de049dfc5cac/hiredis-3.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ead809fb08dd4fdb5b4b6e2999c834e78c3b0c450a07c3ed88983964432d0c64", size = 163644, upload-time = "2025-05-23T11:39:46.755Z" }, - { url = "https://files.pythonhosted.org/packages/3f/24/8a3cee0f08071af0a9632ca81a057fe2b638e7b6956c9b5704a2049c1305/hiredis-3.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b90fada20301c3a257e868dd6a4694febc089b2b6d893fa96a3fc6c1f9ab4340", size = 162180, upload-time = "2025-05-23T11:39:47.939Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2c/34cb6e665535dce1cbb7077cb9cc608198f254050241b5e232d62393f6a7/hiredis-3.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6d8bff53f526da3d9db86c8668011e4f7ca2958ee3a46c648edab6fe2cd1e709", size = 174369, upload-time = "2025-05-23T11:39:49.13Z" }, - { url = "https://files.pythonhosted.org/packages/f8/24/96702f71991d884412d7ac89577ad9caa28875e2e309f53751b8c5f969be/hiredis-3.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:043d929ae262d03e1db0f08616e14504a9119c1ff3de13d66f857d85cd45caff", size = 166511, upload-time = "2025-05-23T11:39:50.232Z" }, - { url = "https://files.pythonhosted.org/packages/de/d0/8d3753244bdea37ab1700db8eec220df8361d0e3f72b9b5314ce4a0471ac/hiredis-3.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8d470fef39d02dbe5c541ec345cc4ffd7d2baec7d6e59c92bd9d9545dc221829", size = 164329, upload-time = "2025-05-23T11:39:51.365Z" }, - { url = "https://files.pythonhosted.org/packages/48/84/2ea9636f2ba0811d9eb3bebbbfa84f488238180ddab70c9cb7fa13419d78/hiredis-3.2.1-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:e4ae0be44cab5e74e6e4c4a93d04784629a45e781ff483b136cc9e1b9c23975c", size = 82425, upload-time = "2025-05-23T11:39:54.135Z" }, - { url = "https://files.pythonhosted.org/packages/fc/24/b9ebf766a99998fda3975937afa4912e98de9d7f8d0b83f48096bdd961c1/hiredis-3.2.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:24647e84c9f552934eb60b7f3d2116f8b64a7020361da9369e558935ca45914d", size = 45231, upload-time = "2025-05-23T11:39:55.455Z" }, - { url = "https://files.pythonhosted.org/packages/68/4c/c009b4d9abeb964d607f0987561892d1589907f770b9e5617552b34a4a4d/hiredis-3.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6fb3e92d1172da8decc5f836bf8b528c0fc9b6d449f1353e79ceeb9dc1801132", size = 43240, upload-time = "2025-05-23T11:39:57.8Z" }, - { url = "https://files.pythonhosted.org/packages/e9/83/d53f3ae9e4ac51b8a35afb7ccd68db871396ed1d7c8ba02ce2c30de0cf17/hiredis-3.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38ba7a32e51e518b6b3e470142e52ed2674558e04d7d73d86eb19ebcb37d7d40", size = 169624, upload-time = "2025-05-23T11:40:00.055Z" }, - { url = "https://files.pythonhosted.org/packages/91/2f/f9f091526e22a45385d45f3870204dc78aee365b6fe32e679e65674da6a7/hiredis-3.2.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4fc632be73174891d6bb71480247e57b2fd8f572059f0a1153e4d0339e919779", size = 165799, upload-time = "2025-05-23T11:40:01.194Z" }, - { url = "https://files.pythonhosted.org/packages/1c/cc/e561274438cdb19794f0638136a5a99a9ca19affcb42679b12a78016b8ad/hiredis-3.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f03e6839ff21379ad3c195e0700fc9c209e7f344946dea0f8a6d7b5137a2a141", size = 180612, upload-time = "2025-05-23T11:40:02.385Z" }, - { url = "https://files.pythonhosted.org/packages/83/ba/a8a989f465191d55672e57aea2a331bfa3a74b5cbc6f590031c9e11f7491/hiredis-3.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99983873e37c71bb71deb544670ff4f9d6920dab272aaf52365606d87a4d6c73", size = 169934, upload-time = "2025-05-23T11:40:03.524Z" }, - { url = "https://files.pythonhosted.org/packages/52/5f/1148e965df1c67b17bdcaef199f54aec3def0955d19660a39c6ee10a6f55/hiredis-3.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffd982c419f48e3a57f592678c72474429465bb4bfc96472ec805f5d836523f0", size = 170074, upload-time = "2025-05-23T11:40:04.618Z" }, - { url = "https://files.pythonhosted.org/packages/43/5e/e6846ad159a938b539fb8d472e2e68cb6758d7c9454ea0520211f335ea72/hiredis-3.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bc993f4aa4abc029347f309e722f122e05a3b8a0c279ae612849b5cc9dc69f2d", size = 164158, upload-time = "2025-05-23T11:40:05.653Z" }, - { url = "https://files.pythonhosted.org/packages/0a/a1/5891e0615f0993f194c1b51a65aaac063b0db318a70df001b28e49f0579d/hiredis-3.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dde790d420081f18b5949227649ccb3ed991459df33279419a25fcae7f97cd92", size = 162591, upload-time = "2025-05-23T11:40:07.041Z" }, - { url = "https://files.pythonhosted.org/packages/d4/da/8bce52ca81716f53c1014f689aea4c170ba6411e6848f81a1bed1fc375eb/hiredis-3.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b0c8cae7edbef860afcf3177b705aef43e10b5628f14d5baf0ec69668247d08d", size = 174808, upload-time = "2025-05-23T11:40:09.146Z" }, - { url = "https://files.pythonhosted.org/packages/84/91/fc1ef444ed4dc432b5da9b48e9bd23266c703528db7be19e2b608d67ba06/hiredis-3.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e8a90eaca7e1ce7f175584f07a2cdbbcab13f4863f9f355d7895c4d28805f65b", size = 167060, upload-time = "2025-05-23T11:40:10.757Z" }, - { url = "https://files.pythonhosted.org/packages/66/ad/beebf73a5455f232b97e00564d1e8ad095d4c6e18858c60c6cfdd893ac1e/hiredis-3.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:476031958fa44e245e803827e0787d49740daa4de708fe514370293ce519893a", size = 164833, upload-time = "2025-05-23T11:40:12.001Z" }, - { url = "https://files.pythonhosted.org/packages/60/a1/6da1578a22df1926497f7a3f6a3d2408fe1d1559f762c1640af5762a8eb6/hiredis-3.2.1-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:3742d8b17e73c198cabeab11da35f2e2a81999d406f52c6275234592256bf8e8", size = 82627, upload-time = "2025-05-23T11:40:15.362Z" }, - { url = "https://files.pythonhosted.org/packages/6c/b1/1056558ca8dc330be5bb25162fe5f268fee71571c9a535153df9f871a073/hiredis-3.2.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:9c2f3176fb617a79f6cccf22cb7d2715e590acb534af6a82b41f8196ad59375d", size = 45404, upload-time = "2025-05-23T11:40:16.72Z" }, - { url = "https://files.pythonhosted.org/packages/58/4f/13d1fa1a6b02a99e9fed8f546396f2d598c3613c98e6c399a3284fa65361/hiredis-3.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a8bd46189c7fa46174e02670dc44dfecb60f5bd4b67ed88cb050d8f1fd842f09", size = 43299, upload-time = "2025-05-23T11:40:17.697Z" }, - { url = "https://files.pythonhosted.org/packages/c0/25/ddfac123ba5a32eb1f0b40ba1b2ec98a599287f7439def8856c3c7e5dd0d/hiredis-3.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f86ee4488c8575b58139cdfdddeae17f91e9a893ffee20260822add443592e2f", size = 172194, upload-time = "2025-05-23T11:40:19.143Z" }, - { url = "https://files.pythonhosted.org/packages/2c/1e/443a3703ce570b631ca43494094fbaeb051578a0ebe4bfcefde351e1ba25/hiredis-3.2.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3717832f4a557b2fe7060b9d4a7900e5de287a15595e398c3f04df69019ca69d", size = 168429, upload-time = "2025-05-23T11:40:20.329Z" }, - { url = "https://files.pythonhosted.org/packages/3b/d6/0d8c6c706ed79b2298c001b5458c055615e3166533dcee3900e821a18a3e/hiredis-3.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5cb12c21fb9e2403d28c4e6a38120164973342d34d08120f2d7009b66785644", size = 182967, upload-time = "2025-05-23T11:40:21.921Z" }, - { url = "https://files.pythonhosted.org/packages/da/68/da8dd231fbce858b5a20ab7d7bf558912cd125f08bac4c778865ef5fe2c2/hiredis-3.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:080fda1510bbd389af91f919c11a4f2aa4d92f0684afa4709236faa084a42cac", size = 172495, upload-time = "2025-05-23T11:40:23.105Z" }, - { url = "https://files.pythonhosted.org/packages/65/25/83a31420535e2778662caa95533d5c997011fa6a88331f0cdb22afea9ec3/hiredis-3.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1252e10a1f3273d1c6bf2021e461652c2e11b05b83e0915d6eb540ec7539afe2", size = 173142, upload-time = "2025-05-23T11:40:24.24Z" }, - { url = "https://files.pythonhosted.org/packages/41/d7/cb907348889eb75e2aa2e6b63e065b611459e0f21fe1e371a968e13f0d55/hiredis-3.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d9e320e99ab7d2a30dc91ff6f745ba38d39b23f43d345cdee9881329d7b511d6", size = 166433, upload-time = "2025-05-23T11:40:25.287Z" }, - { url = "https://files.pythonhosted.org/packages/01/5d/7cbc69d82af7b29a95723d50f5261555ba3d024bfbdc414bdc3d23c0defb/hiredis-3.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:641668f385f16550fdd6fdc109b0af6988b94ba2acc06770a5e06a16e88f320c", size = 164883, upload-time = "2025-05-23T11:40:26.454Z" }, - { url = "https://files.pythonhosted.org/packages/f9/00/f995b1296b1d7e0247651347aa230f3225a9800e504fdf553cf7cd001cf7/hiredis-3.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1e1f44208c39d6c345ff451f82f21e9eeda6fe9af4ac65972cc3eeb58d41f7cb", size = 177262, upload-time = "2025-05-23T11:40:27.576Z" }, - { url = "https://files.pythonhosted.org/packages/c5/f3/723a67d729e94764ce9e0d73fa5f72a0f87d3ce3c98c9a0b27cbf001cc79/hiredis-3.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f882a0d6415fffe1ffcb09e6281d0ba8b1ece470e866612bbb24425bf76cf397", size = 169619, upload-time = "2025-05-23T11:40:29.671Z" }, - { url = "https://files.pythonhosted.org/packages/45/58/f69028df00fb1b223e221403f3be2059ae86031e7885f955d26236bdfc17/hiredis-3.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b4e78719a0730ebffe335528531d154bc8867a246418f74ecd88adbc4d938c49", size = 167303, upload-time = "2025-05-23T11:40:30.902Z" }, - { url = "https://files.pythonhosted.org/packages/47/91/c07e737288e891c974277b9fa090f0a43c72ab6ccb5182117588f1c01269/hiredis-3.2.1-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:7cabf7f1f06be221e1cbed1f34f00891a7bdfad05b23e4d315007dd42148f3d4", size = 82636, upload-time = "2025-05-23T11:40:35.035Z" }, - { url = "https://files.pythonhosted.org/packages/92/20/02cb1820360eda419bc17eb835eca976079e2b3e48aecc5de0666b79a54c/hiredis-3.2.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:db85cb86f8114c314d0ec6d8de25b060a2590b4713135240d568da4f7dea97ac", size = 45404, upload-time = "2025-05-23T11:40:36.113Z" }, - { url = "https://files.pythonhosted.org/packages/87/51/d30a4aadab8670ed9d40df4982bc06c891ee1da5cdd88d16a74e1ecbd520/hiredis-3.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c9a592a49b7b8497e4e62c3ff40700d0c7f1a42d145b71e3e23c385df573c964", size = 43301, upload-time = "2025-05-23T11:40:37.557Z" }, - { url = "https://files.pythonhosted.org/packages/f7/7b/2c613e1bb5c2e2bac36e8befeefdd58b42816befb17e26ab600adfe337fb/hiredis-3.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0079ef1e03930b364556b78548e67236ab3def4e07e674f6adfc52944aa972dd", size = 172486, upload-time = "2025-05-23T11:40:38.659Z" }, - { url = "https://files.pythonhosted.org/packages/1e/df/8f2c4fcc28d6f5178b25ee1ba2157cc473f9908c16ce4b8e0bdd79e38b05/hiredis-3.2.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d6a290ed45d9c14f4c50b6bda07afb60f270c69b5cb626fd23a4c2fde9e3da1", size = 168532, upload-time = "2025-05-23T11:40:39.843Z" }, - { url = "https://files.pythonhosted.org/packages/88/ae/d0864ffaa0461e29a6940a11c858daf78c99476c06ed531b41ad2255ec25/hiredis-3.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79dd5fe8c0892769f82949adeb021342ca46871af26e26945eb55d044fcdf0d0", size = 183216, upload-time = "2025-05-23T11:40:41.005Z" }, - { url = "https://files.pythonhosted.org/packages/75/17/558e831b77692d73f5bcf8b493ab3eace9f11b0aa08839cdbb87995152c7/hiredis-3.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:998a82281a159f4aebbfd4fb45cfe24eb111145206df2951d95bc75327983b58", size = 172689, upload-time = "2025-05-23T11:40:42.153Z" }, - { url = "https://files.pythonhosted.org/packages/35/b9/4fccda21f930f08c5072ad51e825d85d457748138443d7b510afe77b8264/hiredis-3.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41fc3cd52368ffe7c8e489fb83af5e99f86008ed7f9d9ba33b35fec54f215c0a", size = 173319, upload-time = "2025-05-23T11:40:43.328Z" }, - { url = "https://files.pythonhosted.org/packages/3d/8b/596d613588b0a3c58dfcf9a17edc6a886c4de6a3096e27c7142a94e2304d/hiredis-3.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8d10df3575ce09b0fa54b8582f57039dcbdafde5de698923a33f601d2e2a246c", size = 166695, upload-time = "2025-05-23T11:40:44.453Z" }, - { url = "https://files.pythonhosted.org/packages/e7/5b/6a1c266e9f6627a8be1fa0d8622e35e35c76ae40cce6d1c78a7e6021184a/hiredis-3.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1ab010d04be33735ad8e643a40af0d68a21d70a57b1d0bff9b6a66b28cca9dbf", size = 165181, upload-time = "2025-05-23T11:40:45.697Z" }, - { url = "https://files.pythonhosted.org/packages/6c/70/a9b91fa70d21763d9dfd1c27ddd378f130749a0ae4a0645552f754b3d1fc/hiredis-3.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ec3b5f9ea34f70aaba3e061cbe1fa3556fea401d41f5af321b13e326792f3017", size = 177589, upload-time = "2025-05-23T11:40:46.903Z" }, - { url = "https://files.pythonhosted.org/packages/1a/c7/31bbb015156dc4441f6e19daa9598266a61445bf3f6e14c44292764638f6/hiredis-3.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:158dfb505fff6bffd17f823a56effc0c2a7a8bc4fb659d79a52782f22eefc697", size = 169883, upload-time = "2025-05-23T11:40:48.111Z" }, - { url = "https://files.pythonhosted.org/packages/89/44/cddc23379e0ce20ad7514b2adb2aa2c9b470ffb1ca0a2d8c020748962a22/hiredis-3.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d632cd0ddd7895081be76748e6fb9286f81d2a51c371b516541c6324f2fdac9", size = 167585, upload-time = "2025-05-23T11:40:49.208Z" }, - { url = "https://files.pythonhosted.org/packages/ed/f9/04a0a6c760d28e0b7d536646edacd6f5b4c979dd4c848621287bff5be9d0/hiredis-3.2.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:73913d2fa379e722d17ba52f21ce12dd578140941a08efd73e73b6fab1dea4d8", size = 40382, upload-time = "2025-05-23T11:41:34.425Z" }, - { url = "https://files.pythonhosted.org/packages/cd/1c/50fbce19cc5e393cf97a187462377d1c9441337684b3da1ed13ed0f20873/hiredis-3.2.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:15a3dff3eca31ecbf3d7d6d104cf1b318dc2b013bad3f4bdb2839cb9ea2e1584", size = 37760, upload-time = "2025-05-23T11:41:35.432Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e6/d147636edf44e5267f9e4c3483cd8d6b027fd6cf008a003c932f5ff888f7/hiredis-3.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c78258032c2f9fc6f39fee7b07882ce26de281e09178266ce535992572132d95", size = 48738, upload-time = "2025-05-23T11:41:36.452Z" }, - { url = "https://files.pythonhosted.org/packages/97/b0/53c33900139149a9b85878c04748984987b62ee2583d452b4e4d578067a9/hiredis-3.2.1-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:578d6a881e64e46db065256355594e680202c3bacf3270be3140057171d2c23e", size = 56254, upload-time = "2025-05-23T11:41:38.395Z" }, - { url = "https://files.pythonhosted.org/packages/9d/af/b49debecac06674a9ccb51353f497300199d6122a7612f56930872076147/hiredis-3.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b7f34b170093c077c972b8cc0ceb15d8ff88ad0079751a8ae9733e94d77e733", size = 48905, upload-time = "2025-05-23T11:41:39.92Z" }, + { url = "https://files.pythonhosted.org/packages/9e/a5/85ef910a0aa034a2abcfadc360ab5ac6f6bc4e9112349bd40ca97551cff0/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870, upload-time = "2025-10-24T19:04:11.422Z" }, + { url = "https://files.pythonhosted.org/packages/ea/40/e2e0a7eb9a51fe8828ba2d47fe22a7e74914ea8a0db68a18c3aa7449c767/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584, upload-time = "2025-10-24T19:04:09.586Z" }, + { url = "https://files.pythonhosted.org/packages/a5/7d/daf7f8bc4594fdd59a8a596f9e3886133fdc68e675292218a5e4c1b7e834/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004, upload-time = "2025-10-24T19:04:00.314Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/45ea2f605fbf6d81c8b21e4d970b168b18a53515923010c312c06cd83164/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636, upload-time = "2025-10-24T19:03:58.111Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448, upload-time = "2025-10-24T19:04:20.951Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401, upload-time = "2025-10-24T19:04:22.549Z" }, + { url = "https://files.pythonhosted.org/packages/e2/51/f7e2caae42f80af886db414d4e9885fac959330509089f97cccb339c6b87/hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e", size = 2861861, upload-time = "2025-10-24T19:04:19.01Z" }, + { url = "https://files.pythonhosted.org/packages/6e/1d/a641a88b69994f9371bd347f1dd35e5d1e2e2460a2e350c8d5165fc62005/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8", size = 2717699, upload-time = "2025-10-24T19:04:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/df/e0/e5e9bba7d15f0318955f7ec3f4af13f92e773fbb368c0b8008a5acbcb12f/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0", size = 3314885, upload-time = "2025-10-24T19:04:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/21/90/b7fe5ff6f2b7b8cbdf1bd56145f863c90a5807d9758a549bf3d916aa4dec/hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090", size = 3221550, upload-time = "2025-10-24T19:04:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cb/73f276f0a7ce46cc6a6ec7d6c7d61cbfe5f2e107123d9bbd0193c355f106/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a", size = 3408010, upload-time = "2025-10-24T19:04:28.598Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1e/d642a12caa78171f4be64f7cd9c40e3ca5279d055d0873188a58c0f5fbb9/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f", size = 3503264, upload-time = "2025-10-24T19:04:30.397Z" }, + { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054, upload-time = "2025-10-24T19:04:01.949Z" }, + { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812, upload-time = "2025-10-24T19:04:24.585Z" }, + { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920, upload-time = "2025-10-24T19:04:26.927Z" }, +] + +[[package]] +name = "hiredis" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/82/d2817ce0653628e0a0cb128533f6af0dd6318a49f3f3a6a7bd1f2f2154af/hiredis-3.3.0.tar.gz", hash = "sha256:105596aad9249634361815c574351f1bd50455dc23b537c2940066c4a9dea685", size = 89048, upload-time = "2025-10-14T16:33:34.263Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/44/20a95f4d5f9c0ffe4e5c095cd467545d4dc929840ab27f48c093dc364293/hiredis-3.3.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:9937d9b69321b393fbace69f55423480f098120bc55a3316e1ca3508c4dbbd6f", size = 81824, upload-time = "2025-10-14T16:31:46.655Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d9/acfcbcc648fa42a37ed90286f5f71dc4fd012a4347d008b0c67a6ed79492/hiredis-3.3.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:50351b77f89ba6a22aff430b993653847f36b71d444509036baa0f2d79d1ebf4", size = 46047, upload-time = "2025-10-14T16:31:48.207Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ad/fde44d70f6a5eed57dfebc6953a61cc69e6e331a673839f3fb7e186db606/hiredis-3.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1d00bce25c813eec45a2f524249f58daf51d38c9d3347f6f643ae53826fc735a", size = 41818, upload-time = "2025-10-14T16:31:49.242Z" }, + { url = "https://files.pythonhosted.org/packages/8e/99/175ef7110ada8ec6c247377f9b697d6c6237692313963fd666336e75f7bd/hiredis-3.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ef840d9f142556ed384180ed8cdf14ff875fcae55c980cbe5cec7adca2ef4d8", size = 167063, upload-time = "2025-10-14T16:31:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/7f/0d/766366e1b9fe84cde707728ec847fc78ff9fdee05c4a186203e4da270ffe/hiredis-3.3.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:88bc79d7e9b94d17ed1bd8b7f2815ed0eada376ed5f48751044e5e4d179aa2f2", size = 178930, upload-time = "2025-10-14T16:31:50.871Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ae/b0e532fef2eea0d16aeada2af5e40aa42ba6838748ef5f5b55f2fb2982e7/hiredis-3.3.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7165c7363e59b258e1875c51f35c0b2b9901e6c691037b487d8a0ace2c137ed2", size = 176735, upload-time = "2025-10-14T16:31:51.994Z" }, + { url = "https://files.pythonhosted.org/packages/4f/03/772b7b0f2464fb16fecb849127f34bace2983bb490eb59e89468b245033b/hiredis-3.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8c3be446f0c38fbe6863a7cf4522c9a463df6e64bee87c4402e9f6d7d2e7f869", size = 168800, upload-time = "2025-10-14T16:31:53.204Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e5/d14302ac17684fe742613d44c9d39ddeb21e5239e0f74a34f60effd7bf8e/hiredis-3.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:96f9a27643279853b91a1fb94a88b559e55fdecec86f1fcd5f2561492be52e47", size = 163475, upload-time = "2025-10-14T16:31:54.33Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cf/eaf1030e3afd55729f2764cde0d9dca8395a37680af13acc1f917e40b4a2/hiredis-3.3.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0a5eebb170de1b415c78ae5ca3aee17cff8b885df93c2055d54320e789d838f4", size = 174188, upload-time = "2025-10-14T16:31:55.519Z" }, + { url = "https://files.pythonhosted.org/packages/92/94/6b000f417f6893525f76809ab27b09cc378ca5878a18b5e27bd09541f16a/hiredis-3.3.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:200678547ac3966bac3e38df188211fdc13d5f21509c23267e7def411710e112", size = 167143, upload-time = "2025-10-14T16:31:56.444Z" }, + { url = "https://files.pythonhosted.org/packages/6e/b2/cc593707b4f0e0f15fcf389d6a0d50898404453f442095e73e4e15164de1/hiredis-3.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dd9d78c5363a858f9dc5e698e5e1e402b83c00226cba294f977a92c53092b549", size = 164898, upload-time = "2025-10-14T16:31:57.332Z" }, + { url = "https://files.pythonhosted.org/packages/34/0c/be3b1093f93a7c823ca16fbfbb83d3a1de671bbd2add8da1fe2bcfccb2b8/hiredis-3.3.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:63ee6c1ae6a2462a2439eb93c38ab0315cd5f4b6d769c6a34903058ba538b5d6", size = 81813, upload-time = "2025-10-14T16:32:00.576Z" }, + { url = "https://files.pythonhosted.org/packages/95/2b/ed722d392ac59a7eee548d752506ef32c06ffdd0bce9cf91125a74b8edf9/hiredis-3.3.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:31eda3526e2065268a8f97fbe3d0e9a64ad26f1d89309e953c80885c511ea2ae", size = 46049, upload-time = "2025-10-14T16:32:01.319Z" }, + { url = "https://files.pythonhosted.org/packages/e5/61/8ace8027d5b3f6b28e1dc55f4a504be038ba8aa8bf71882b703e8f874c91/hiredis-3.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a26bae1b61b7bcafe3d0d0c7d012fb66ab3c95f2121dbea336df67e344e39089", size = 41814, upload-time = "2025-10-14T16:32:02.076Z" }, + { url = "https://files.pythonhosted.org/packages/23/0e/380ade1ffb21034976663a5128f0383533f35caccdba13ff0537dd5ace79/hiredis-3.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9546079f7fd5c50fbff9c791710049b32eebe7f9b94debec1e8b9f4c048cba2", size = 167572, upload-time = "2025-10-14T16:32:03.125Z" }, + { url = "https://files.pythonhosted.org/packages/ca/60/b4a8d2177575b896730f73e6890644591aa56790a75c2b6d6f2302a1dae6/hiredis-3.3.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae327fc13b1157b694d53f92d50920c0051e30b0c245f980a7036e299d039ab4", size = 179373, upload-time = "2025-10-14T16:32:04.04Z" }, + { url = "https://files.pythonhosted.org/packages/31/53/a473a18d27cfe8afda7772ff9adfba1718fd31d5e9c224589dc17774fa0b/hiredis-3.3.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4016e50a8be5740a59c5af5252e5ad16c395021a999ad24c6604f0d9faf4d346", size = 177504, upload-time = "2025-10-14T16:32:04.934Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0f/f6ee4c26b149063dbf5b1b6894b4a7a1f00a50e3d0cfd30a22d4c3479db3/hiredis-3.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c17b473f273465a3d2168a57a5b43846165105ac217d5652a005e14068589ddc", size = 169449, upload-time = "2025-10-14T16:32:05.808Z" }, + { url = "https://files.pythonhosted.org/packages/64/38/e3e113172289e1261ccd43e387a577dd268b0b9270721b5678735803416c/hiredis-3.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9ecd9b09b11bd0b8af87d29c3f5da628d2bdc2a6c23d2dd264d2da082bd4bf32", size = 164010, upload-time = "2025-10-14T16:32:06.695Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9a/ccf4999365691ea73d0dd2ee95ee6ef23ebc9a835a7417f81765bc49eade/hiredis-3.3.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:00fb04eac208cd575d14f246e74a468561081ce235937ab17d77cde73aefc66c", size = 174623, upload-time = "2025-10-14T16:32:07.627Z" }, + { url = "https://files.pythonhosted.org/packages/ed/c7/ee55fa2ade078b7c4f17e8ddc9bc28881d0b71b794ebf9db4cfe4c8f0623/hiredis-3.3.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:60814a7d0b718adf3bfe2c32c6878b0e00d6ae290ad8e47f60d7bba3941234a6", size = 167650, upload-time = "2025-10-14T16:32:08.615Z" }, + { url = "https://files.pythonhosted.org/packages/bf/06/f6cd90275dcb0ba03f69767805151eb60b602bc25830648bd607660e1f97/hiredis-3.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fcbd1a15e935aa323b5b2534b38419511b7909b4b8ee548e42b59090a1b37bb1", size = 165452, upload-time = "2025-10-14T16:32:09.561Z" }, + { url = "https://files.pythonhosted.org/packages/48/1c/ed28ae5d704f5c7e85b946fa327f30d269e6272c847fef7e91ba5fc86193/hiredis-3.3.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:5b8e1d6a2277ec5b82af5dce11534d3ed5dffeb131fd9b210bc1940643b39b5f", size = 82026, upload-time = "2025-10-14T16:32:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/f4/9b/79f30c5c40e248291023b7412bfdef4ad9a8a92d9e9285d65d600817dac7/hiredis-3.3.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c4981de4d335f996822419e8a8b3b87367fcef67dc5fb74d3bff4df9f6f17783", size = 46217, upload-time = "2025-10-14T16:32:13.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c3/02b9ed430ad9087aadd8afcdf616717452d16271b701fa47edfe257b681e/hiredis-3.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1706480a683e328ae9ba5d704629dee2298e75016aa0207e7067b9c40cecc271", size = 41858, upload-time = "2025-10-14T16:32:13.98Z" }, + { url = "https://files.pythonhosted.org/packages/f1/98/b2a42878b82130a535c7aa20bc937ba2d07d72e9af3ad1ad93e837c419b5/hiredis-3.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a95cef9989736ac313639f8f545b76b60b797e44e65834aabbb54e4fad8d6c8", size = 170195, upload-time = "2025-10-14T16:32:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/9dcde7a75115d3601b016113d9b90300726fa8e48aacdd11bf01a453c145/hiredis-3.3.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca2802934557ccc28a954414c245ba7ad904718e9712cb67c05152cf6b9dd0a3", size = 181808, upload-time = "2025-10-14T16:32:15.622Z" }, + { url = "https://files.pythonhosted.org/packages/56/a1/60f6bda9b20b4e73c85f7f5f046bc2c154a5194fc94eb6861e1fd97ced52/hiredis-3.3.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fe730716775f61e76d75810a38ee4c349d3af3896450f1525f5a4034cf8f2ed7", size = 180578, upload-time = "2025-10-14T16:32:16.514Z" }, + { url = "https://files.pythonhosted.org/packages/d9/01/859d21de65085f323a701824e23ea3330a0ac05f8e184544d7aa5c26128d/hiredis-3.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:749faa69b1ce1f741f5eaf743435ac261a9262e2d2d66089192477e7708a9abc", size = 172508, upload-time = "2025-10-14T16:32:17.411Z" }, + { url = "https://files.pythonhosted.org/packages/99/a8/28fd526e554c80853d0fbf57ef2a3235f00e4ed34ce0e622e05d27d0f788/hiredis-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:95c9427f2ac3f1dd016a3da4e1161fa9d82f221346c8f3fdd6f3f77d4e28946c", size = 166341, upload-time = "2025-10-14T16:32:18.561Z" }, + { url = "https://files.pythonhosted.org/packages/f2/91/ded746b7d2914f557fbbf77be55e90d21f34ba758ae10db6591927c642c8/hiredis-3.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c863ee44fe7bff25e41f3a5105c936a63938b76299b802d758f40994ab340071", size = 176765, upload-time = "2025-10-14T16:32:19.491Z" }, + { url = "https://files.pythonhosted.org/packages/d6/4c/04aa46ff386532cb5f08ee495c2bf07303e93c0acf2fa13850e031347372/hiredis-3.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2213c7eb8ad5267434891f3241c7776e3bafd92b5933fc57d53d4456247dc542", size = 170312, upload-time = "2025-10-14T16:32:20.404Z" }, + { url = "https://files.pythonhosted.org/packages/90/6e/67f9d481c63f542a9cf4c9f0ea4e5717db0312fb6f37fb1f78f3a66de93c/hiredis-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a172bae3e2837d74530cd60b06b141005075db1b814d966755977c69bd882ce8", size = 167965, upload-time = "2025-10-14T16:32:21.259Z" }, + { url = "https://files.pythonhosted.org/packages/6d/39/2b789ebadd1548ccb04a2c18fbc123746ad1a7e248b7f3f3cac618ca10a6/hiredis-3.3.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:b7048b4ec0d5dddc8ddd03da603de0c4b43ef2540bf6e4c54f47d23e3480a4fa", size = 82035, upload-time = "2025-10-14T16:32:23.715Z" }, + { url = "https://files.pythonhosted.org/packages/85/74/4066d9c1093be744158ede277f2a0a4e4cd0fefeaa525c79e2876e9e5c72/hiredis-3.3.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:e5f86ce5a779319c15567b79e0be806e8e92c18bb2ea9153e136312fafa4b7d6", size = 46219, upload-time = "2025-10-14T16:32:24.554Z" }, + { url = "https://files.pythonhosted.org/packages/fa/3f/f9e0f6d632f399d95b3635703e1558ffaa2de3aea4cfcbc2d7832606ba43/hiredis-3.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fbdb97a942e66016fff034df48a7a184e2b7dc69f14c4acd20772e156f20d04b", size = 41860, upload-time = "2025-10-14T16:32:25.356Z" }, + { url = "https://files.pythonhosted.org/packages/4a/c5/b7dde5ec390dabd1cabe7b364a509c66d4e26de783b0b64cf1618f7149fc/hiredis-3.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0fb4bea72fe45ff13e93ddd1352b43ff0749f9866263b5cca759a4c960c776f", size = 170094, upload-time = "2025-10-14T16:32:26.148Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d6/7f05c08ee74d41613be466935688068e07f7b6c55266784b5ace7b35b766/hiredis-3.3.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:85b9baf98050e8f43c2826ab46aaf775090d608217baf7af7882596aef74e7f9", size = 181746, upload-time = "2025-10-14T16:32:27.844Z" }, + { url = "https://files.pythonhosted.org/packages/0e/d2/aaf9f8edab06fbf5b766e0cae3996324297c0516a91eb2ca3bd1959a0308/hiredis-3.3.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:69079fb0f0ebb61ba63340b9c4bce9388ad016092ca157e5772eb2818209d930", size = 180465, upload-time = "2025-10-14T16:32:29.185Z" }, + { url = "https://files.pythonhosted.org/packages/8d/1e/93ded8b9b484519b211fc71746a231af98c98928e3ebebb9086ed20bb1ad/hiredis-3.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c17f77b79031ea4b0967d30255d2ae6e7df0603ee2426ad3274067f406938236", size = 172419, upload-time = "2025-10-14T16:32:30.059Z" }, + { url = "https://files.pythonhosted.org/packages/68/13/02880458e02bbfcedcaabb8f7510f9dda1c89d7c1921b1bb28c22bb38cbf/hiredis-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45d14f745fc177bc05fc24bdf20e2b515e9a068d3d4cce90a0fb78d04c9c9d9a", size = 166400, upload-time = "2025-10-14T16:32:31.173Z" }, + { url = "https://files.pythonhosted.org/packages/11/60/896e03267670570f19f61dc65a2137fcb2b06e83ab0911d58eeec9f3cb88/hiredis-3.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ba063fdf1eff6377a0c409609cbe890389aefddfec109c2d20fcc19cfdafe9da", size = 176845, upload-time = "2025-10-14T16:32:32.12Z" }, + { url = "https://files.pythonhosted.org/packages/f1/90/a1d4bd0cdcf251fda72ac0bd932f547b48ad3420f89bb2ef91bf6a494534/hiredis-3.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1799cc66353ad066bfdd410135c951959da9f16bcb757c845aab2f21fc4ef099", size = 170365, upload-time = "2025-10-14T16:32:33.035Z" }, + { url = "https://files.pythonhosted.org/packages/f1/9a/7c98f7bb76bdb4a6a6003cf8209721f083e65d2eed2b514f4a5514bda665/hiredis-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2cbf71a121996ffac82436b6153290815b746afb010cac19b3290a1644381b07", size = 168022, upload-time = "2025-10-14T16:32:34.81Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b3/b948ee76a6b2bc7e45249861646f91f29704f743b52565cf64cee9c4658b/hiredis-3.3.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c135bda87211f7af9e2fd4e046ab433c576cd17b69e639a0f5bb2eed5e0e71a9", size = 82105, upload-time = "2025-10-14T16:32:37.204Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/4210f4ebfb3ab4ada964b8de08190f54cbac147198fb463cd3c111cc13e0/hiredis-3.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2f855c678230aed6fc29b962ce1cc67e5858a785ef3a3fd6b15dece0487a2e60", size = 46237, upload-time = "2025-10-14T16:32:38.07Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7a/e38bfd7d04c05036b4ccc6f42b86b1032185cf6ae426e112a97551fece14/hiredis-3.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4059c78a930cbb33c391452ccce75b137d6f89e2eebf6273d75dafc5c2143c03", size = 41894, upload-time = "2025-10-14T16:32:38.929Z" }, + { url = "https://files.pythonhosted.org/packages/28/d3/eae43d9609c5d9a6effef0586ee47e13a0d84b44264b688d97a75cd17ee5/hiredis-3.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:334a3f1d14c253bb092e187736c3384203bd486b244e726319bbb3f7dffa4a20", size = 170486, upload-time = "2025-10-14T16:32:40.147Z" }, + { url = "https://files.pythonhosted.org/packages/c3/fd/34d664554880b27741ab2916d66207357563b1639e2648685f4c84cfb755/hiredis-3.3.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd137b147235447b3d067ec952c5b9b95ca54b71837e1b38dbb2ec03b89f24fc", size = 182031, upload-time = "2025-10-14T16:32:41.06Z" }, + { url = "https://files.pythonhosted.org/packages/08/a3/0c69fdde3f4155b9f7acc64ccffde46f312781469260061b3bbaa487fd34/hiredis-3.3.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8f88f4f2aceb73329ece86a1cb0794fdbc8e6d614cb5ca2d1023c9b7eb432db8", size = 180542, upload-time = "2025-10-14T16:32:42.993Z" }, + { url = "https://files.pythonhosted.org/packages/68/7a/ad5da4d7bc241e57c5b0c4fe95aa75d1f2116e6e6c51577394d773216e01/hiredis-3.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:550f4d1538822fc75ebf8cf63adc396b23d4958bdbbad424521f2c0e3dfcb169", size = 172353, upload-time = "2025-10-14T16:32:43.965Z" }, + { url = "https://files.pythonhosted.org/packages/4b/dc/c46eace64eb047a5b31acd5e4b0dc6d2f0390a4a3f6d507442d9efa570ad/hiredis-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:54b14211fbd5930fc696f6fcd1f1f364c660970d61af065a80e48a1fa5464dd6", size = 166435, upload-time = "2025-10-14T16:32:44.97Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ac/ad13a714e27883a2e4113c980c94caf46b801b810de5622c40f8d3e8335f/hiredis-3.3.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9e96f63dbc489fc86f69951e9f83dadb9582271f64f6822c47dcffa6fac7e4a", size = 177218, upload-time = "2025-10-14T16:32:45.936Z" }, + { url = "https://files.pythonhosted.org/packages/c2/38/268fabd85b225271fe1ba82cb4a484fcc1bf922493ff2c74b400f1a6f339/hiredis-3.3.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:106e99885d46684d62ab3ec1d6b01573cc0e0083ac295b11aaa56870b536c7ec", size = 170477, upload-time = "2025-10-14T16:32:46.898Z" }, + { url = "https://files.pythonhosted.org/packages/20/6b/02bb8af810ea04247334ab7148acff7a61c08a8832830c6703f464be83a9/hiredis-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:087e2ef3206361281b1a658b5b4263572b6ba99465253e827796964208680459", size = 167915, upload-time = "2025-10-14T16:32:47.847Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b6/d7e6c17da032665a954a89c1e6ee3bd12cb51cd78c37527842b03519981d/hiredis-3.3.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:f7f80442a32ce51ee5d89aeb5a84ee56189a0e0e875f1a57bbf8d462555ae48f", size = 83034, upload-time = "2025-10-14T16:32:52.395Z" }, + { url = "https://files.pythonhosted.org/packages/27/6c/6751b698060cdd1b2d8427702cff367c9ed7a1705bcf3792eb5b896f149b/hiredis-3.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a1a67530da714954ed50579f4fe1ab0ddbac9c43643b1721c2cb226a50dde263", size = 46701, upload-time = "2025-10-14T16:32:53.572Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8e/20a5cf2c83c7a7e08c76b9abab113f99f71cd57468a9c7909737ce6e9bf8/hiredis-3.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:616868352e47ab355559adca30f4f3859f9db895b4e7bc71e2323409a2add751", size = 42381, upload-time = "2025-10-14T16:32:54.762Z" }, + { url = "https://files.pythonhosted.org/packages/be/0a/547c29c06e8c9c337d0df3eec39da0cf1aad701daf8a9658dd37f25aca66/hiredis-3.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e799b79f3150083e9702fc37e6243c0bd47a443d6eae3f3077b0b3f510d6a145", size = 180313, upload-time = "2025-10-14T16:32:55.644Z" }, + { url = "https://files.pythonhosted.org/packages/89/8a/488de5469e3d0921a1c425045bf00e983d48b2111a90e47cf5769eaa536c/hiredis-3.3.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ef1dfb0d2c92c3701655e2927e6bbe10c499aba632c7ea57b6392516df3864b", size = 190488, upload-time = "2025-10-14T16:32:56.649Z" }, + { url = "https://files.pythonhosted.org/packages/b5/59/8493edc3eb9ae0dbea2b2230c2041a52bc03e390b02ffa3ac0bca2af9aea/hiredis-3.3.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c290da6bc2a57e854c7da9956cd65013483ede935677e84560da3b848f253596", size = 189210, upload-time = "2025-10-14T16:32:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/f0/de/8c9a653922057b32fb1e2546ecd43ef44c9aa1a7cf460c87cae507eb2bc7/hiredis-3.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd8c438d9e1728f0085bf9b3c9484d19ec31f41002311464e75b69550c32ffa8", size = 180972, upload-time = "2025-10-14T16:32:58.737Z" }, + { url = "https://files.pythonhosted.org/packages/e4/a3/51e6e6afaef2990986d685ca6e254ffbd191f1635a59b2d06c9e5d10c8a2/hiredis-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1bbc6b8a88bbe331e3ebf6685452cebca6dfe6d38a6d4efc5651d7e363ba28bd", size = 175315, upload-time = "2025-10-14T16:32:59.774Z" }, + { url = "https://files.pythonhosted.org/packages/96/54/e436312feb97601f70f8b39263b8da5ac4a5d18305ebdfb08ad7621f6119/hiredis-3.3.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:55d8c18fe9a05496c5c04e6eccc695169d89bf358dff964bcad95696958ec05f", size = 185653, upload-time = "2025-10-14T16:33:00.749Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a3/88e66030d066337c6c0f883a912c6d4b2d6d7173490fbbc113a6cbe414ff/hiredis-3.3.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:4ddc79afa76b805d364e202a754666cb3c4d9c85153cbfed522871ff55827838", size = 179032, upload-time = "2025-10-14T16:33:01.711Z" }, + { url = "https://files.pythonhosted.org/packages/bc/1f/fb7375467e9adaa371cd617c2984fefe44bdce73add4c70b8dd8cab1b33a/hiredis-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e8a4b8540581dcd1b2b25827a54cfd538e0afeaa1a0e3ca87ad7126965981cc", size = 176127, upload-time = "2025-10-14T16:33:02.793Z" }, ] [[package]] @@ -1343,12 +1865,33 @@ wheels = [ ] [[package]] -name = "humanize" -version = "4.13.0" +name = "huggingface-hub" +version = "1.3.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/1d/3062fcc89ee05a715c0b9bfe6490c00c576314f27ffee3a704122c6fd259/humanize-4.13.0.tar.gz", hash = "sha256:78f79e68f76f0b04d711c4e55d32bebef5be387148862cb1ef83d2b58e7935a0", size = 81884, upload-time = "2025-08-25T09:39:20.04Z" } +dependencies = [ + { name = "filelock", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "fsspec", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "hf-xet", marker = "(platform_machine == 'AMD64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'darwin') or (platform_machine == 'amd64' and sys_platform == 'darwin') or (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'AMD64' and sys_platform == 'linux') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'amd64' and sys_platform == 'linux') or (platform_machine == 'arm64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "shellingham", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typer-slim", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/25/74af9d16cd59ae15b12467a79a84aa0fe24be4aba68fc4da0c1864d49c17/huggingface_hub-1.3.4.tar.gz", hash = "sha256:c20d5484a611b7b7891d272e8fc9f77d5de025b0480bdacfa858efb3780b455f", size = 627683, upload-time = "2026-01-26T14:05:10.656Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/c7/316e7ca04d26695ef0635dc81683d628350810eb8e9b2299fc08ba49f366/humanize-4.13.0-py3-none-any.whl", hash = "sha256:b810820b31891813b1673e8fec7f1ed3312061eab2f26e3fa192c393d11ed25f", size = 128869, upload-time = "2025-08-25T09:39:18.54Z" }, + { url = "https://files.pythonhosted.org/packages/55/07/3d0c34c345043c6a398a5882e196b2220dc5861adfa18322448b90908f26/huggingface_hub-1.3.4-py3-none-any.whl", hash = "sha256:a0c526e76eb316e96a91e8a1a7a93cf66b0dd210be1a17bd5fc5ae53cba76bfd", size = 536611, upload-time = "2026-01-26T14:05:08.549Z" }, +] + +[[package]] +name = "humanize" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/66/a3921783d54be8a6870ac4ccffcd15c4dc0dd7fcce51c6d63b8c63935276/humanize-4.15.0.tar.gz", hash = "sha256:1dd098483eb1c7ee8e32eb2e99ad1910baefa4b75c3aff3a82f4d78688993b10", size = 83599, upload-time = "2025-12-20T20:16:13.19Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/7b/bca5613a0c3b542420cf92bd5e5fb8ebd5435ce1011a091f66bb7693285e/humanize-4.15.0-py3-none-any.whl", hash = "sha256:b1186eb9f5a9749cd9cb8565aee77919dd7c8d076161cf44d70e59e3301e1769", size = 132203, upload-time = "2025-12-20T20:16:11.67Z" }, ] [[package]] @@ -1374,20 +1917,20 @@ wheels = [ [[package]] name = "identify" -version = "2.6.14" +version = "2.6.16" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/c4/62963f25a678f6a050fb0505a65e9e726996171e6dbe1547f79619eefb15/identify-2.6.14.tar.gz", hash = "sha256:663494103b4f717cb26921c52f8751363dc89db64364cd836a9bf1535f53cd6a", size = 99283, upload-time = "2025-09-06T19:30:52.938Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/8d/e8b97e6bd3fb6fb271346f7981362f1e04d6a7463abd0de79e1fda17c067/identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980", size = 99360, upload-time = "2026-01-12T18:58:58.201Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/ae/2ad30f4652712c82f1c23423d79136fbce338932ad166d70c1efb86a5998/identify-2.6.14-py2.py3-none-any.whl", hash = "sha256:11a073da82212c6646b1f39bb20d4483bfb9543bd5566fec60053c4bb309bf2e", size = 99172, upload-time = "2025-09-06T19:30:51.759Z" }, + { url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" }, ] [[package]] name = "idna" -version = "3.10" +version = "3.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] [[package]] @@ -1396,12 +1939,12 @@ version = "4.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, - { name = "numpy", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux')" }, + { name = "numpy", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux')" }, { name = "pillow", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "pywavelets", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, { name = "pywavelets", version = "1.9.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux')" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, - { name = "scipy", version = "1.16.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux')" }, + { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/de/5c0189b0582e21583c2a213081c35a2501c0f9e51f21f6a52f55fbb9a4ff/ImageHash-4.3.2.tar.gz", hash = "sha256:e54a79805afb82a34acde4746a16540503a9636fd1ffb31d8e099b29bbbf8156", size = 303190, upload-time = "2025-02-01T08:45:39.328Z" } wheels = [ @@ -1410,34 +1953,37 @@ wheels = [ [[package]] name = "imap-tools" -version = "1.11.0" +version = "1.11.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/76/2d74bf4702d7d9fb2dd056e058929961a05389be47b990f3275e8596012e/imap_tools-1.11.0.tar.gz", hash = "sha256:77b055d301f24e668ff54ad50cc32a36d1579c6aa9b26e5fb6501fb622feb6ea", size = 46191, upload-time = "2025-06-30T05:47:21.111Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/eb/de91770dc8f34691c33355f7b6277500db32f68ce039c08e2a40ad5a0536/imap_tools-1.11.1.tar.gz", hash = "sha256:e3aa02ff3415a2b50a47707eacbf7386bb79aabc34e370c6bb95f9ad20504389", size = 46562, upload-time = "2026-01-15T08:25:47.357Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/8f/75524e1a040183cc437332e2de6e8f975c345fff8b5aaa35e0d20dec24f9/imap_tools-1.11.0-py3-none-any.whl", hash = "sha256:7c797b421fdf1b898b4ee0042fe02d10037d56f9acacca64086c2af36d830a24", size = 34855, upload-time = "2025-06-30T05:47:15.657Z" }, + { url = "https://files.pythonhosted.org/packages/64/bb/94eca066102559a20953475779a4d4d6b39712985014bceba726e1f65aab/imap_tools-1.11.1-py3-none-any.whl", hash = "sha256:0749d0a1f2b9041be1533ea98cc3e9f7977ba86ad3669c1fcf89c27969dcfb0a", size = 35291, upload-time = "2026-01-15T08:25:42.773Z" }, ] [[package]] name = "img2pdf" -version = "0.6.1" +version = "0.6.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pikepdf", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "pillow", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/82/c3/023387e00682dc1b46bd719ec19c4c9206dc8eb182dfd02bc62c5b9320a2/img2pdf-0.6.1.tar.gz", hash = "sha256:306e279eb832bc159d7d6294b697a9fbd11b4be1f799b14b3b2174fb506af289", size = 106513, upload-time = "2025-04-27T16:55:10.039Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/97/ca44c467131b93fda82d2a2f21b738c8bcf63b5259e3b8250e928b8dd52a/img2pdf-0.6.3.tar.gz", hash = "sha256:219518020f5bd242bdc46493941ea3f756f664c2e86f2454721e74353f58cd95", size = 120350, upload-time = "2025-11-05T20:51:57.558Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/dc/91e3a4a11c25ae183bd5a71b84ecb298db76405ff70013f76b10877bdfe3/img2pdf-0.6.3-py3-none-any.whl", hash = "sha256:44d12d235752edd17c43c04ff39952cdc5dd4c6aba90569c4902bd445085266b", size = 49701, upload-time = "2025-11-05T20:51:55.469Z" }, +] [[package]] name = "incremental" -version = "24.7.2" +version = "24.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "setuptools", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "tomli", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/27/87/156b374ff6578062965afe30cc57627d35234369b3336cf244b240c8d8e6/incremental-24.7.2.tar.gz", hash = "sha256:fb4f1d47ee60efe87d4f6f0ebb5f70b9760db2b2574c59c8e8912be4ebd464c9", size = 28157, upload-time = "2024-07-29T20:03:55.441Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/3c/82e84109e02c492f382c711c58a3dd91badda6d746def81a1465f74dc9f5/incremental-24.11.0.tar.gz", hash = "sha256:87d3480dbb083c1d736222511a8cf380012a8176c2456d01ef483242abbbcf8c", size = 24000, upload-time = "2025-11-28T02:30:17.861Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/38/221e5b2ae676a3938c2c1919131410c342b6efc2baffeda395dd66eeca8f/incremental-24.7.2-py3-none-any.whl", hash = "sha256:8cb2c3431530bec48ad70513931a760f446ad6c25e8333ca5d95e24b0ed7b8fe", size = 20516, upload-time = "2024-07-29T20:03:53.677Z" }, + { url = "https://files.pythonhosted.org/packages/1d/55/0f4df2a44053867ea9cbea73fc588b03c55605cd695cee0a3d86f0029cb2/incremental-24.11.0-py3-none-any.whl", hash = "sha256:a34450716b1c4341fe6676a0598e88a39e04189f4dce5dc96f656e040baa10b3", size = 21109, upload-time = "2025-11-28T02:30:16.442Z" }, ] [[package]] @@ -1451,41 +1997,20 @@ wheels = [ [[package]] name = "iniconfig" -version = "2.1.0" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, -] - -[[package]] -name = "inotify-simple" -version = "2.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/5c/bfe40e15d684bc30b0073aa97c39be410a5fbef3d33cad6f0bf2012571e0/inotify_simple-2.0.1.tar.gz", hash = "sha256:f010bbbd8283bd71a9f4eb2de94765804ede24bd47320b0e6ef4136e541cdc2c", size = 7101, upload-time = "2025-08-25T06:28:20.998Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/86/8be1ac7e90f80b413e81f1e235148e8db771218886a2353392f02da01be3/inotify_simple-2.0.1-py3-none-any.whl", hash = "sha256:e5da495f2064889f8e68b67f9358b0d102e03b783c2d42e5b8e132ab859a5d8a", size = 7449, upload-time = "2025-08-25T06:28:19.919Z" }, -] - -[[package]] -name = "inotifyrecursive" -version = "0.3.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "inotify-simple", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/3a/9ed038cb750a3ba8090869cf3ad50f5628077a936d911aee14ca83e40f6a/inotifyrecursive-0.3.5.tar.gz", hash = "sha256:a2c450b317693e4538416f90eb1d7858506dafe6b8b885037bd2dd9ae2dafa1e", size = 4576, upload-time = "2020-11-20T12:38:48.035Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/fc/4e5a141c3f7c7bed550ac1f69e599e92b6be449dd4677ec09f325cad0955/inotifyrecursive-0.3.5-py3-none-any.whl", hash = "sha256:7e5f4a2e1dc2bef0efa3b5f6b339c41fb4599055a2b54909d020e9e932cc8d2f", size = 8009, upload-time = "2020-11-20T12:38:46.981Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] name = "isodate" version = "0.7.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705 } +sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320 }, + { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, ] [[package]] @@ -1501,17 +2026,95 @@ wheels = [ ] [[package]] -name = "joblib" -version = "1.5.2" +name = "jiter" +version = "0.12.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/5d/447af5ea094b9e4c4054f82e223ada074c552335b9b4b2d14bd9b35a67c4/joblib-1.5.2.tar.gz", hash = "sha256:3faa5c39054b2f03ca547da9b2f52fde67c06240c31853f306aea97f13647b55", size = 331077, upload-time = "2025-08-27T12:15:46.575Z" } +sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/e8/685f47e0d754320684db4425a0967f7d3fa70126bffd76110b7009a0090f/joblib-1.5.2-py3-none-any.whl", hash = "sha256:4e1f0bdbb987e6d843c70cf43714cb276623def372df3c22fe5266b2670bc241", size = 308396, upload-time = "2025-08-27T12:15:45.188Z" }, + { url = "https://files.pythonhosted.org/packages/3b/91/13cb9505f7be74a933f37da3af22e029f6ba64f5669416cb8b2774bc9682/jiter-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:e7acbaba9703d5de82a2c98ae6a0f59ab9770ab5af5fa35e43a303aee962cf65", size = 316652, upload-time = "2025-11-09T20:46:41.021Z" }, + { url = "https://files.pythonhosted.org/packages/4e/76/4e9185e5d9bb4e482cf6dec6410d5f78dfeb374cfcecbbe9888d07c52daa/jiter-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:364f1a7294c91281260364222f535bc427f56d4de1d8ffd718162d21fbbd602e", size = 319829, upload-time = "2025-11-09T20:46:43.281Z" }, + { url = "https://files.pythonhosted.org/packages/86/af/727de50995d3a153138139f259baae2379d8cb0522c0c00419957bc478a6/jiter-0.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85ee4d25805d4fb23f0a5167a962ef8e002dbfb29c0989378488e32cf2744b62", size = 350568, upload-time = "2025-11-09T20:46:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c1/d6e9f4b7a3d5ac63bcbdfddeb50b2dcfbdc512c86cffc008584fdc350233/jiter-0.12.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:796f466b7942107eb889c08433b6e31b9a7ed31daceaecf8af1be26fb26c0ca8", size = 369052, upload-time = "2025-11-09T20:46:46.818Z" }, + { url = "https://files.pythonhosted.org/packages/eb/be/00824cd530f30ed73fa8a4f9f3890a705519e31ccb9e929f1e22062e7c76/jiter-0.12.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35506cb71f47dba416694e67af996bbdefb8e3608f1f78799c2e1f9058b01ceb", size = 481585, upload-time = "2025-11-09T20:46:48.319Z" }, + { url = "https://files.pythonhosted.org/packages/74/b6/2ad7990dff9504d4b5052eef64aa9574bd03d722dc7edced97aad0d47be7/jiter-0.12.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:726c764a90c9218ec9e4f99a33d6bf5ec169163f2ca0fc21b654e88c2abc0abc", size = 380541, upload-time = "2025-11-09T20:46:49.643Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c7/f3c26ecbc1adbf1db0d6bba99192143d8fe8504729d9594542ecc4445784/jiter-0.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa47810c5565274810b726b0dc86d18dce5fd17b190ebdc3890851d7b2a0e74", size = 364423, upload-time = "2025-11-09T20:46:51.731Z" }, + { url = "https://files.pythonhosted.org/packages/18/51/eac547bf3a2d7f7e556927278e14c56a0604b8cddae75815d5739f65f81d/jiter-0.12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8ec0259d3f26c62aed4d73b198c53e316ae11f0f69c8fbe6682c6dcfa0fcce2", size = 389958, upload-time = "2025-11-09T20:46:53.432Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1f/9ca592e67175f2db156cff035e0d817d6004e293ee0c1d73692d38fcb596/jiter-0.12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:79307d74ea83465b0152fa23e5e297149506435535282f979f18b9033c0bb025", size = 522084, upload-time = "2025-11-09T20:46:54.848Z" }, + { url = "https://files.pythonhosted.org/packages/83/ff/597d9cdc3028f28224f53e1a9d063628e28b7a5601433e3196edda578cdd/jiter-0.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cf6e6dd18927121fec86739f1a8906944703941d000f0639f3eb6281cc601dca", size = 513054, upload-time = "2025-11-09T20:46:56.487Z" }, + { url = "https://files.pythonhosted.org/packages/32/f9/eaca4633486b527ebe7e681c431f529b63fe2709e7c5242fc0f43f77ce63/jiter-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8f8a7e317190b2c2d60eb2e8aa835270b008139562d70fe732e1c0020ec53c9", size = 316435, upload-time = "2025-11-09T20:47:02.087Z" }, + { url = "https://files.pythonhosted.org/packages/10/c1/40c9f7c22f5e6ff715f28113ebaba27ab85f9af2660ad6e1dd6425d14c19/jiter-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2218228a077e784c6c8f1a8e5d6b8cb1dea62ce25811c356364848554b2056cd", size = 320548, upload-time = "2025-11-09T20:47:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1b/efbb68fe87e7711b00d2cfd1f26bb4bfc25a10539aefeaa7727329ffb9cb/jiter-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9354ccaa2982bf2188fd5f57f79f800ef622ec67beb8329903abf6b10da7d423", size = 351915, upload-time = "2025-11-09T20:47:05.171Z" }, + { url = "https://files.pythonhosted.org/packages/15/2d/c06e659888c128ad1e838123d0638f0efad90cc30860cb5f74dd3f2fc0b3/jiter-0.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2607185ea89b4af9a604d4c7ec40e45d3ad03ee66998b031134bc510232bb7", size = 368966, upload-time = "2025-11-09T20:47:06.508Z" }, + { url = "https://files.pythonhosted.org/packages/6b/20/058db4ae5fb07cf6a4ab2e9b9294416f606d8e467fb74c2184b2a1eeacba/jiter-0.12.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a585a5e42d25f2e71db5f10b171f5e5ea641d3aa44f7df745aa965606111cc2", size = 482047, upload-time = "2025-11-09T20:47:08.382Z" }, + { url = "https://files.pythonhosted.org/packages/49/bb/dc2b1c122275e1de2eb12905015d61e8316b2f888bdaac34221c301495d6/jiter-0.12.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd9e21d34edff5a663c631f850edcb786719c960ce887a5661e9c828a53a95d9", size = 380835, upload-time = "2025-11-09T20:47:09.81Z" }, + { url = "https://files.pythonhosted.org/packages/23/7d/38f9cd337575349de16da575ee57ddb2d5a64d425c9367f5ef9e4612e32e/jiter-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a612534770470686cd5431478dc5a1b660eceb410abade6b1b74e320ca98de6", size = 364587, upload-time = "2025-11-09T20:47:11.529Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a3/b13e8e61e70f0bb06085099c4e2462647f53cc2ca97614f7fedcaa2bb9f3/jiter-0.12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3985aea37d40a908f887b34d05111e0aae822943796ebf8338877fee2ab67725", size = 390492, upload-time = "2025-11-09T20:47:12.993Z" }, + { url = "https://files.pythonhosted.org/packages/07/71/e0d11422ed027e21422f7bc1883c61deba2d9752b720538430c1deadfbca/jiter-0.12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b1207af186495f48f72529f8d86671903c8c10127cac6381b11dddc4aaa52df6", size = 522046, upload-time = "2025-11-09T20:47:14.6Z" }, + { url = "https://files.pythonhosted.org/packages/9f/59/b968a9aa7102a8375dbbdfbd2aeebe563c7e5dddf0f47c9ef1588a97e224/jiter-0.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef2fb241de583934c9915a33120ecc06d94aa3381a134570f59eed784e87001e", size = 513392, upload-time = "2025-11-09T20:47:16.011Z" }, + { url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449, upload-time = "2025-11-09T20:47:22.999Z" }, + { url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855, upload-time = "2025-11-09T20:47:24.779Z" }, + { url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171, upload-time = "2025-11-09T20:47:26.469Z" }, + { url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590, upload-time = "2025-11-09T20:47:27.918Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462, upload-time = "2025-11-09T20:47:29.654Z" }, + { url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983, upload-time = "2025-11-09T20:47:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328, upload-time = "2025-11-09T20:47:33.286Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740, upload-time = "2025-11-09T20:47:34.703Z" }, + { url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875, upload-time = "2025-11-09T20:47:36.058Z" }, + { url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457, upload-time = "2025-11-09T20:47:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658, upload-time = "2025-11-09T20:47:44.424Z" }, + { url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605, upload-time = "2025-11-09T20:47:45.973Z" }, + { url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803, upload-time = "2025-11-09T20:47:47.535Z" }, + { url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120, upload-time = "2025-11-09T20:47:49.284Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918, upload-time = "2025-11-09T20:47:50.807Z" }, + { url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008, upload-time = "2025-11-09T20:47:52.211Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785, upload-time = "2025-11-09T20:47:53.512Z" }, + { url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108, upload-time = "2025-11-09T20:47:54.893Z" }, + { url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937, upload-time = "2025-11-09T20:47:56.253Z" }, + { url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853, upload-time = "2025-11-09T20:47:58.32Z" }, + { url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965, upload-time = "2025-11-09T20:48:03.783Z" }, + { url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831, upload-time = "2025-11-09T20:48:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272, upload-time = "2025-11-09T20:48:06.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478, upload-time = "2025-11-09T20:48:10.898Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706, upload-time = "2025-11-09T20:48:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894, upload-time = "2025-11-09T20:48:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714, upload-time = "2025-11-09T20:48:15.083Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989, upload-time = "2025-11-09T20:48:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615, upload-time = "2025-11-09T20:48:18.614Z" }, + { url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745, upload-time = "2025-11-09T20:48:20.117Z" }, + { url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502, upload-time = "2025-11-09T20:48:21.543Z" }, + { url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845, upload-time = "2025-11-09T20:48:22.964Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701, upload-time = "2025-11-09T20:48:24.483Z" }, + { url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974, upload-time = "2025-11-09T20:48:30.87Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932, upload-time = "2025-11-09T20:48:32.658Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243, upload-time = "2025-11-09T20:48:34.093Z" }, + { url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315, upload-time = "2025-11-09T20:48:35.507Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714, upload-time = "2025-11-09T20:48:40.014Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168, upload-time = "2025-11-09T20:48:41.462Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893, upload-time = "2025-11-09T20:48:42.921Z" }, + { url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828, upload-time = "2025-11-09T20:48:44.278Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009, upload-time = "2025-11-09T20:48:45.726Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/5339ef1ecaa881c6948669956567a64d2670941925f245c434f494ffb0e5/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:4739a4657179ebf08f85914ce50332495811004cc1747852e8b2041ed2aab9b8", size = 311144, upload-time = "2025-11-09T20:49:10.503Z" }, + { url = "https://files.pythonhosted.org/packages/27/74/3446c652bffbd5e81ab354e388b1b5fc1d20daac34ee0ed11ff096b1b01a/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:41da8def934bf7bec16cb24bd33c0ca62126d2d45d81d17b864bd5ad721393c3", size = 305877, upload-time = "2025-11-09T20:49:12.269Z" }, + { url = "https://files.pythonhosted.org/packages/a1/f4/ed76ef9043450f57aac2d4fbeb27175aa0eb9c38f833be6ef6379b3b9a86/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c44ee814f499c082e69872d426b624987dbc5943ab06e9bbaa4f81989fdb79e", size = 340419, upload-time = "2025-11-09T20:49:13.803Z" }, + { url = "https://files.pythonhosted.org/packages/21/01/857d4608f5edb0664aa791a3d45702e1a5bcfff9934da74035e7b9803846/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2097de91cf03eaa27b3cbdb969addf83f0179c6afc41bbc4513705e013c65d", size = 347212, upload-time = "2025-11-09T20:49:15.643Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974, upload-time = "2025-11-09T20:49:17.187Z" }, + { url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233, upload-time = "2025-11-09T20:49:18.734Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537, upload-time = "2025-11-09T20:49:20.317Z" }, + { url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110, upload-time = "2025-11-09T20:49:21.817Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, ] [[package]] name = "jsonschema" -version = "4.25.1" +version = "4.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -1519,9 +2122,9 @@ dependencies = [ { name = "referencing", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "rpds-py", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, ] [[package]] @@ -1538,7 +2141,7 @@ wheels = [ [[package]] name = "kombu" -version = "5.5.4" +version = "5.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "amqp", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -1546,9 +2149,9 @@ dependencies = [ { name = "tzdata", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "vine", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0f/d3/5ff936d8319ac86b9c409f1501b07c426e6ad41966fedace9ef1b966e23f/kombu-5.5.4.tar.gz", hash = "sha256:886600168275ebeada93b888e831352fe578168342f0d1d5833d88ba0d847363", size = 461992, upload-time = "2025-06-01T10:19:22.281Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/a5/607e533ed6c83ae1a696969b8e1c137dfebd5759a2e9682e26ff1b97740b/kombu-5.6.2.tar.gz", hash = "sha256:8060497058066c6f5aed7c26d7cd0d3b574990b09de842a8c5aaed0b92cc5a55", size = 472594, upload-time = "2025-12-29T20:30:07.779Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/70/a07dcf4f62598c8ad579df241af55ced65bed76e42e45d3c368a6d82dbc1/kombu-5.5.4-py3-none-any.whl", hash = "sha256:a12ed0557c238897d8e518f1d1fdf84bd1516c5e305af2dacd85c2015115feb8", size = 210034, upload-time = "2025-06-01T10:19:20.436Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0f/834427d8c03ff1d7e867d3db3d176470c64871753252b21b4f4897d1fa45/kombu-5.6.2-py3-none-any.whl", hash = "sha256:efcfc559da324d41d61ca311b0c64965ea35b4c55cc04ee36e55386145dace93", size = 214219, upload-time = "2025-12-29T20:30:05.74Z" }, ] [package.optional-dependencies] @@ -1565,6 +2168,194 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/0e/72/a3add0e4eec4eb9e2569554f7c70f4a3c27712f40e3284d483e88094cc0e/langdetect-1.0.9.tar.gz", hash = "sha256:cbc1fef89f8d062739774bd51eda3da3274006b3661d199c2655f6b3f6d605a0", size = 981474, upload-time = "2021-05-07T07:54:13.562Z" } +[[package]] +name = "librt" +version = "0.7.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323, upload-time = "2026-01-14T12:56:16.876Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/13/57b06758a13550c5f09563893b004f98e9537ee6ec67b7df85c3571c8832/librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d", size = 56521, upload-time = "2026-01-14T12:54:40.066Z" }, + { url = "https://files.pythonhosted.org/packages/c2/24/bbea34d1452a10612fb45ac8356f95351ba40c2517e429602160a49d1fd0/librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b", size = 58456, upload-time = "2026-01-14T12:54:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/04/72/a168808f92253ec3a810beb1eceebc465701197dbc7e865a1c9ceb3c22c7/librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d", size = 164392, upload-time = "2026-01-14T12:54:42.843Z" }, + { url = "https://files.pythonhosted.org/packages/14/5c/4c0d406f1b02735c2e7af8ff1ff03a6577b1369b91aa934a9fa2cc42c7ce/librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d", size = 172959, upload-time = "2026-01-14T12:54:44.602Z" }, + { url = "https://files.pythonhosted.org/packages/82/5f/3e85351c523f73ad8d938989e9a58c7f59fb9c17f761b9981b43f0025ce7/librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c", size = 186717, upload-time = "2026-01-14T12:54:45.986Z" }, + { url = "https://files.pythonhosted.org/packages/08/f8/18bfe092e402d00fe00d33aa1e01dda1bd583ca100b393b4373847eade6d/librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c", size = 184585, upload-time = "2026-01-14T12:54:47.139Z" }, + { url = "https://files.pythonhosted.org/packages/4e/fc/f43972ff56fd790a9fa55028a52ccea1875100edbb856b705bd393b601e3/librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d", size = 180497, upload-time = "2026-01-14T12:54:48.946Z" }, + { url = "https://files.pythonhosted.org/packages/e1/3a/25e36030315a410d3ad0b7d0f19f5f188e88d1613d7d3fd8150523ea1093/librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0", size = 200052, upload-time = "2026-01-14T12:54:50.382Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a3/87ea9c1049f2c781177496ebee29430e4631f439b8553a4969c88747d5d8/librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f", size = 56507, upload-time = "2026-01-14T12:54:54.156Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4a/23bcef149f37f771ad30203d561fcfd45b02bc54947b91f7a9ac34815747/librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac", size = 58455, upload-time = "2026-01-14T12:54:55.978Z" }, + { url = "https://files.pythonhosted.org/packages/22/6e/46eb9b85c1b9761e0f42b6e6311e1cc544843ac897457062b9d5d0b21df4/librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c", size = 164956, upload-time = "2026-01-14T12:54:57.311Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3f/aa7c7f6829fb83989feb7ba9aa11c662b34b4bd4bd5b262f2876ba3db58d/librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8", size = 174364, upload-time = "2026-01-14T12:54:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/3f/2d/d57d154b40b11f2cb851c4df0d4c4456bacd9b1ccc4ecb593ddec56c1a8b/librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff", size = 188034, upload-time = "2026-01-14T12:55:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/59/f9/36c4dad00925c16cd69d744b87f7001792691857d3b79187e7a673e812fb/librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3", size = 186295, upload-time = "2026-01-14T12:55:01.303Z" }, + { url = "https://files.pythonhosted.org/packages/23/9b/8a9889d3df5efb67695a67785028ccd58e661c3018237b73ad081691d0cb/librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75", size = 181470, upload-time = "2026-01-14T12:55:02.492Z" }, + { url = "https://files.pythonhosted.org/packages/43/64/54d6ef11afca01fef8af78c230726a9394759f2addfbf7afc5e3cc032a45/librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873", size = 201713, upload-time = "2026-01-14T12:55:03.919Z" }, + { url = "https://files.pythonhosted.org/packages/56/04/79d8fcb43cae376c7adbab7b2b9f65e48432c9eced62ac96703bcc16e09b/librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63", size = 57472, upload-time = "2026-01-14T12:55:08.528Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ba/60b96e93043d3d659da91752689023a73981336446ae82078cddf706249e/librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93", size = 58986, upload-time = "2026-01-14T12:55:09.466Z" }, + { url = "https://files.pythonhosted.org/packages/7c/26/5215e4cdcc26e7be7eee21955a7e13cbf1f6d7d7311461a6014544596fac/librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592", size = 168422, upload-time = "2026-01-14T12:55:10.499Z" }, + { url = "https://files.pythonhosted.org/packages/0f/84/e8d1bc86fa0159bfc24f3d798d92cafd3897e84c7fea7fe61b3220915d76/librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850", size = 177478, upload-time = "2026-01-14T12:55:11.577Z" }, + { url = "https://files.pythonhosted.org/packages/57/11/d0268c4b94717a18aa91df1100e767b010f87b7ae444dafaa5a2d80f33a6/librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62", size = 192439, upload-time = "2026-01-14T12:55:12.7Z" }, + { url = "https://files.pythonhosted.org/packages/8d/56/1e8e833b95fe684f80f8894ae4d8b7d36acc9203e60478fcae599120a975/librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b", size = 191483, upload-time = "2026-01-14T12:55:13.838Z" }, + { url = "https://files.pythonhosted.org/packages/17/48/f11cf28a2cb6c31f282009e2208312aa84a5ee2732859f7856ee306176d5/librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714", size = 185376, upload-time = "2026-01-14T12:55:15.017Z" }, + { url = "https://files.pythonhosted.org/packages/b8/6a/d7c116c6da561b9155b184354a60a3d5cdbf08fc7f3678d09c95679d13d9/librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449", size = 206234, upload-time = "2026-01-14T12:55:16.571Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fe/b1f9de2829cf7fc7649c1dcd202cfd873837c5cc2fc9e526b0e7f716c3d2/librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc", size = 57500, upload-time = "2026-01-14T12:55:21.219Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d4/4a60fbe2e53b825f5d9a77325071d61cd8af8506255067bf0c8527530745/librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2", size = 59019, upload-time = "2026-01-14T12:55:22.256Z" }, + { url = "https://files.pythonhosted.org/packages/6a/37/61ff80341ba5159afa524445f2d984c30e2821f31f7c73cf166dcafa5564/librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3", size = 169015, upload-time = "2026-01-14T12:55:23.24Z" }, + { url = "https://files.pythonhosted.org/packages/1c/86/13d4f2d6a93f181ebf2fc953868826653ede494559da8268023fe567fca3/librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6", size = 178161, upload-time = "2026-01-14T12:55:24.826Z" }, + { url = "https://files.pythonhosted.org/packages/88/26/e24ef01305954fc4d771f1f09f3dd682f9eb610e1bec188ffb719374d26e/librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d", size = 193015, upload-time = "2026-01-14T12:55:26.04Z" }, + { url = "https://files.pythonhosted.org/packages/88/a0/92b6bd060e720d7a31ed474d046a69bd55334ec05e9c446d228c4b806ae3/librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e", size = 192038, upload-time = "2026-01-14T12:55:27.208Z" }, + { url = "https://files.pythonhosted.org/packages/06/bb/6f4c650253704279c3a214dad188101d1b5ea23be0606628bc6739456624/librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca", size = 186006, upload-time = "2026-01-14T12:55:28.594Z" }, + { url = "https://files.pythonhosted.org/packages/dc/00/1c409618248d43240cadf45f3efb866837fa77e9a12a71481912135eb481/librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93", size = 206888, upload-time = "2026-01-14T12:55:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/1a/73/fa8814c6ce2d49c3827829cadaa1589b0bf4391660bd4510899393a23ebc/librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418", size = 57049, upload-time = "2026-01-14T12:55:35.056Z" }, + { url = "https://files.pythonhosted.org/packages/53/fe/f6c70956da23ea235fd2e3cc16f4f0b4ebdfd72252b02d1164dd58b4e6c3/librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611", size = 58689, upload-time = "2026-01-14T12:55:36.078Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4d/7a2481444ac5fba63050d9abe823e6bc16896f575bfc9c1e5068d516cdce/librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758", size = 166808, upload-time = "2026-01-14T12:55:37.595Z" }, + { url = "https://files.pythonhosted.org/packages/ac/3c/10901d9e18639f8953f57c8986796cfbf4c1c514844a41c9197cf87cb707/librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea", size = 175614, upload-time = "2026-01-14T12:55:38.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/01/5cbdde0951a5090a80e5ba44e6357d375048123c572a23eecfb9326993a7/librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac", size = 189955, upload-time = "2026-01-14T12:55:39.939Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b4/e80528d2f4b7eaf1d437fcbd6fc6ba4cbeb3e2a0cb9ed5a79f47c7318706/librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398", size = 189370, upload-time = "2026-01-14T12:55:41.057Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ab/938368f8ce31a9787ecd4becb1e795954782e4312095daf8fd22420227c8/librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81", size = 183224, upload-time = "2026-01-14T12:55:42.328Z" }, + { url = "https://files.pythonhosted.org/packages/3c/10/559c310e7a6e4014ac44867d359ef8238465fb499e7eb31b6bfe3e3f86f5/librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83", size = 203541, upload-time = "2026-01-14T12:55:43.501Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9c/2481d80950b83085fb14ba3c595db56330d21bbc7d88a19f20165f3538db/librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f", size = 59161, upload-time = "2026-01-14T12:55:48.45Z" }, + { url = "https://files.pythonhosted.org/packages/96/79/108df2cfc4e672336765d54e3ff887294c1cc36ea4335c73588875775527/librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde", size = 61008, upload-time = "2026-01-14T12:55:49.527Z" }, + { url = "https://files.pythonhosted.org/packages/46/f2/30179898f9994a5637459d6e169b6abdc982012c0a4b2d4c26f50c06f911/librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e", size = 187199, upload-time = "2026-01-14T12:55:50.587Z" }, + { url = "https://files.pythonhosted.org/packages/b4/da/f7563db55cebdc884f518ba3791ad033becc25ff68eb70902b1747dc0d70/librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b", size = 198317, upload-time = "2026-01-14T12:55:51.991Z" }, + { url = "https://files.pythonhosted.org/packages/b3/6c/4289acf076ad371471fa86718c30ae353e690d3de6167f7db36f429272f1/librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666", size = 210334, upload-time = "2026-01-14T12:55:53.682Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7f/377521ac25b78ac0a5ff44127a0360ee6d5ddd3ce7327949876a30533daa/librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581", size = 211031, upload-time = "2026-01-14T12:55:54.827Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/e1e96c3e20b23d00cf90f4aad48f0deb4cdfec2f0ed8380d0d85acf98bbf/librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a", size = 204581, upload-time = "2026-01-14T12:55:56.811Z" }, + { url = "https://files.pythonhosted.org/packages/43/71/0f5d010e92ed9747e14bef35e91b6580533510f1e36a8a09eb79ee70b2f0/librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca", size = 224731, upload-time = "2026-01-14T12:55:58.175Z" }, +] + +[[package]] +name = "llama-index-core" +version = "0.14.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "aiosqlite", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "banks", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "dataclasses-json", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "deprecated", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "dirtyjson", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "filetype", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "fsspec", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "llama-index-workflows", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "nest-asyncio", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux')" }, + { name = "nltk", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, + { name = "numpy", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux')" }, + { name = "pillow", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "platformdirs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "setuptools", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "sqlalchemy", extra = ["asyncio"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "tenacity", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "tiktoken", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-inspect", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "wrapt", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/54/d6043a088e5e9c1d62300db7ad0ef417c6b9a92f7b4a5cade066aeafdaca/llama_index_core-0.14.13.tar.gz", hash = "sha256:c3b30d20ae0407e5d0a1d35bb3376a98e242661ebfc22da754b5a3da1f8108c0", size = 11589074, upload-time = "2026-01-21T20:44:16.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/59/9769f03f1cccadcc014b3b65c166de18999b51459a0f0a579d80f6c91d80/llama_index_core-0.14.13-py3-none-any.whl", hash = "sha256:392f0a5a09433e9dea786964ef5fe5ca2a2b10aee9f979a9507c19a14da2a20a", size = 11934761, upload-time = "2026-01-21T20:44:18.892Z" }, +] + +[[package]] +name = "llama-index-embeddings-huggingface" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "llama-index-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "sentence-transformers", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/a0/77beca4ed28af68db6ab9c647b3fa75fae905d33ace96e91010cc9b96027/llama_index_embeddings_huggingface-0.6.1.tar.gz", hash = "sha256:3b21ffeda22f8221ed55778bb3daed71664ab07b341f1dd2f408963bd20355b9", size = 8694, upload-time = "2025-09-08T20:25:27.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/b0/4d327cb2f2e039606feb9345b4de975090b9659c4ee84b00443bb149a80d/llama_index_embeddings_huggingface-0.6.1-py3-none-any.whl", hash = "sha256:b63990cf71ee7a36c51f36657133fcf76130e9bf5dcf9eb5a73a5087106d6881", size = 8903, upload-time = "2025-09-08T20:25:27.038Z" }, +] + +[[package]] +name = "llama-index-embeddings-openai" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/36/90336d054a5061a3f5bc17ac2c18ef63d9d84c55c14d557de484e811ea4d/llama_index_embeddings_openai-0.5.1.tar.gz", hash = "sha256:1c89867a48b0d0daa3d2d44f5e76b394b2b2ef9935932daf921b9e77939ccda8", size = 7020, upload-time = "2025-09-08T20:17:44.681Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/4a/8ab11026cf8deff8f555aa73919be0bac48332683111e5fc4290f352dc50/llama_index_embeddings_openai-0.5.1-py3-none-any.whl", hash = "sha256:a2fcda3398bbd987b5ce3f02367caee8e84a56b930fdf43cc1d059aa9fd20ca5", size = 7011, upload-time = "2025-09-08T20:17:44.015Z" }, +] + +[[package]] +name = "llama-index-instrumentation" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/b9/a7a74de6d8aacf4be329329495983d78d96b1a6e69b6d9fcf4a233febd4b/llama_index_instrumentation-0.4.2.tar.gz", hash = "sha256:dc4957b64da0922060690e85a6be9698ac08e34e0f69e90b01364ddec4f3de7f", size = 46146, upload-time = "2025-10-13T20:44:48.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/54/df8063b0441242e250e03d1e31ebde5dffbe24e1af32b025cb1a4544150c/llama_index_instrumentation-0.4.2-py3-none-any.whl", hash = "sha256:b4989500e6454059ab3f3c4a193575d47ab1fadb730c2e8f2b962649ae88b70b", size = 15411, upload-time = "2025-10-13T20:44:47.685Z" }, +] + +[[package]] +name = "llama-index-llms-ollama" +version = "0.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "ollama", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/42/866e43f86ebe6c8ae0c3a2a473642f1438d0f7e006188a998c3ef4e5e357/llama_index_llms_ollama-0.9.1.tar.gz", hash = "sha256:d5885ed65ae2e2bc74ba9e3ffd3a5bcd7c5341ef0670e3d9fe200880fc19f9a6", size = 9077, upload-time = "2025-12-19T03:24:46.466Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/cd/aa2499dc38c1cc513cab56f9a74e5526d1a97717f35a62295fda0d363bf6/llama_index_llms_ollama-0.9.1-py3-none-any.whl", hash = "sha256:da6469be951605841f7e33ce1bbb5b72c65de0f084b250e7f78aacac84379d5f", size = 8721, upload-time = "2025-12-19T03:24:47.438Z" }, +] + +[[package]] +name = "llama-index-llms-openai" +version = "0.6.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/66/71d2e7fb2dcf077d02065346969ccd84a38188f399ac5835408ccfce7c2e/llama_index_llms_openai-0.6.15.tar.gz", hash = "sha256:5bd059ea44412e927743a98bb1e5b84b2e194bb396ef959527fc3a8ac80b025d", size = 25856, upload-time = "2026-01-26T12:07:15.727Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/18/47190a84b7085536669161c2bd932f1e187d0ea4526c5e3e4c690dcb7cd4/llama_index_llms_openai-0.6.15-py3-none-any.whl", hash = "sha256:b4ef1756a0815e7d930678ba8c6e69f56aea0bc8ca5372759fbb90f678bbff3d", size = 26874, upload-time = "2026-01-26T12:07:14.614Z" }, +] + +[[package]] +name = "llama-index-vector-stores-faiss" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/5f/c4ae340f178f202cf09dcc24dd0953a41d9ab24bc33e1f7220544ba86e41/llama_index_vector_stores_faiss-0.5.2.tar.gz", hash = "sha256:924504765e68b1f84ec602feb2d9516be6a6c12fad5e133f19cc5da3b23f4282", size = 5910, upload-time = "2025-12-17T21:01:13.21Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/c1/c8317250c2a83d1d439814d1a7f41fa34a23c224b3099da898f08a249859/llama_index_vector_stores_faiss-0.5.2-py3-none-any.whl", hash = "sha256:72a3a03d9f25c70bbcc8c61aa860cd1db69f2a8070606ecc3266d767b71ff2a2", size = 7605, upload-time = "2025-12-17T21:01:12.429Z" }, +] + +[[package]] +name = "llama-index-workflows" +version = "2.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-instrumentation", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/7b/00c35d14dc4a7dc64c63dad8b1532f55cd5b5f8856c34f5bf587693ac270/llama_index_workflows-2.13.1.tar.gz", hash = "sha256:55cd3cff9c92a37272ab8651ad750288abc339165b009066f130cb0c5c65994b", size = 84279, upload-time = "2026-01-25T14:51:50.493Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/8d/ba97cda62829b60658a86d4c012d59e3b13c99d9b336fcf0e507d03812df/llama_index_workflows-2.13.1-py3-none-any.whl", hash = "sha256:e779078817d413b29a5297521fb71694a80e502f18dfd41e8b342f83e45f2c19", size = 107344, upload-time = "2026-01-25T14:51:49.297Z" }, +] + [[package]] name = "lxml" version = "6.0.2" @@ -1680,11 +2471,11 @@ wheels = [ [[package]] name = "markdown" -version = "3.9" +version = "3.10.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8d/37/02347f6d6d8279247a5837082ebc26fc0d5aaeaf75aa013fcbb433c777ab/markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a", size = 364585, upload-time = "2025-09-04T20:25:22.885Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/b1/af95bcae8549f1f3fd70faacb29075826a0d689a27f232e8cee315efa053/markdown-3.10.1.tar.gz", hash = "sha256:1c19c10bd5c14ac948c53d0d762a04e2fa35a6d58a6b7b1e6bfcbe6fefc0001a", size = 365402, upload-time = "2026-01-21T18:09:28.206Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/ae/44c4a6a4cbb496d93c6257954260fe3a6e91b7bed2240e5dad2a717f5111/markdown-3.9-py3-none-any.whl", hash = "sha256:9f4d91ed810864ea88a6f32c07ba8bee1346c0cc1f6b1f9f6c822f2a9667d280", size = 107441, upload-time = "2025-09-04T20:25:21.784Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/6ef961f543593969d25b2afe57a3564200280528caa9bd1082eecdd7b3bc/markdown-3.10.1-py3-none-any.whl", hash = "sha256:867d788939fe33e4b736426f5b9f651ad0c0ae0ecf89df0ca5d1176c70812fe3", size = 107684, upload-time = "2026-01-21T18:09:27.203Z" }, ] [[package]] @@ -1763,6 +2554,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, ] +[[package]] +name = "marshmallow" +version = "3.26.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/79/de6c16cc902f4fc372236926b0ce2ab7845268dcc30fb2fbb7f71b418631/marshmallow-3.26.2.tar.gz", hash = "sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57", size = 222095, upload-time = "2025-12-22T06:53:53.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/2f/5108cb3ee4ba6501748c4908b908e55f42a5b66245b4cfe0c99326e1ef6e/marshmallow-3.26.2-py3-none-any.whl", hash = "sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73", size = 50964, upload-time = "2025-12-22T06:53:51.801Z" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -1832,7 +2635,7 @@ wheels = [ [[package]] name = "mkdocs-material" -version = "9.7.0" +version = "9.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -1847,9 +2650,9 @@ dependencies = [ { name = "pymdown-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9c/3b/111b84cd6ff28d9e955b5f799ef217a17bc1684ac346af333e6100e413cb/mkdocs_material-9.7.0.tar.gz", hash = "sha256:602b359844e906ee402b7ed9640340cf8a474420d02d8891451733b6b02314ec", size = 4094546, upload-time = "2025-11-11T08:49:09.73Z" } +sdist = { url = "https://files.pythonhosted.org/packages/27/e2/2ffc356cd72f1473d07c7719d82a8f2cbd261666828614ecb95b12169f41/mkdocs_material-9.7.1.tar.gz", hash = "sha256:89601b8f2c3e6c6ee0a918cc3566cb201d40bf37c3cd3c2067e26fadb8cce2b8", size = 4094392, upload-time = "2025-12-18T09:49:00.308Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/87/eefe8d5e764f4cf50ed91b943f8e8f96b5efd65489d8303b7a36e2e79834/mkdocs_material-9.7.0-py3-none-any.whl", hash = "sha256:da2866ea53601125ff5baa8aa06404c6e07af3c5ce3d5de95e3b52b80b442887", size = 9283770, upload-time = "2025-11-11T08:49:06.26Z" }, + { url = "https://files.pythonhosted.org/packages/3e/32/ed071cb721aca8c227718cffcf7bd539620e9799bbf2619e90c757bfd030/mkdocs_material-9.7.1-py3-none-any.whl", hash = "sha256:3f6100937d7d731f87f1e3e3b021c97f7239666b9ba1151ab476cabb96c60d5c", size = 9297166, upload-time = "2025-12-18T09:48:56.664Z" }, ] [[package]] @@ -1862,83 +2665,214 @@ wheels = [ ] [[package]] -name = "msgpack" -version = "1.1.1" +name = "mpmath" +version = "1.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/45/b1/ea4f68038a18c77c9467400d166d74c4ffa536f34761f7983a104357e614/msgpack-1.1.1.tar.gz", hash = "sha256:77b79ce34a2bdab2594f490c8e80dd62a02d650b91a75159a63ec413b8d104cd", size = 173555, upload-time = "2025-06-13T06:52:51.324Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/52/f30da112c1dc92cf64f57d08a273ac771e7b29dea10b4b30369b2d7e8546/msgpack-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:353b6fc0c36fde68b661a12949d7d49f8f51ff5fa019c1e47c87c4ff34b080ed", size = 81799, upload-time = "2025-06-13T06:51:37.228Z" }, - { url = "https://files.pythonhosted.org/packages/e4/35/7bfc0def2f04ab4145f7f108e3563f9b4abae4ab0ed78a61f350518cc4d2/msgpack-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:79c408fcf76a958491b4e3b103d1c417044544b68e96d06432a189b43d1215c8", size = 78278, upload-time = "2025-06-13T06:51:38.534Z" }, - { url = "https://files.pythonhosted.org/packages/e8/c5/df5d6c1c39856bc55f800bf82778fd4c11370667f9b9e9d51b2f5da88f20/msgpack-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78426096939c2c7482bf31ef15ca219a9e24460289c00dd0b94411040bb73ad2", size = 402805, upload-time = "2025-06-13T06:51:39.538Z" }, - { url = "https://files.pythonhosted.org/packages/20/8e/0bb8c977efecfe6ea7116e2ed73a78a8d32a947f94d272586cf02a9757db/msgpack-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b17ba27727a36cb73aabacaa44b13090feb88a01d012c0f4be70c00f75048b4", size = 408642, upload-time = "2025-06-13T06:51:41.092Z" }, - { url = "https://files.pythonhosted.org/packages/59/a1/731d52c1aeec52006be6d1f8027c49fdc2cfc3ab7cbe7c28335b2910d7b6/msgpack-1.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a17ac1ea6ec3c7687d70201cfda3b1e8061466f28f686c24f627cae4ea8efd0", size = 395143, upload-time = "2025-06-13T06:51:42.575Z" }, - { url = "https://files.pythonhosted.org/packages/2b/92/b42911c52cda2ba67a6418ffa7d08969edf2e760b09015593c8a8a27a97d/msgpack-1.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:88d1e966c9235c1d4e2afac21ca83933ba59537e2e2727a999bf3f515ca2af26", size = 395986, upload-time = "2025-06-13T06:51:43.807Z" }, - { url = "https://files.pythonhosted.org/packages/61/dc/8ae165337e70118d4dab651b8b562dd5066dd1e6dd57b038f32ebc3e2f07/msgpack-1.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f6d58656842e1b2ddbe07f43f56b10a60f2ba5826164910968f5933e5178af75", size = 402682, upload-time = "2025-06-13T06:51:45.534Z" }, - { url = "https://files.pythonhosted.org/packages/58/27/555851cb98dcbd6ce041df1eacb25ac30646575e9cd125681aa2f4b1b6f1/msgpack-1.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:96decdfc4adcbc087f5ea7ebdcfd3dee9a13358cae6e81d54be962efc38f6338", size = 406368, upload-time = "2025-06-13T06:51:46.97Z" }, - { url = "https://files.pythonhosted.org/packages/7f/83/97f24bf9848af23fe2ba04380388216defc49a8af6da0c28cc636d722502/msgpack-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:71ef05c1726884e44f8b1d1773604ab5d4d17729d8491403a705e649116c9558", size = 82728, upload-time = "2025-06-13T06:51:50.68Z" }, - { url = "https://files.pythonhosted.org/packages/aa/7f/2eaa388267a78401f6e182662b08a588ef4f3de6f0eab1ec09736a7aaa2b/msgpack-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:36043272c6aede309d29d56851f8841ba907a1a3d04435e43e8a19928e243c1d", size = 79279, upload-time = "2025-06-13T06:51:51.72Z" }, - { url = "https://files.pythonhosted.org/packages/f8/46/31eb60f4452c96161e4dfd26dbca562b4ec68c72e4ad07d9566d7ea35e8a/msgpack-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a32747b1b39c3ac27d0670122b57e6e57f28eefb725e0b625618d1b59bf9d1e0", size = 423859, upload-time = "2025-06-13T06:51:52.749Z" }, - { url = "https://files.pythonhosted.org/packages/45/16/a20fa8c32825cc7ae8457fab45670c7a8996d7746ce80ce41cc51e3b2bd7/msgpack-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a8b10fdb84a43e50d38057b06901ec9da52baac6983d3f709d8507f3889d43f", size = 429975, upload-time = "2025-06-13T06:51:53.97Z" }, - { url = "https://files.pythonhosted.org/packages/86/ea/6c958e07692367feeb1a1594d35e22b62f7f476f3c568b002a5ea09d443d/msgpack-1.1.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba0c325c3f485dc54ec298d8b024e134acf07c10d494ffa24373bea729acf704", size = 413528, upload-time = "2025-06-13T06:51:55.507Z" }, - { url = "https://files.pythonhosted.org/packages/75/05/ac84063c5dae79722bda9f68b878dc31fc3059adb8633c79f1e82c2cd946/msgpack-1.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:88daaf7d146e48ec71212ce21109b66e06a98e5e44dca47d853cbfe171d6c8d2", size = 413338, upload-time = "2025-06-13T06:51:57.023Z" }, - { url = "https://files.pythonhosted.org/packages/69/e8/fe86b082c781d3e1c09ca0f4dacd457ede60a13119b6ce939efe2ea77b76/msgpack-1.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8b55ea20dc59b181d3f47103f113e6f28a5e1c89fd5b67b9140edb442ab67f2", size = 422658, upload-time = "2025-06-13T06:51:58.419Z" }, - { url = "https://files.pythonhosted.org/packages/3b/2b/bafc9924df52d8f3bb7c00d24e57be477f4d0f967c0a31ef5e2225e035c7/msgpack-1.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4a28e8072ae9779f20427af07f53bbb8b4aa81151054e882aee333b158da8752", size = 427124, upload-time = "2025-06-13T06:51:59.969Z" }, - { url = "https://files.pythonhosted.org/packages/e3/26/389b9c593eda2b8551b2e7126ad3a06af6f9b44274eb3a4f054d48ff7e47/msgpack-1.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ae497b11f4c21558d95de9f64fff7053544f4d1a17731c866143ed6bb4591238", size = 82359, upload-time = "2025-06-13T06:52:03.909Z" }, - { url = "https://files.pythonhosted.org/packages/ab/65/7d1de38c8a22cf8b1551469159d4b6cf49be2126adc2482de50976084d78/msgpack-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:33be9ab121df9b6b461ff91baac6f2731f83d9b27ed948c5b9d1978ae28bf157", size = 79172, upload-time = "2025-06-13T06:52:05.246Z" }, - { url = "https://files.pythonhosted.org/packages/0f/bd/cacf208b64d9577a62c74b677e1ada005caa9b69a05a599889d6fc2ab20a/msgpack-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f64ae8fe7ffba251fecb8408540c34ee9df1c26674c50c4544d72dbf792e5ce", size = 425013, upload-time = "2025-06-13T06:52:06.341Z" }, - { url = "https://files.pythonhosted.org/packages/4d/ec/fd869e2567cc9c01278a736cfd1697941ba0d4b81a43e0aa2e8d71dab208/msgpack-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a494554874691720ba5891c9b0b39474ba43ffb1aaf32a5dac874effb1619e1a", size = 426905, upload-time = "2025-06-13T06:52:07.501Z" }, - { url = "https://files.pythonhosted.org/packages/55/2a/35860f33229075bce803a5593d046d8b489d7ba2fc85701e714fc1aaf898/msgpack-1.1.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb643284ab0ed26f6957d969fe0dd8bb17beb567beb8998140b5e38a90974f6c", size = 407336, upload-time = "2025-06-13T06:52:09.047Z" }, - { url = "https://files.pythonhosted.org/packages/8c/16/69ed8f3ada150bf92745fb4921bd621fd2cdf5a42e25eb50bcc57a5328f0/msgpack-1.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d275a9e3c81b1093c060c3837e580c37f47c51eca031f7b5fb76f7b8470f5f9b", size = 409485, upload-time = "2025-06-13T06:52:10.382Z" }, - { url = "https://files.pythonhosted.org/packages/c6/b6/0c398039e4c6d0b2e37c61d7e0e9d13439f91f780686deb8ee64ecf1ae71/msgpack-1.1.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fd6b577e4541676e0cc9ddc1709d25014d3ad9a66caa19962c4f5de30fc09ef", size = 412182, upload-time = "2025-06-13T06:52:11.644Z" }, - { url = "https://files.pythonhosted.org/packages/b8/d0/0cf4a6ecb9bc960d624c93effaeaae75cbf00b3bc4a54f35c8507273cda1/msgpack-1.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb29aaa613c0a1c40d1af111abf025f1732cab333f96f285d6a93b934738a68a", size = 419883, upload-time = "2025-06-13T06:52:12.806Z" }, - { url = "https://files.pythonhosted.org/packages/a1/38/561f01cf3577430b59b340b51329803d3a5bf6a45864a55f4ef308ac11e3/msgpack-1.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3765afa6bd4832fc11c3749be4ba4b69a0e8d7b728f78e68120a157a4c5d41f0", size = 81677, upload-time = "2025-06-13T06:52:16.64Z" }, - { url = "https://files.pythonhosted.org/packages/09/48/54a89579ea36b6ae0ee001cba8c61f776451fad3c9306cd80f5b5c55be87/msgpack-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8ddb2bcfd1a8b9e431c8d6f4f7db0773084e107730ecf3472f1dfe9ad583f3d9", size = 78603, upload-time = "2025-06-13T06:52:17.843Z" }, - { url = "https://files.pythonhosted.org/packages/a0/60/daba2699b308e95ae792cdc2ef092a38eb5ee422f9d2fbd4101526d8a210/msgpack-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:196a736f0526a03653d829d7d4c5500a97eea3648aebfd4b6743875f28aa2af8", size = 420504, upload-time = "2025-06-13T06:52:18.982Z" }, - { url = "https://files.pythonhosted.org/packages/20/22/2ebae7ae43cd8f2debc35c631172ddf14e2a87ffcc04cf43ff9df9fff0d3/msgpack-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d592d06e3cc2f537ceeeb23d38799c6ad83255289bb84c2e5792e5a8dea268a", size = 423749, upload-time = "2025-06-13T06:52:20.211Z" }, - { url = "https://files.pythonhosted.org/packages/40/1b/54c08dd5452427e1179a40b4b607e37e2664bca1c790c60c442c8e972e47/msgpack-1.1.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4df2311b0ce24f06ba253fda361f938dfecd7b961576f9be3f3fbd60e87130ac", size = 404458, upload-time = "2025-06-13T06:52:21.429Z" }, - { url = "https://files.pythonhosted.org/packages/2e/60/6bb17e9ffb080616a51f09928fdd5cac1353c9becc6c4a8abd4e57269a16/msgpack-1.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e4141c5a32b5e37905b5940aacbc59739f036930367d7acce7a64e4dec1f5e0b", size = 405976, upload-time = "2025-06-13T06:52:22.995Z" }, - { url = "https://files.pythonhosted.org/packages/ee/97/88983e266572e8707c1f4b99c8fd04f9eb97b43f2db40e3172d87d8642db/msgpack-1.1.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b1ce7f41670c5a69e1389420436f41385b1aa2504c3b0c30620764b15dded2e7", size = 408607, upload-time = "2025-06-13T06:52:24.152Z" }, - { url = "https://files.pythonhosted.org/packages/bc/66/36c78af2efaffcc15a5a61ae0df53a1d025f2680122e2a9eb8442fed3ae4/msgpack-1.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4147151acabb9caed4e474c3344181e91ff7a388b888f1e19ea04f7e73dc7ad5", size = 424172, upload-time = "2025-06-13T06:52:25.704Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "msgpack" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/a2/3b68a9e769db68668b25c6108444a35f9bd163bb848c0650d516761a59c0/msgpack-1.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2", size = 81318, upload-time = "2025-10-08T09:14:38.722Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e1/2b720cc341325c00be44e1ed59e7cfeae2678329fbf5aa68f5bda57fe728/msgpack-1.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a605409040f2da88676e9c9e5853b3449ba8011973616189ea5ee55ddbc5bc87", size = 83786, upload-time = "2025-10-08T09:14:40.082Z" }, + { url = "https://files.pythonhosted.org/packages/71/e5/c2241de64bfceac456b140737812a2ab310b10538a7b34a1d393b748e095/msgpack-1.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251", size = 398240, upload-time = "2025-10-08T09:14:41.151Z" }, + { url = "https://files.pythonhosted.org/packages/b7/09/2a06956383c0fdebaef5aa9246e2356776f12ea6f2a44bd1368abf0e46c4/msgpack-1.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a", size = 406070, upload-time = "2025-10-08T09:14:42.821Z" }, + { url = "https://files.pythonhosted.org/packages/0e/74/2957703f0e1ef20637d6aead4fbb314330c26f39aa046b348c7edcf6ca6b/msgpack-1.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41d1a5d875680166d3ac5c38573896453bbbea7092936d2e107214daf43b1d4f", size = 393403, upload-time = "2025-10-08T09:14:44.38Z" }, + { url = "https://files.pythonhosted.org/packages/a5/09/3bfc12aa90f77b37322fc33e7a8a7c29ba7c8edeadfa27664451801b9860/msgpack-1.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:354e81bcdebaab427c3df4281187edc765d5d76bfb3a7c125af9da7a27e8458f", size = 398947, upload-time = "2025-10-08T09:14:45.56Z" }, + { url = "https://files.pythonhosted.org/packages/2c/97/560d11202bcd537abca693fd85d81cebe2107ba17301de42b01ac1677b69/msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c", size = 82271, upload-time = "2025-10-08T09:14:49.967Z" }, + { url = "https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0", size = 84914, upload-time = "2025-10-08T09:14:50.958Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/b817349db6886d79e57a966346cf0902a426375aadc1e8e7a86a75e22f19/msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296", size = 416962, upload-time = "2025-10-08T09:14:51.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef", size = 426183, upload-time = "2025-10-08T09:14:53.477Z" }, + { url = "https://files.pythonhosted.org/packages/25/98/6a19f030b3d2ea906696cedd1eb251708e50a5891d0978b012cb6107234c/msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c", size = 411454, upload-time = "2025-10-08T09:14:54.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/cd/9098fcb6adb32187a70b7ecaabf6339da50553351558f37600e53a4a2a23/msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e", size = 422341, upload-time = "2025-10-08T09:14:56.328Z" }, + { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" }, + { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" }, + { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" }, + { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" }, + { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" }, + { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" }, + { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" }, + { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" }, + { url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" }, + { url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" }, + { url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" }, + { url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" }, + { url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" }, + { url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" }, + { url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" }, + { url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/0b/19348d4c98980c4851d2f943f8ebafdece2ae7ef737adcfa5994ce8e5f10/multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5", size = 77176, upload-time = "2026-01-26T02:42:59.784Z" }, + { url = "https://files.pythonhosted.org/packages/ef/04/9de3f8077852e3d438215c81e9b691244532d2e05b4270e89ce67b7d103c/multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8", size = 44996, upload-time = "2026-01-26T02:43:01.674Z" }, + { url = "https://files.pythonhosted.org/packages/31/5c/08c7f7fe311f32e83f7621cd3f99d805f45519cd06fafb247628b861da7d/multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872", size = 44631, upload-time = "2026-01-26T02:43:03.169Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7f/0e3b1390ae772f27501199996b94b52ceeb64fe6f9120a32c6c3f6b781be/multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991", size = 242561, upload-time = "2026-01-26T02:43:04.733Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f4/8719f4f167586af317b69dd3e90f913416c91ca610cac79a45c53f590312/multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03", size = 242223, upload-time = "2026-01-26T02:43:06.695Z" }, + { url = "https://files.pythonhosted.org/packages/47/ab/7c36164cce64a6ad19c6d9a85377b7178ecf3b89f8fd589c73381a5eedfd/multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981", size = 222322, upload-time = "2026-01-26T02:43:08.472Z" }, + { url = "https://files.pythonhosted.org/packages/f5/79/a25add6fb38035b5337bc5734f296d9afc99163403bbcf56d4170f97eb62/multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6", size = 254005, upload-time = "2026-01-26T02:43:10.127Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7b/64a87cf98e12f756fc8bd444b001232ffff2be37288f018ad0d3f0aae931/multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190", size = 251173, upload-time = "2026-01-26T02:43:11.731Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ac/b605473de2bb404e742f2cc3583d12aedb2352a70e49ae8fce455b50c5aa/multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92", size = 243273, upload-time = "2026-01-26T02:43:13.063Z" }, + { url = "https://files.pythonhosted.org/packages/03/65/11492d6a0e259783720f3bc1d9ea55579a76f1407e31ed44045c99542004/multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee", size = 238956, upload-time = "2026-01-26T02:43:14.843Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a7/7ee591302af64e7c196fb63fe856c788993c1372df765102bd0448e7e165/multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2", size = 233477, upload-time = "2026-01-26T02:43:16.025Z" }, + { url = "https://files.pythonhosted.org/packages/9c/99/c109962d58756c35fd9992fed7f2355303846ea2ff054bb5f5e9d6b888de/multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568", size = 243615, upload-time = "2026-01-26T02:43:17.84Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5f/1973e7c771c86e93dcfe1c9cc55a5481b610f6614acfc28c0d326fe6bfad/multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40", size = 249930, upload-time = "2026-01-26T02:43:19.06Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a5/f170fc2268c3243853580203378cd522446b2df632061e0a5409817854c7/multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962", size = 243807, upload-time = "2026-01-26T02:43:20.286Z" }, + { url = "https://files.pythonhosted.org/packages/de/01/73856fab6d125e5bc652c3986b90e8699a95e84b48d72f39ade6c0e74a8c/multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505", size = 239103, upload-time = "2026-01-26T02:43:21.508Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" }, + { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" }, + { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, ] [[package]] name = "mypy" -version = "1.18.2" +version = "1.19.1" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "librt", marker = "(platform_python_implementation != 'PyPy' and sys_platform == 'darwin') or (platform_python_implementation != 'PyPy' and sys_platform == 'linux')" }, { name = "mypy-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "pathspec", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "tomli", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/6f/657961a0743cff32e6c0611b63ff1c1970a0b482ace35b069203bf705187/mypy-1.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c", size = 12807973, upload-time = "2025-09-19T00:10:35.282Z" }, - { url = "https://files.pythonhosted.org/packages/10/e9/420822d4f661f13ca8900f5fa239b40ee3be8b62b32f3357df9a3045a08b/mypy-1.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e", size = 11896527, upload-time = "2025-09-19T00:10:55.791Z" }, - { url = "https://files.pythonhosted.org/packages/aa/73/a05b2bbaa7005f4642fcfe40fb73f2b4fb6bb44229bd585b5878e9a87ef8/mypy-1.18.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b", size = 12507004, upload-time = "2025-09-19T00:11:05.411Z" }, - { url = "https://files.pythonhosted.org/packages/4f/01/f6e4b9f0d031c11ccbd6f17da26564f3a0f3c4155af344006434b0a05a9d/mypy-1.18.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66", size = 13245947, upload-time = "2025-09-19T00:10:46.923Z" }, - { url = "https://files.pythonhosted.org/packages/d7/97/19727e7499bfa1ae0773d06afd30ac66a58ed7437d940c70548634b24185/mypy-1.18.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428", size = 13499217, upload-time = "2025-09-19T00:09:39.472Z" }, - { url = "https://files.pythonhosted.org/packages/88/87/cafd3ae563f88f94eec33f35ff722d043e09832ea8530ef149ec1efbaf08/mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f", size = 12731198, upload-time = "2025-09-19T00:09:44.857Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e0/1e96c3d4266a06d4b0197ace5356d67d937d8358e2ee3ffac71faa843724/mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341", size = 11817879, upload-time = "2025-09-19T00:09:47.131Z" }, - { url = "https://files.pythonhosted.org/packages/72/ef/0c9ba89eb03453e76bdac5a78b08260a848c7bfc5d6603634774d9cd9525/mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d", size = 12427292, upload-time = "2025-09-19T00:10:22.472Z" }, - { url = "https://files.pythonhosted.org/packages/1a/52/ec4a061dd599eb8179d5411d99775bec2a20542505988f40fc2fee781068/mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86", size = 13163750, upload-time = "2025-09-19T00:09:51.472Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5f/2cf2ceb3b36372d51568f2208c021870fe7834cf3186b653ac6446511839/mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37", size = 13351827, upload-time = "2025-09-19T00:09:58.311Z" }, - { url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" }, - { url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" }, - { url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" }, - { url = "https://files.pythonhosted.org/packages/77/ae/6c3d2c7c61ff21f2bee938c917616c92ebf852f015fb55917fd6e2811db2/mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914", size = 13348562, upload-time = "2025-09-19T00:10:11.51Z" }, - { url = "https://files.pythonhosted.org/packages/4d/31/aec68ab3b4aebdf8f36d191b0685d99faa899ab990753ca0fee60fb99511/mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8", size = 13533296, upload-time = "2025-09-19T00:10:06.568Z" }, - { url = "https://files.pythonhosted.org/packages/5f/04/7f462e6fbba87a72bc8097b93f6842499c428a6ff0c81dd46948d175afe8/mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc", size = 12898728, upload-time = "2025-09-19T00:10:01.33Z" }, - { url = "https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e", size = 11910758, upload-time = "2025-09-19T00:10:42.607Z" }, - { url = "https://files.pythonhosted.org/packages/3c/46/d297d4b683cc89a6e4108c4250a6a6b717f5fa96e1a30a7944a6da44da35/mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986", size = 12475342, upload-time = "2025-09-19T00:11:00.371Z" }, - { url = "https://files.pythonhosted.org/packages/83/45/4798f4d00df13eae3bfdf726c9244bcb495ab5bd588c0eed93a2f2dd67f3/mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d", size = 13338709, upload-time = "2025-09-19T00:11:03.358Z" }, - { url = "https://files.pythonhosted.org/packages/d7/09/479f7358d9625172521a87a9271ddd2441e1dab16a09708f056e97007207/mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba", size = 13529806, upload-time = "2025-09-19T00:10:26.073Z" }, - { url = "https://files.pythonhosted.org/packages/5a/0c/7d5300883da16f0063ae53996358758b2a2df2a09c72a5061fa79a1f5006/mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce", size = 12893775, upload-time = "2025-09-19T00:10:03.814Z" }, - { url = "https://files.pythonhosted.org/packages/50/df/2cffbf25737bdb236f60c973edf62e3e7b4ee1c25b6878629e88e2cde967/mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d", size = 11936852, upload-time = "2025-09-19T00:10:51.631Z" }, - { url = "https://files.pythonhosted.org/packages/be/50/34059de13dd269227fb4a03be1faee6e2a4b04a2051c82ac0a0b5a773c9a/mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c", size = 12480242, upload-time = "2025-09-19T00:11:07.955Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/040983fad5132d85914c874a2836252bbc57832065548885b5bb5b0d4359/mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb", size = 13326683, upload-time = "2025-09-19T00:09:55.572Z" }, - { url = "https://files.pythonhosted.org/packages/e9/ba/89b2901dd77414dd7a8c8729985832a5735053be15b744c18e4586e506ef/mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075", size = 13514749, upload-time = "2025-09-19T00:10:44.827Z" }, - { url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" }, + { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, ] [[package]] @@ -1956,6 +2890,45 @@ version = "2.2.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/61/68/810093cb579daae426794bbd9d88aa830fae296e85172d18cb0f0e5dd4bc/mysqlclient-2.2.7.tar.gz", hash = "sha256:24ae22b59416d5fcce7e99c9d37548350b4565baac82f95e149cac6ce4163845", size = 91383, upload-time = "2025-01-10T12:06:00.763Z" } +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + +[[package]] +name = "networkx" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform == 'linux'", +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'linux'", +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + [[package]] name = "nltk" version = "3.9.2" @@ -1973,11 +2946,11 @@ wheels = [ [[package]] name = "nodeenv" -version = "1.9.1" +version = "1.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] [[package]] @@ -2037,70 +3010,70 @@ wheels = [ [[package]] name = "numpy" -version = "2.3.3" +version = "2.4.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.11' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'linux'", ] -sdist = { url = "https://files.pythonhosted.org/packages/d0/19/95b3d357407220ed24c139018d2518fab0a61a948e68286a25f1a4d049ff/numpy-2.3.3.tar.gz", hash = "sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029", size = 20576648, upload-time = "2025-09-09T16:54:12.543Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/62/ae72ff66c0f1fd959925b4c11f8c2dea61f47f6acaea75a08512cdfe3fed/numpy-2.4.1.tar.gz", hash = "sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690", size = 20721320, upload-time = "2026-01-10T06:44:59.619Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/45/e80d203ef6b267aa29b22714fb558930b27960a0c5ce3c19c999232bb3eb/numpy-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ffc4f5caba7dfcbe944ed674b7eef683c7e94874046454bb79ed7ee0236f59d", size = 21259253, upload-time = "2025-09-09T15:56:02.094Z" }, - { url = "https://files.pythonhosted.org/packages/52/18/cf2c648fccf339e59302e00e5f2bc87725a3ce1992f30f3f78c9044d7c43/numpy-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7e946c7170858a0295f79a60214424caac2ffdb0063d4d79cb681f9aa0aa569", size = 14450980, upload-time = "2025-09-09T15:56:05.926Z" }, - { url = "https://files.pythonhosted.org/packages/93/fb/9af1082bec870188c42a1c239839915b74a5099c392389ff04215dcee812/numpy-2.3.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:cd4260f64bc794c3390a63bf0728220dd1a68170c169088a1e0dfa2fde1be12f", size = 5379709, upload-time = "2025-09-09T15:56:07.95Z" }, - { url = "https://files.pythonhosted.org/packages/75/0f/bfd7abca52bcbf9a4a65abc83fe18ef01ccdeb37bfb28bbd6ad613447c79/numpy-2.3.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:f0ddb4b96a87b6728df9362135e764eac3cfa674499943ebc44ce96c478ab125", size = 6913923, upload-time = "2025-09-09T15:56:09.443Z" }, - { url = "https://files.pythonhosted.org/packages/79/55/d69adad255e87ab7afda1caf93ca997859092afeb697703e2f010f7c2e55/numpy-2.3.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:afd07d377f478344ec6ca2b8d4ca08ae8bd44706763d1efb56397de606393f48", size = 14589591, upload-time = "2025-09-09T15:56:11.234Z" }, - { url = "https://files.pythonhosted.org/packages/10/a2/010b0e27ddeacab7839957d7a8f00e91206e0c2c47abbb5f35a2630e5387/numpy-2.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc92a5dedcc53857249ca51ef29f5e5f2f8c513e22cfb90faeb20343b8c6f7a6", size = 16938714, upload-time = "2025-09-09T15:56:14.637Z" }, - { url = "https://files.pythonhosted.org/packages/1c/6b/12ce8ede632c7126eb2762b9e15e18e204b81725b81f35176eac14dc5b82/numpy-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7af05ed4dc19f308e1d9fc759f36f21921eb7bbfc82843eeec6b2a2863a0aefa", size = 16370592, upload-time = "2025-09-09T15:56:17.285Z" }, - { url = "https://files.pythonhosted.org/packages/b4/35/aba8568b2593067bb6a8fe4c52babb23b4c3b9c80e1b49dff03a09925e4a/numpy-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:433bf137e338677cebdd5beac0199ac84712ad9d630b74eceeb759eaa45ddf30", size = 18884474, upload-time = "2025-09-09T15:56:20.943Z" }, - { url = "https://files.pythonhosted.org/packages/51/5d/bb7fc075b762c96329147799e1bcc9176ab07ca6375ea976c475482ad5b3/numpy-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cfdd09f9c84a1a934cde1eec2267f0a43a7cd44b2cca4ff95b7c0d14d144b0bf", size = 20957014, upload-time = "2025-09-09T15:56:29.966Z" }, - { url = "https://files.pythonhosted.org/packages/6b/0e/c6211bb92af26517acd52125a237a92afe9c3124c6a68d3b9f81b62a0568/numpy-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb32e3cf0f762aee47ad1ddc6672988f7f27045b0783c887190545baba73aa25", size = 14185220, upload-time = "2025-09-09T15:56:32.175Z" }, - { url = "https://files.pythonhosted.org/packages/22/f2/07bb754eb2ede9073f4054f7c0286b0d9d2e23982e090a80d478b26d35ca/numpy-2.3.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:396b254daeb0a57b1fe0ecb5e3cff6fa79a380fa97c8f7781a6d08cd429418fe", size = 5113918, upload-time = "2025-09-09T15:56:34.175Z" }, - { url = "https://files.pythonhosted.org/packages/81/0a/afa51697e9fb74642f231ea36aca80fa17c8fb89f7a82abd5174023c3960/numpy-2.3.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b", size = 6647922, upload-time = "2025-09-09T15:56:36.149Z" }, - { url = "https://files.pythonhosted.org/packages/5d/f5/122d9cdb3f51c520d150fef6e87df9279e33d19a9611a87c0d2cf78a89f4/numpy-2.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c02d0629d25d426585fb2e45a66154081b9fa677bc92a881ff1d216bc9919a8", size = 14281991, upload-time = "2025-09-09T15:56:40.548Z" }, - { url = "https://files.pythonhosted.org/packages/51/64/7de3c91e821a2debf77c92962ea3fe6ac2bc45d0778c1cbe15d4fce2fd94/numpy-2.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9192da52b9745f7f0766531dcfa978b7763916f158bb63bdb8a1eca0068ab20", size = 16641643, upload-time = "2025-09-09T15:56:43.343Z" }, - { url = "https://files.pythonhosted.org/packages/30/e4/961a5fa681502cd0d68907818b69f67542695b74e3ceaa513918103b7e80/numpy-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cd7de500a5b66319db419dc3c345244404a164beae0d0937283b907d8152e6ea", size = 16056787, upload-time = "2025-09-09T15:56:46.141Z" }, - { url = "https://files.pythonhosted.org/packages/99/26/92c912b966e47fbbdf2ad556cb17e3a3088e2e1292b9833be1dfa5361a1a/numpy-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:93d4962d8f82af58f0b2eb85daaf1b3ca23fe0a85d0be8f1f2b7bb46034e56d7", size = 18579598, upload-time = "2025-09-09T15:56:49.844Z" }, - { url = "https://files.pythonhosted.org/packages/7d/b9/984c2b1ee61a8b803bf63582b4ac4242cf76e2dbd663efeafcb620cc0ccb/numpy-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf", size = 20949588, upload-time = "2025-09-09T15:56:59.087Z" }, - { url = "https://files.pythonhosted.org/packages/a6/e4/07970e3bed0b1384d22af1e9912527ecbeb47d3b26e9b6a3bced068b3bea/numpy-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7", size = 14177802, upload-time = "2025-09-09T15:57:01.73Z" }, - { url = "https://files.pythonhosted.org/packages/35/c7/477a83887f9de61f1203bad89cf208b7c19cc9fef0cebef65d5a1a0619f2/numpy-2.3.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6", size = 5106537, upload-time = "2025-09-09T15:57:03.765Z" }, - { url = "https://files.pythonhosted.org/packages/52/47/93b953bd5866a6f6986344d045a207d3f1cfbad99db29f534ea9cee5108c/numpy-2.3.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7", size = 6640743, upload-time = "2025-09-09T15:57:07.921Z" }, - { url = "https://files.pythonhosted.org/packages/23/83/377f84aaeb800b64c0ef4de58b08769e782edcefa4fea712910b6f0afd3c/numpy-2.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c", size = 14278881, upload-time = "2025-09-09T15:57:11.349Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a5/bf3db6e66c4b160d6ea10b534c381a1955dfab34cb1017ea93aa33c70ed3/numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93", size = 16636301, upload-time = "2025-09-09T15:57:14.245Z" }, - { url = "https://files.pythonhosted.org/packages/a2/59/1287924242eb4fa3f9b3a2c30400f2e17eb2707020d1c5e3086fe7330717/numpy-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae", size = 16053645, upload-time = "2025-09-09T15:57:16.534Z" }, - { url = "https://files.pythonhosted.org/packages/e6/93/b3d47ed882027c35e94ac2320c37e452a549f582a5e801f2d34b56973c97/numpy-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86", size = 18578179, upload-time = "2025-09-09T15:57:18.883Z" }, - { url = "https://files.pythonhosted.org/packages/11/d0/0d1ddec56b162042ddfafeeb293bac672de9b0cfd688383590090963720a/numpy-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc", size = 21048025, upload-time = "2025-09-09T15:57:27.257Z" }, - { url = "https://files.pythonhosted.org/packages/36/9e/1996ca6b6d00415b6acbdd3c42f7f03ea256e2c3f158f80bd7436a8a19f3/numpy-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc", size = 14301053, upload-time = "2025-09-09T15:57:30.077Z" }, - { url = "https://files.pythonhosted.org/packages/05/24/43da09aa764c68694b76e84b3d3f0c44cb7c18cdc1ba80e48b0ac1d2cd39/numpy-2.3.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b", size = 5229444, upload-time = "2025-09-09T15:57:32.733Z" }, - { url = "https://files.pythonhosted.org/packages/bc/14/50ffb0f22f7218ef8af28dd089f79f68289a7a05a208db9a2c5dcbe123c1/numpy-2.3.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19", size = 6738039, upload-time = "2025-09-09T15:57:34.328Z" }, - { url = "https://files.pythonhosted.org/packages/55/52/af46ac0795e09657d45a7f4db961917314377edecf66db0e39fa7ab5c3d3/numpy-2.3.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30", size = 14352314, upload-time = "2025-09-09T15:57:36.255Z" }, - { url = "https://files.pythonhosted.org/packages/a7/b1/dc226b4c90eb9f07a3fff95c2f0db3268e2e54e5cce97c4ac91518aee71b/numpy-2.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e", size = 16701722, upload-time = "2025-09-09T15:57:38.622Z" }, - { url = "https://files.pythonhosted.org/packages/9d/9d/9d8d358f2eb5eced14dba99f110d83b5cd9a4460895230f3b396ad19a323/numpy-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3", size = 16132755, upload-time = "2025-09-09T15:57:41.16Z" }, - { url = "https://files.pythonhosted.org/packages/b6/27/b3922660c45513f9377b3fb42240bec63f203c71416093476ec9aa0719dc/numpy-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea", size = 18651560, upload-time = "2025-09-09T15:57:43.459Z" }, - { url = "https://files.pythonhosted.org/packages/6b/01/342ad585ad82419b99bcf7cebe99e61da6bedb89e213c5fd71acc467faee/numpy-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cd052f1fa6a78dee696b58a914b7229ecfa41f0a6d96dc663c1220a55e137593", size = 20951527, upload-time = "2025-09-09T15:57:52.006Z" }, - { url = "https://files.pythonhosted.org/packages/ef/d8/204e0d73fc1b7a9ee80ab1fe1983dd33a4d64a4e30a05364b0208e9a241a/numpy-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:414a97499480067d305fcac9716c29cf4d0d76db6ebf0bf3cbce666677f12652", size = 14186159, upload-time = "2025-09-09T15:57:54.407Z" }, - { url = "https://files.pythonhosted.org/packages/22/af/f11c916d08f3a18fb8ba81ab72b5b74a6e42ead4c2846d270eb19845bf74/numpy-2.3.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:50a5fe69f135f88a2be9b6ca0481a68a136f6febe1916e4920e12f1a34e708a7", size = 5114624, upload-time = "2025-09-09T15:57:56.5Z" }, - { url = "https://files.pythonhosted.org/packages/fb/11/0ed919c8381ac9d2ffacd63fd1f0c34d27e99cab650f0eb6f110e6ae4858/numpy-2.3.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:b912f2ed2b67a129e6a601e9d93d4fa37bef67e54cac442a2f588a54afe5c67a", size = 6642627, upload-time = "2025-09-09T15:57:58.206Z" }, - { url = "https://files.pythonhosted.org/packages/ee/83/deb5f77cb0f7ba6cb52b91ed388b47f8f3c2e9930d4665c600408d9b90b9/numpy-2.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e318ee0596d76d4cb3d78535dc005fa60e5ea348cd131a51e99d0bdbe0b54fe", size = 14296926, upload-time = "2025-09-09T15:58:00.035Z" }, - { url = "https://files.pythonhosted.org/packages/77/cc/70e59dcb84f2b005d4f306310ff0a892518cc0c8000a33d0e6faf7ca8d80/numpy-2.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce020080e4a52426202bdb6f7691c65bb55e49f261f31a8f506c9f6bc7450421", size = 16638958, upload-time = "2025-09-09T15:58:02.738Z" }, - { url = "https://files.pythonhosted.org/packages/b6/5a/b2ab6c18b4257e099587d5b7f903317bd7115333ad8d4ec4874278eafa61/numpy-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e6687dc183aa55dae4a705b35f9c0f8cb178bcaa2f029b241ac5356221d5c021", size = 16071920, upload-time = "2025-09-09T15:58:05.029Z" }, - { url = "https://files.pythonhosted.org/packages/b8/f1/8b3fdc44324a259298520dd82147ff648979bed085feeacc1250ef1656c0/numpy-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d8f3b1080782469fdc1718c4ed1d22549b5fb12af0d57d35e992158a772a37cf", size = 18577076, upload-time = "2025-09-09T15:58:07.745Z" }, - { url = "https://files.pythonhosted.org/packages/d5/df/ee2f1c0a9de7347f14da5dd3cd3c3b034d1b8607ccb6883d7dd5c035d631/numpy-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9cc48e09feb11e1db00b320e9d30a4151f7369afb96bd0e48d942d09da3a0d00", size = 21047987, upload-time = "2025-09-09T15:58:16.889Z" }, - { url = "https://files.pythonhosted.org/packages/d6/92/9453bdc5a4e9e69cf4358463f25e8260e2ffc126d52e10038b9077815989/numpy-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:901bf6123879b7f251d3631967fd574690734236075082078e0571977c6a8e6a", size = 14301076, upload-time = "2025-09-09T15:58:20.343Z" }, - { url = "https://files.pythonhosted.org/packages/13/77/1447b9eb500f028bb44253105bd67534af60499588a5149a94f18f2ca917/numpy-2.3.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:7f025652034199c301049296b59fa7d52c7e625017cae4c75d8662e377bf487d", size = 5229491, upload-time = "2025-09-09T15:58:22.481Z" }, - { url = "https://files.pythonhosted.org/packages/3d/f9/d72221b6ca205f9736cb4b2ce3b002f6e45cd67cd6a6d1c8af11a2f0b649/numpy-2.3.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:533ca5f6d325c80b6007d4d7fb1984c303553534191024ec6a524a4c92a5935a", size = 6737913, upload-time = "2025-09-09T15:58:24.569Z" }, - { url = "https://files.pythonhosted.org/packages/3c/5f/d12834711962ad9c46af72f79bb31e73e416ee49d17f4c797f72c96b6ca5/numpy-2.3.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0edd58682a399824633b66885d699d7de982800053acf20be1eaa46d92009c54", size = 14352811, upload-time = "2025-09-09T15:58:26.416Z" }, - { url = "https://files.pythonhosted.org/packages/a1/0d/fdbec6629d97fd1bebed56cd742884e4eead593611bbe1abc3eb40d304b2/numpy-2.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:367ad5d8fbec5d9296d18478804a530f1191e24ab4d75ab408346ae88045d25e", size = 16702689, upload-time = "2025-09-09T15:58:28.831Z" }, - { url = "https://files.pythonhosted.org/packages/9b/09/0a35196dc5575adde1eb97ddfbc3e1687a814f905377621d18ca9bc2b7dd/numpy-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8f6ac61a217437946a1fa48d24c47c91a0c4f725237871117dea264982128097", size = 16133855, upload-time = "2025-09-09T15:58:31.349Z" }, - { url = "https://files.pythonhosted.org/packages/7a/ca/c9de3ea397d576f1b6753eaa906d4cdef1bf97589a6d9825a349b4729cc2/numpy-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:179a42101b845a816d464b6fe9a845dfaf308fdfc7925387195570789bb2c970", size = 18652520, upload-time = "2025-09-09T15:58:33.762Z" }, - { url = "https://files.pythonhosted.org/packages/b8/f2/7e0a37cfced2644c9563c529f29fa28acbd0960dde32ece683aafa6f4949/numpy-2.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1e02c7159791cd481e1e6d5ddd766b62a4d5acf8df4d4d1afe35ee9c5c33a41e", size = 21131019, upload-time = "2025-09-09T15:58:42.838Z" }, - { url = "https://files.pythonhosted.org/packages/1a/7e/3291f505297ed63831135a6cc0f474da0c868a1f31b0dd9a9f03a7a0d2ed/numpy-2.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:dca2d0fc80b3893ae72197b39f69d55a3cd8b17ea1b50aa4c62de82419936150", size = 14376288, upload-time = "2025-09-09T15:58:45.425Z" }, - { url = "https://files.pythonhosted.org/packages/bf/4b/ae02e985bdeee73d7b5abdefeb98aef1207e96d4c0621ee0cf228ddfac3c/numpy-2.3.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:99683cbe0658f8271b333a1b1b4bb3173750ad59c0c61f5bbdc5b318918fffe3", size = 5305425, upload-time = "2025-09-09T15:58:48.6Z" }, - { url = "https://files.pythonhosted.org/packages/8b/eb/9df215d6d7250db32007941500dc51c48190be25f2401d5b2b564e467247/numpy-2.3.3-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:d9d537a39cc9de668e5cd0e25affb17aec17b577c6b3ae8a3d866b479fbe88d0", size = 6819053, upload-time = "2025-09-09T15:58:50.401Z" }, - { url = "https://files.pythonhosted.org/packages/57/62/208293d7d6b2a8998a4a1f23ac758648c3c32182d4ce4346062018362e29/numpy-2.3.3-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8596ba2f8af5f93b01d97563832686d20206d303024777f6dfc2e7c7c3f1850e", size = 14420354, upload-time = "2025-09-09T15:58:52.704Z" }, - { url = "https://files.pythonhosted.org/packages/ed/0c/8e86e0ff7072e14a71b4c6af63175e40d1e7e933ce9b9e9f765a95b4e0c3/numpy-2.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1ec5615b05369925bd1125f27df33f3b6c8bc10d788d5999ecd8769a1fa04db", size = 16760413, upload-time = "2025-09-09T15:58:55.027Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/2b1bc18424f3ad9af577f6ce23600319968a70575bd7db31ce66731bbef9/numpy-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0cce2a669e3c8ba02ee563c7835f92c153cf02edff1ae05e1823f1dde21b16a5", size = 16944563, upload-time = "2026-01-10T06:42:14.615Z" }, + { url = "https://files.pythonhosted.org/packages/2c/57/26e5f97d075aef3794045a6ca9eada6a4ed70eb9a40e7a4a93f9ac80d704/numpy-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:899d2c18024984814ac7e83f8f49d8e8180e2fbe1b2e252f2e7f1d06bea92425", size = 12645658, upload-time = "2026-01-10T06:42:17.298Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ba/80fc0b1e3cb2fd5c6143f00f42eb67762aa043eaa05ca924ecc3222a7849/numpy-2.4.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:09aa8a87e45b55a1c2c205d42e2808849ece5c484b2aab11fecabec3841cafba", size = 5474132, upload-time = "2026-01-10T06:42:19.637Z" }, + { url = "https://files.pythonhosted.org/packages/40/ae/0a5b9a397f0e865ec171187c78d9b57e5588afc439a04ba9cab1ebb2c945/numpy-2.4.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:edee228f76ee2dab4579fad6f51f6a305de09d444280109e0f75df247ff21501", size = 6804159, upload-time = "2026-01-10T06:42:21.44Z" }, + { url = "https://files.pythonhosted.org/packages/86/9c/841c15e691c7085caa6fd162f063eff494099c8327aeccd509d1ab1e36ab/numpy-2.4.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a92f227dbcdc9e4c3e193add1a189a9909947d4f8504c576f4a732fd0b54240a", size = 14708058, upload-time = "2026-01-10T06:42:23.546Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9d/7862db06743f489e6a502a3b93136d73aea27d97b2cf91504f70a27501d6/numpy-2.4.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:538bf4ec353709c765ff75ae616c34d3c3dca1a68312727e8f2676ea644f8509", size = 16651501, upload-time = "2026-01-10T06:42:25.909Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9c/6fc34ebcbd4015c6e5f0c0ce38264010ce8a546cb6beacb457b84a75dfc8/numpy-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ac08c63cb7779b85e9d5318e6c3518b424bc1f364ac4cb2c6136f12e5ff2dccc", size = 16492627, upload-time = "2026-01-10T06:42:28.938Z" }, + { url = "https://files.pythonhosted.org/packages/aa/63/2494a8597502dacda439f61b3c0db4da59928150e62be0e99395c3ad23c5/numpy-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f9c360ecef085e5841c539a9a12b883dff005fbd7ce46722f5e9cef52634d82", size = 18585052, upload-time = "2026-01-10T06:42:31.312Z" }, + { url = "https://files.pythonhosted.org/packages/78/7f/ec53e32bf10c813604edf07a3682616bd931d026fcde7b6d13195dfb684a/numpy-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d3703409aac693fa82c0aee023a1ae06a6e9d065dba10f5e8e80f642f1e9d0a2", size = 16656888, upload-time = "2026-01-10T06:42:40.913Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e0/1f9585d7dae8f14864e948fd7fa86c6cb72dee2676ca2748e63b1c5acfe0/numpy-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7211b95ca365519d3596a1d8688a95874cc94219d417504d9ecb2df99fa7bfa8", size = 12373956, upload-time = "2026-01-10T06:42:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/8e/43/9762e88909ff2326f5e7536fa8cb3c49fb03a7d92705f23e6e7f553d9cb3/numpy-2.4.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5adf01965456a664fc727ed69cc71848f28d063217c63e1a0e200a118d5eec9a", size = 5202567, upload-time = "2026-01-10T06:42:45.107Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ee/34b7930eb61e79feb4478800a4b95b46566969d837546aa7c034c742ef98/numpy-2.4.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:26f0bcd9c79a00e339565b303badc74d3ea2bd6d52191eeca5f95936cad107d0", size = 6549459, upload-time = "2026-01-10T06:42:48.152Z" }, + { url = "https://files.pythonhosted.org/packages/79/e3/5f115fae982565771be994867c89bcd8d7208dbfe9469185497d70de5ddf/numpy-2.4.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0093e85df2960d7e4049664b26afc58b03236e967fb942354deef3208857a04c", size = 14404859, upload-time = "2026-01-10T06:42:49.947Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7d/9c8a781c88933725445a859cac5d01b5871588a15969ee6aeb618ba99eee/numpy-2.4.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad270f438cbdd402c364980317fb6b117d9ec5e226fff5b4148dd9aa9fc6e02", size = 16371419, upload-time = "2026-01-10T06:42:52.409Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d2/8aa084818554543f17cf4162c42f162acbd3bb42688aefdba6628a859f77/numpy-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:297c72b1b98100c2e8f873d5d35fb551fce7040ade83d67dd51d38c8d42a2162", size = 16182131, upload-time = "2026-01-10T06:42:54.694Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/0425216684297c58a8df35f3284ef56ec4a043e6d283f8a59c53562caf1b/numpy-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf6470d91d34bf669f61d515499859fa7a4c2f7c36434afb70e82df7217933f9", size = 18295342, upload-time = "2026-01-10T06:42:56.991Z" }, + { url = "https://files.pythonhosted.org/packages/04/68/732d4b7811c00775f3bd522a21e8dd5a23f77eb11acdeb663e4a4ebf0ef4/numpy-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d797454e37570cfd61143b73b8debd623c3c0952959adb817dd310a483d58a1b", size = 16652495, upload-time = "2026-01-10T06:43:06.283Z" }, + { url = "https://files.pythonhosted.org/packages/20/ca/857722353421a27f1465652b2c66813eeeccea9d76d5f7b74b99f298e60e/numpy-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c55962006156aeef1629b953fd359064aa47e4d82cfc8e67f0918f7da3344f", size = 12368657, upload-time = "2026-01-10T06:43:09.094Z" }, + { url = "https://files.pythonhosted.org/packages/81/0d/2377c917513449cc6240031a79d30eb9a163d32a91e79e0da47c43f2c0c8/numpy-2.4.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:71abbea030f2cfc3092a0ff9f8c8fdefdc5e0bf7d9d9c99663538bb0ecdac0b9", size = 5197256, upload-time = "2026-01-10T06:43:13.634Z" }, + { url = "https://files.pythonhosted.org/packages/17/39/569452228de3f5de9064ac75137082c6214be1f5c532016549a7923ab4b5/numpy-2.4.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5b55aa56165b17aaf15520beb9cbd33c9039810e0d9643dd4379e44294c7303e", size = 6545212, upload-time = "2026-01-10T06:43:15.661Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/77333f4d1e4dac4395385482557aeecf4826e6ff517e32ca48e1dafbe42a/numpy-2.4.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0faba4a331195bfa96f93dd9dfaa10b2c7aa8cda3a02b7fd635e588fe821bf5", size = 14402871, upload-time = "2026-01-10T06:43:17.324Z" }, + { url = "https://files.pythonhosted.org/packages/ba/87/d341e519956273b39d8d47969dd1eaa1af740615394fe67d06f1efa68773/numpy-2.4.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e3087f53e2b4428766b54932644d148613c5a595150533ae7f00dab2f319a8", size = 16359305, upload-time = "2026-01-10T06:43:19.376Z" }, + { url = "https://files.pythonhosted.org/packages/32/91/789132c6666288eaa20ae8066bb99eba1939362e8f1a534949a215246e97/numpy-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:49e792ec351315e16da54b543db06ca8a86985ab682602d90c60ef4ff4db2a9c", size = 16181909, upload-time = "2026-01-10T06:43:21.808Z" }, + { url = "https://files.pythonhosted.org/packages/cf/b8/090b8bd27b82a844bb22ff8fdf7935cb1980b48d6e439ae116f53cdc2143/numpy-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79e9e06c4c2379db47f3f6fc7a8652e7498251789bf8ff5bd43bf478ef314ca2", size = 18284380, upload-time = "2026-01-10T06:43:23.957Z" }, + { url = "https://files.pythonhosted.org/packages/da/a1/354583ac5c4caa566de6ddfbc42744409b515039e085fab6e0ff942e0df5/numpy-2.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f93bc6892fe7b0663e5ffa83b61aab510aacffd58c16e012bb9352d489d90cb7", size = 12496156, upload-time = "2026-01-10T06:43:34.237Z" }, + { url = "https://files.pythonhosted.org/packages/51/b0/42807c6e8cce58c00127b1dc24d365305189991f2a7917aa694a109c8d7d/numpy-2.4.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:178de8f87948163d98a4c9ab5bee4ce6519ca918926ec8df195af582de28544d", size = 5324663, upload-time = "2026-01-10T06:43:36.211Z" }, + { url = "https://files.pythonhosted.org/packages/fe/55/7a621694010d92375ed82f312b2f28017694ed784775269115323e37f5e2/numpy-2.4.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:98b35775e03ab7f868908b524fc0a84d38932d8daf7b7e1c3c3a1b6c7a2c9f15", size = 6645224, upload-time = "2026-01-10T06:43:37.884Z" }, + { url = "https://files.pythonhosted.org/packages/50/96/9fa8635ed9d7c847d87e30c834f7109fac5e88549d79ef3324ab5c20919f/numpy-2.4.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941c2a93313d030f219f3a71fd3d91a728b82979a5e8034eb2e60d394a2b83f9", size = 14462352, upload-time = "2026-01-10T06:43:39.479Z" }, + { url = "https://files.pythonhosted.org/packages/03/d1/8cf62d8bb2062da4fb82dd5d49e47c923f9c0738032f054e0a75342faba7/numpy-2.4.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:529050522e983e00a6c1c6b67411083630de8b57f65e853d7b03d9281b8694d2", size = 16407279, upload-time = "2026-01-10T06:43:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/86/1c/95c86e17c6b0b31ce6ef219da00f71113b220bcb14938c8d9a05cee0ff53/numpy-2.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2302dc0224c1cbc49bb94f7064f3f923a971bfae45c33870dcbff63a2a550505", size = 16248316, upload-time = "2026-01-10T06:43:44.121Z" }, + { url = "https://files.pythonhosted.org/packages/30/b4/e7f5ff8697274c9d0fa82398b6a372a27e5cef069b37df6355ccb1f1db1a/numpy-2.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9171a42fcad32dcf3fa86f0a4faa5e9f8facefdb276f54b8b390d90447cff4e2", size = 18329884, upload-time = "2026-01-10T06:43:46.613Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a7/ef08d25698e0e4b4efbad8d55251d20fe2a15f6d9aa7c9b30cd03c165e6f/numpy-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3869ea1ee1a1edc16c29bbe3a2f2a4e515cc3a44d43903ad41e0cacdbaf733dc", size = 16652046, upload-time = "2026-01-10T06:43:54.797Z" }, + { url = "https://files.pythonhosted.org/packages/8f/39/e378b3e3ca13477e5ac70293ec027c438d1927f18637e396fe90b1addd72/numpy-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e867df947d427cdd7a60e3e271729090b0f0df80f5f10ab7dd436f40811699c3", size = 12378858, upload-time = "2026-01-10T06:43:57.099Z" }, + { url = "https://files.pythonhosted.org/packages/c3/74/7ec6154f0006910ed1fdbb7591cf4432307033102b8a22041599935f8969/numpy-2.4.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e3bd2cb07841166420d2fa7146c96ce00cb3410664cbc1a6be028e456c4ee220", size = 5207417, upload-time = "2026-01-10T06:43:59.037Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b7/053ac11820d84e42f8feea5cb81cc4fcd1091499b45b1ed8c7415b1bf831/numpy-2.4.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:f0a90aba7d521e6954670550e561a4cb925713bd944445dbe9e729b71f6cabee", size = 6542643, upload-time = "2026-01-10T06:44:01.852Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c4/2e7908915c0e32ca636b92e4e4a3bdec4cb1e7eb0f8aedf1ed3c68a0d8cd/numpy-2.4.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d558123217a83b2d1ba316b986e9248a1ed1971ad495963d555ccd75dcb1556", size = 14418963, upload-time = "2026-01-10T06:44:04.047Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c0/3ed5083d94e7ffd7c404e54619c088e11f2e1939a9544f5397f4adb1b8ba/numpy-2.4.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f44de05659b67d20499cbc96d49f2650769afcb398b79b324bb6e297bfe3844", size = 16363811, upload-time = "2026-01-10T06:44:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/0e/68/42b66f1852bf525050a67315a4fb94586ab7e9eaa541b1bef530fab0c5dd/numpy-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:69e7419c9012c4aaf695109564e3387f1259f001b4326dfa55907b098af082d3", size = 16197643, upload-time = "2026-01-10T06:44:08.33Z" }, + { url = "https://files.pythonhosted.org/packages/d2/40/e8714fc933d85f82c6bfc7b998a0649ad9769a32f3494ba86598aaf18a48/numpy-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ffd257026eb1b34352e749d7cc1678b5eeec3e329ad8c9965a797e08ccba205", size = 18289601, upload-time = "2026-01-10T06:44:10.841Z" }, + { url = "https://files.pythonhosted.org/packages/de/bc/ea3f2c96fcb382311827231f911723aeff596364eb6e1b6d1d91128aa29b/numpy-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e53170557d37ae404bf8d542ca5b7c629d6efa1117dac6a83e394142ea0a43f", size = 12498774, upload-time = "2026-01-10T06:44:19.467Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ab/ef9d939fe4a812648c7a712610b2ca6140b0853c5efea361301006c02ae5/numpy-2.4.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:a73044b752f5d34d4232f25f18160a1cc418ea4507f5f11e299d8ac36875f8a0", size = 5327274, upload-time = "2026-01-10T06:44:23.189Z" }, + { url = "https://files.pythonhosted.org/packages/bd/31/d381368e2a95c3b08b8cf7faac6004849e960f4a042d920337f71cef0cae/numpy-2.4.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:fb1461c99de4d040666ca0444057b06541e5642f800b71c56e6ea92d6a853a0c", size = 6648306, upload-time = "2026-01-10T06:44:25.012Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e5/0989b44ade47430be6323d05c23207636d67d7362a1796ccbccac6773dd2/numpy-2.4.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423797bdab2eeefbe608d7c1ec7b2b4fd3c58d51460f1ee26c7500a1d9c9ee93", size = 14464653, upload-time = "2026-01-10T06:44:26.706Z" }, + { url = "https://files.pythonhosted.org/packages/10/a7/cfbe475c35371cae1358e61f20c5f075badc18c4797ab4354140e1d283cf/numpy-2.4.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52b5f61bdb323b566b528899cc7db2ba5d1015bda7ea811a8bcf3c89c331fa42", size = 16405144, upload-time = "2026-01-10T06:44:29.378Z" }, + { url = "https://files.pythonhosted.org/packages/f8/a3/0c63fe66b534888fa5177cc7cef061541064dbe2b4b60dcc60ffaf0d2157/numpy-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42d7dd5fa36d16d52a84f821eb96031836fd405ee6955dd732f2023724d0aa01", size = 16247425, upload-time = "2026-01-10T06:44:31.721Z" }, + { url = "https://files.pythonhosted.org/packages/6b/2b/55d980cfa2c93bd40ff4c290bf824d792bd41d2fe3487b07707559071760/numpy-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7b6b5e28bbd47b7532698e5db2fe1db693d84b58c254e4389d99a27bb9b8f6b", size = 18330053, upload-time = "2026-01-10T06:44:34.617Z" }, + { url = "https://files.pythonhosted.org/packages/1e/48/d86f97919e79314a1cdee4c832178763e6e98e623e123d0bada19e92c15a/numpy-2.4.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8ad35f20be147a204e28b6a0575fbf3540c5e5f802634d4258d55b1ff5facce1", size = 16822202, upload-time = "2026-01-10T06:44:43.738Z" }, + { url = "https://files.pythonhosted.org/packages/51/e9/1e62a7f77e0f37dcfb0ad6a9744e65df00242b6ea37dfafb55debcbf5b55/numpy-2.4.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8097529164c0f3e32bb89412a0905d9100bf434d9692d9fc275e18dcf53c9344", size = 12569985, upload-time = "2026-01-10T06:44:45.945Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7e/914d54f0c801342306fdcdce3e994a56476f1b818c46c47fc21ae968088c/numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:ea66d2b41ca4a1630aae5507ee0a71647d3124d1741980138aa8f28f44dac36e", size = 5398484, upload-time = "2026-01-10T06:44:48.012Z" }, + { url = "https://files.pythonhosted.org/packages/1c/d8/9570b68584e293a33474e7b5a77ca404f1dcc655e40050a600dee81d27fb/numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:d3f8f0df9f4b8be57b3bf74a1d087fec68f927a2fab68231fdb442bf2c12e426", size = 6713216, upload-time = "2026-01-10T06:44:49.725Z" }, + { url = "https://files.pythonhosted.org/packages/33/9b/9dd6e2db8d49eb24f86acaaa5258e5f4c8ed38209a4ee9de2d1a0ca25045/numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2023ef86243690c2791fd6353e5b4848eedaa88ca8a2d129f462049f6d484696", size = 14538937, upload-time = "2026-01-10T06:44:51.498Z" }, + { url = "https://files.pythonhosted.org/packages/53/87/d5bd995b0f798a37105b876350d346eea5838bd8f77ea3d7a48392f3812b/numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8361ea4220d763e54cff2fbe7d8c93526b744f7cd9ddab47afeff7e14e8503be", size = 16479830, upload-time = "2026-01-10T06:44:53.931Z" }, ] [[package]] @@ -2114,7 +3087,7 @@ wheels = [ [[package]] name = "ocrmypdf" -version = "16.12.0" +version = "16.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "deprecation", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -2127,18 +3100,50 @@ dependencies = [ { name = "pluggy", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "rich", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2b/ed/dacc0f189e4fcefc52d709e9961929e3f622a85efa5ae47c9d9663d75cab/ocrmypdf-16.12.0.tar.gz", hash = "sha256:a0f6509e7780b286391f8847fae1811d2b157b14283ad74a2431d6755c5c0ed0", size = 7037326, upload-time = "2025-11-11T22:30:14.223Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/52/be1aaece0703a736757d8957c0d4f19c37561054169b501eb0e7132f15e5/ocrmypdf-16.13.0.tar.gz", hash = "sha256:29d37e915234ce717374863a9cc5dd32d29e063dfe60c51380dda71254c88248", size = 7042247, upload-time = "2025-12-24T07:58:35.86Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/34/d9d04420e6f7a71e2135b41599dae273e4ef36e2ce79b065b65fb2471636/ocrmypdf-16.12.0-py3-none-any.whl", hash = "sha256:0ea5c42027db9cf3bd12b0d0b4190689027ef813fdad3377106ea66bba0012c3", size = 163415, upload-time = "2025-11-11T22:30:11.56Z" }, + { url = "https://files.pythonhosted.org/packages/41/b1/e2e7ad98de0d3ee05b44dbc3f78ccb158a620f3add82d00c85490120e7f2/ocrmypdf-16.13.0-py3-none-any.whl", hash = "sha256:fad8a6f7cc52cdc6225095c401a1766c778c47efe9f1e854ae4dc64a550a3d37", size = 165377, upload-time = "2025-12-24T07:58:33.925Z" }, +] + +[[package]] +name = "ollama" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/5a/652dac4b7affc2b37b95386f8ae78f22808af09d720689e3d7a86b6ed98e/ollama-0.6.1.tar.gz", hash = "sha256:478c67546836430034b415ed64fa890fd3d1ff91781a9d548b3325274e69d7c6", size = 51620, upload-time = "2025-11-13T23:02:17.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/4f/4a617ee93d8208d2bcf26b2d8b9402ceaed03e3853c754940e2290fed063/ollama-0.6.1-py3-none-any.whl", hash = "sha256:fc4c984b345735c5486faeee67d8a265214a31cbb828167782dc642ce0a2bf8c", size = 14354, upload-time = "2025-11-13T23:02:16.292Z" }, +] + +[[package]] +name = "openai" +version = "2.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "distro", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "jiter", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "sniffio", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/f4/4690ecb5d70023ce6bfcfeabfe717020f654bde59a775058ec6ac4692463/openai-2.15.0.tar.gz", hash = "sha256:42eb8cbb407d84770633f31bf727d4ffb4138711c670565a41663d9439174fba", size = 627383, upload-time = "2026-01-09T22:10:08.603Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/df/c306f7375d42bafb379934c2df4c2fa3964656c8c782bac75ee10c102818/openai-2.15.0-py3-none-any.whl", hash = "sha256:6ae23b932cd7230f7244e52954daa6602716d6b9bf235401a107af731baea6c3", size = 1067879, upload-time = "2026-01-09T22:10:06.446Z" }, ] [[package]] name = "packaging" -version = "25.0" +version = "26.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] [[package]] @@ -2152,7 +3157,7 @@ wheels = [ [[package]] name = "paperless-ngx" -version = "2.20.3" +version = "2.20.5" source = { virtual = "." } dependencies = [ { name = "azure-ai-documentintelligence", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -2181,16 +3186,23 @@ dependencies = [ { name = "drf-spectacular", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "drf-spectacular-sidecar", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "drf-writable-nested", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "faiss-cpu", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "filelock", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "flower", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "gotenberg-client", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "httpx-oauth", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "imap-tools", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "inotifyrecursive", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "jinja2", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "langdetect", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "llama-index-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "llama-index-embeddings-huggingface", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "llama-index-embeddings-openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "llama-index-llms-ollama", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "llama-index-llms-openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "llama-index-vector-stores-faiss", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "nltk", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "ocrmypdf", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "pathvalidate", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "pdf2image", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -2203,10 +3215,13 @@ dependencies = [ { name = "redis", extra = ["hiredis"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "regex", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "scikit-learn", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "sentence-transformers", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "setproctitle", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "tika-client", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "torch", version = "2.9.1", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, + { name = "torch", version = "2.9.1+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'linux'" }, { name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "watchdog", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "watchfiles", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "whitenoise", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "whoosh-reloaded", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "zxing-cpp", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version != '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version != '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or sys_platform == 'darwin'" }, @@ -2220,9 +3235,9 @@ mariadb = [ ] postgres = [ { name = "psycopg", extra = ["c", "pool"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "psycopg-c", version = "3.2.12", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version != '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version != '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or sys_platform == 'darwin'" }, - { name = "psycopg-c", version = "3.2.12", source = { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-bookworm-3.2.12/psycopg_c-3.2.12-cp312-cp312-linux_aarch64.whl" }, marker = "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'" }, - { name = "psycopg-c", version = "3.2.12", source = { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-bookworm-3.2.12/psycopg_c-3.2.12-cp312-cp312-linux_x86_64.whl" }, marker = "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "psycopg-c", version = "3.3.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version != '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version != '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or sys_platform == 'darwin'" }, + { name = "psycopg-c", version = "3.3.0", source = { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-trixie-3.3.0/psycopg_c-3.3.0-cp312-cp312-linux_aarch64.whl" }, marker = "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'" }, + { name = "psycopg-c", version = "3.3.0", source = { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-trixie-3.3.0/psycopg_c-3.3.0-cp312-cp312-linux_x86_64.whl" }, marker = "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "psycopg-pool", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] webserver = [ @@ -2296,14 +3311,14 @@ requires-dist = [ { name = "azure-ai-documentintelligence", specifier = ">=1.0.2" }, { name = "babel", specifier = ">=2.17" }, { name = "bleach", specifier = "~=6.3.0" }, - { name = "celery", extras = ["redis"], specifier = "~=5.5.1" }, + { name = "celery", extras = ["redis"], specifier = "~=5.6.2" }, { name = "channels", specifier = "~=4.2" }, { name = "channels-redis", specifier = "~=4.2" }, { name = "concurrent-log-handler", specifier = "~=0.9.25" }, { name = "dateparser", specifier = "~=1.2" }, - { name = "django", specifier = "~=5.2.5" }, - { name = "django-allauth", extras = ["mfa", "socialaccount"], specifier = "~=65.12.1" }, - { name = "django-auditlog", specifier = "~=3.3.0" }, + { name = "django", specifier = "~=5.2.10" }, + { name = "django-allauth", extras = ["mfa", "socialaccount"], specifier = "~=65.13.1" }, + { name = "django-auditlog", specifier = "~=3.4.1" }, { name = "django-cachalot", specifier = "~=2.8.0" }, { name = "django-celery-results", specifier = "~=2.6.0" }, { name = "django-compression-middleware", specifier = "~=0.5.0" }, @@ -2319,25 +3334,32 @@ requires-dist = [ { name = "drf-spectacular", specifier = "~=0.28" }, { name = "drf-spectacular-sidecar", specifier = "~=2025.10.1" }, { name = "drf-writable-nested", specifier = "~=0.7.1" }, + { name = "faiss-cpu", specifier = ">=1.10" }, { name = "filelock", specifier = "~=3.20.0" }, { name = "flower", specifier = "~=2.0.1" }, - { name = "gotenberg-client", specifier = "~=0.12.0" }, - { name = "granian", extras = ["uvloop"], marker = "extra == 'webserver'", specifier = "~=2.5.1" }, + { name = "gotenberg-client", specifier = "~=0.13.1" }, + { name = "granian", extras = ["uvloop"], marker = "extra == 'webserver'", specifier = "~=2.6.0" }, { name = "httpx-oauth", specifier = "~=0.16" }, { name = "imap-tools", specifier = "~=1.11.0" }, - { name = "inotifyrecursive", specifier = "~=0.3" }, { name = "jinja2", specifier = "~=3.1.5" }, { name = "langdetect", specifier = "~=1.0.9" }, + { name = "llama-index-core", specifier = ">=0.14.12" }, + { name = "llama-index-embeddings-huggingface", specifier = ">=0.6.1" }, + { name = "llama-index-embeddings-openai", specifier = ">=0.5.1" }, + { name = "llama-index-llms-ollama", specifier = ">=0.9.1" }, + { name = "llama-index-llms-openai", specifier = ">=0.6.13" }, + { name = "llama-index-vector-stores-faiss", specifier = ">=0.5.2" }, { name = "mysqlclient", marker = "extra == 'mariadb'", specifier = "~=2.2.7" }, { name = "nltk", specifier = "~=3.9.1" }, - { name = "ocrmypdf", specifier = "~=16.12.0" }, + { name = "ocrmypdf", specifier = "~=16.13.0" }, + { name = "openai", specifier = ">=1.76" }, { name = "pathvalidate", specifier = "~=3.3.1" }, { name = "pdf2image", specifier = "~=1.17.0" }, - { name = "psycopg", extras = ["c", "pool"], marker = "extra == 'postgres'", specifier = "==3.2.12" }, - { name = "psycopg-c", marker = "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'postgres'", url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-bookworm-3.2.12/psycopg_c-3.2.12-cp312-cp312-linux_aarch64.whl" }, - { name = "psycopg-c", marker = "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'postgres'", url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-bookworm-3.2.12/psycopg_c-3.2.12-cp312-cp312-linux_x86_64.whl" }, - { name = "psycopg-c", marker = "(python_full_version != '3.12.*' and platform_machine == 'aarch64' and extra == 'postgres') or (python_full_version != '3.12.*' and platform_machine == 'x86_64' and extra == 'postgres') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'postgres') or (sys_platform != 'linux' and extra == 'postgres')", specifier = "==3.2.12" }, - { name = "psycopg-pool", marker = "extra == 'postgres'", specifier = "==3.2.7" }, + { name = "psycopg", extras = ["c", "pool"], marker = "extra == 'postgres'", specifier = "==3.3" }, + { name = "psycopg-c", marker = "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'postgres'", url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-trixie-3.3.0/psycopg_c-3.3.0-cp312-cp312-linux_aarch64.whl" }, + { name = "psycopg-c", marker = "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'postgres'", url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-trixie-3.3.0/psycopg_c-3.3.0-cp312-cp312-linux_x86_64.whl" }, + { name = "psycopg-c", marker = "(python_full_version != '3.12.*' and platform_machine == 'aarch64' and extra == 'postgres') or (python_full_version != '3.12.*' and platform_machine == 'x86_64' and extra == 'postgres') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'postgres') or (sys_platform != 'linux' and extra == 'postgres')", specifier = "==3.3" }, + { name = "psycopg-pool", marker = "extra == 'postgres'", specifier = "==3.3" }, { name = "python-dateutil", specifier = "~=2.9.0" }, { name = "python-dotenv", specifier = "~=1.2.1" }, { name = "python-gnupg", specifier = "~=0.5.4" }, @@ -2348,11 +3370,13 @@ requires-dist = [ { name = "redis", extras = ["hiredis"], specifier = "~=5.2.1" }, { name = "regex", specifier = ">=2025.9.18" }, { name = "scikit-learn", specifier = "~=1.7.0" }, + { name = "sentence-transformers", specifier = ">=4.1" }, { name = "setproctitle", specifier = "~=1.3.4" }, { name = "tika-client", specifier = "~=0.10.0" }, + { name = "torch", specifier = "~=2.9.1", index = "https://download.pytorch.org/whl/cpu" }, { name = "tqdm", specifier = "~=4.67.1" }, - { name = "watchdog", specifier = "~=6.0" }, - { name = "whitenoise", specifier = "~=6.9" }, + { name = "watchfiles", specifier = ">=1.1.1" }, + { name = "whitenoise", specifier = "~=6.11" }, { name = "whoosh-reloaded", specifier = ">=2.7.5" }, { name = "zxing-cpp", marker = "(python_full_version != '3.12.*' and platform_machine == 'aarch64') or (python_full_version != '3.12.*' and platform_machine == 'x86_64') or (platform_machine != 'aarch64' and platform_machine != 'x86_64') or sys_platform != 'linux'", specifier = "~=2.3.0" }, { name = "zxing-cpp", marker = "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", url = "https://github.com/paperless-ngx/builder/releases/download/zxing-2.3.0/zxing_cpp-2.3.0-cp312-cp312-linux_aarch64.whl" }, @@ -2367,17 +3391,17 @@ dev = [ { name = "imagehash" }, { name = "mkdocs-glightbox", specifier = "~=0.5.1" }, { name = "mkdocs-material", specifier = "~=9.7.0" }, - { name = "pre-commit", specifier = "~=4.4.0" }, + { name = "pre-commit", specifier = "~=4.5.1" }, { name = "pre-commit-uv", specifier = "~=4.2.0" }, - { name = "pytest", specifier = "~=8.4.1" }, + { name = "pytest", specifier = "~=9.0.0" }, { name = "pytest-cov", specifier = "~=7.0.0" }, { name = "pytest-django", specifier = "~=4.11.1" }, - { name = "pytest-env" }, + { name = "pytest-env", specifier = "~=1.2.0" }, { name = "pytest-httpx" }, - { name = "pytest-mock" }, - { name = "pytest-rerunfailures" }, + { name = "pytest-mock", specifier = "~=3.15.1" }, + { name = "pytest-rerunfailures", specifier = "~=16.1" }, { name = "pytest-sugar" }, - { name = "pytest-xdist" }, + { name = "pytest-xdist", specifier = "~=3.8.0" }, { name = "ruff", specifier = "~=0.14.0" }, ] docs = [ @@ -2385,7 +3409,7 @@ docs = [ { name = "mkdocs-material", specifier = "~=9.7.0" }, ] lint = [ - { name = "pre-commit", specifier = "~=4.4.0" }, + { name = "pre-commit", specifier = "~=4.5.1" }, { name = "pre-commit-uv", specifier = "~=4.2.0" }, { name = "ruff", specifier = "~=0.14.0" }, ] @@ -2393,15 +3417,15 @@ testing = [ { name = "daphne" }, { name = "factory-boy", specifier = "~=3.3.1" }, { name = "imagehash" }, - { name = "pytest", specifier = "~=8.4.1" }, + { name = "pytest", specifier = "~=9.0.0" }, { name = "pytest-cov", specifier = "~=7.0.0" }, { name = "pytest-django", specifier = "~=4.11.1" }, - { name = "pytest-env" }, + { name = "pytest-env", specifier = "~=1.2.0" }, { name = "pytest-httpx" }, - { name = "pytest-mock" }, - { name = "pytest-rerunfailures" }, + { name = "pytest-mock", specifier = "~=3.15.1" }, + { name = "pytest-rerunfailures", specifier = "~=16.1" }, { name = "pytest-sugar" }, - { name = "pytest-xdist" }, + { name = "pytest-xdist", specifier = "~=3.8.0" }, ] typing = [ { name = "celery-types" }, @@ -2424,11 +3448,11 @@ typing = [ [[package]] name = "pathspec" -version = "0.12.1" +version = "1.0.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] [[package]] @@ -2454,69 +3478,65 @@ wheels = [ [[package]] name = "pdfminer-six" -version = "20250506" +version = "20260107" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "charset-normalizer", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "cryptography", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/78/46/5223d613ac4963e1f7c07b2660fe0e9e770102ec6bda8c038400113fb215/pdfminer_six-20250506.tar.gz", hash = "sha256:b03cc8df09cf3c7aba8246deae52e0bca7ebb112a38895b5e1d4f5dd2b8ca2e7", size = 7387678, upload-time = "2025-05-06T16:17:00.787Z" } +sdist = { url = "https://files.pythonhosted.org/packages/34/a4/5cec1112009f0439a5ca6afa8ace321f0ab2f48da3255b7a1c8953014670/pdfminer_six-20260107.tar.gz", hash = "sha256:96bfd431e3577a55a0efd25676968ca4ce8fd5b53f14565f85716ff363889602", size = 8512094, upload-time = "2026-01-07T13:29:12.937Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/16/7a432c0101fa87457e75cb12c879e1749c5870a786525e2e0f42871d6462/pdfminer_six-20250506-py3-none-any.whl", hash = "sha256:d81ad173f62e5f841b53a8ba63af1a4a355933cfc0ffabd608e568b9193909e3", size = 5620187, upload-time = "2025-05-06T16:16:58.669Z" }, + { url = "https://files.pythonhosted.org/packages/20/8b/28c4eaec9d6b036a52cb44720408f26b1a143ca9bce76cc19e8f5de00ab4/pdfminer_six-20260107-py3-none-any.whl", hash = "sha256:366585ba97e80dffa8f00cebe303d2f381884d8637af4ce422f1df3ef38111a9", size = 6592252, upload-time = "2026-01-07T13:29:10.742Z" }, ] [[package]] name = "pi-heif" -version = "1.1.0" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pillow", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/3c/15d70bac37e50bd03ca2cdf7f7237d237c6f4e3e6d6cefdcc95b53dd708e/pi_heif-1.1.0.tar.gz", hash = "sha256:bac501008a000f2c560086d82e785e3ca2fc688b24b66c1d7dae537ef2fd6a6e", size = 18269246, upload-time = "2025-08-02T09:59:46.64Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/0b/0c97767b8171c7f9f0584c0a70e7b86655a1898c2f5b8ae04a69f4e481a1/pi_heif-1.2.0.tar.gz", hash = "sha256:52bbbc8c30b803288a9f1bb02e4575797940fdc1f5091fce743c699e812418cc", size = 17126431, upload-time = "2026-01-23T07:36:26.924Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/37/008dc1c55afb339bcc11e210d3f9a5220e7e0e03a74d109493cc3888feed/pi_heif-1.1.0-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:888c195a097cfe8d03ef6c30a8d57d7ef21795b67d7ec79769c2707e2d919e32", size = 714302, upload-time = "2025-08-02T09:58:35.47Z" }, - { url = "https://files.pythonhosted.org/packages/bf/98/73fa3e7159a6074c2c88f4a89adb15e3cba23cd732b3fff3c9d9bdba29de/pi_heif-1.1.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:f03ebfe71ab89b1e9d8d9976f306bb881e156d16ecb323ced9fce59a6ca46a20", size = 642373, upload-time = "2025-08-02T09:58:36.929Z" }, - { url = "https://files.pythonhosted.org/packages/03/3a/773d071227e5d2c740afa73c5766d31d82f3866895f0234246c301cceda2/pi_heif-1.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88d3825502b6fa415021c400e955b07e9111122eb8d1a4b5e3dd75c6936491d9", size = 1290964, upload-time = "2025-08-02T09:58:38.112Z" }, - { url = "https://files.pythonhosted.org/packages/47/3c/7c6c7475d57d52cf45c9a8cbb170c6e9904792b2700c15f1df98fbfa23b4/pi_heif-1.1.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f24a6bf385374e7fc94f3ca17eed3d47a0f16ef7bcdeaa3aabdc3c5e43858643", size = 1415137, upload-time = "2025-08-02T09:58:39.212Z" }, - { url = "https://files.pythonhosted.org/packages/5b/d6/bba3d4872e3b50752fdb2bc0c65dbbe1f1c181cc1eecc4961df988fb7c94/pi_heif-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d25aaee75cbd4ff209088513d2e8b0361d8fd24dfb68b848cd78f04a273c0904", size = 2272169, upload-time = "2025-08-02T09:58:40.42Z" }, - { url = "https://files.pythonhosted.org/packages/59/c5/f202417df536bd0ea9c63daf7623cd2259fb4adb9a8909d274949f221faa/pi_heif-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:919d147273cd53116aa7bad1530e0b5d466a72959bc6455497bf833565484992", size = 2430695, upload-time = "2025-08-02T09:58:42.088Z" }, - { url = "https://files.pythonhosted.org/packages/10/5e/b32bb95bd047e1831095bc9480c7280dc593657ec048312df86b623f4083/pi_heif-1.1.0-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:104ed9674f5e3067e3dc413a38c26611b7256760c7f3d5c5adaaf6651a009cc8", size = 714303, upload-time = "2025-08-02T09:58:44.731Z" }, - { url = "https://files.pythonhosted.org/packages/97/6c/221fbf9fbcfd66508e376883ac12d4e590210ecded78a046e2c515594b19/pi_heif-1.1.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4c468181374f64ab6376893541bc91b954e62c2e92703b2e0a8a9d1b023d78b1", size = 642375, upload-time = "2025-08-02T09:58:46.314Z" }, - { url = "https://files.pythonhosted.org/packages/6d/78/e956fc64d901b2e55d946ff819275fa9cb0156299f52e3570e2f73fb1de9/pi_heif-1.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1074ad1d30fb45222b741b1d5daf7daa84414fd137ed3ca1a495557b0e9b377", size = 1292835, upload-time = "2025-08-02T09:58:47.435Z" }, - { url = "https://files.pythonhosted.org/packages/54/cb/7eff4030c63b286aba023d6c12c57a7e73ece21ebdfd8405a6c8e4170999/pi_heif-1.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40754dd2085f8b9c2367c1c49c06fb8e96f93bd5701f6bcb26311442c07373f5", size = 1416805, upload-time = "2025-08-02T09:58:48.605Z" }, - { url = "https://files.pythonhosted.org/packages/1b/bf/c37eefec6c252d3065fb390744b16520a744498f9233b2abec7124c4f9b0/pi_heif-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a599f40fc94d4ed00efd0bed5066b8522a0f6cea79f73990042204cb1598c38a", size = 2273717, upload-time = "2025-08-02T09:58:49.761Z" }, - { url = "https://files.pythonhosted.org/packages/c2/52/50dc5c9f7c3c3f515dcc90f45aa2f8ad19b40ee3ab6b86a86e446653f528/pi_heif-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2eae0dcf439b7592f754c594e74c866b5fa791fa07428d29166755fbe18b4e1d", size = 2432169, upload-time = "2025-08-02T09:58:50.97Z" }, - { url = "https://files.pythonhosted.org/packages/38/49/64b2de6f44c1ad2e7be034d516a24a8e5e4acbc66ed6f1d195d46b0737ac/pi_heif-1.1.0-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:b6d7db7d5506afc6755f535cded7f21b3759457a52734776d929cf4fec220ec1", size = 714538, upload-time = "2025-08-02T09:58:53.507Z" }, - { url = "https://files.pythonhosted.org/packages/de/ff/2c8021d6db6836e65b88f63aba0f1c551c293f51bc1533ed68440432baf0/pi_heif-1.1.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:ff0e6b2036f6f3f6bb4147b4ae99735c3e199ce58e3473d830decae76238f986", size = 642359, upload-time = "2025-08-02T09:58:54.745Z" }, - { url = "https://files.pythonhosted.org/packages/b1/19/14d2eed494ee1492cc881149cbbbc48113dc1f63239b8f1167321b0b28c4/pi_heif-1.1.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ac2ba474703d70e2e62aa9f73ccfe4929274f22f2219c0152a8f481eba3c0d5", size = 1291396, upload-time = "2025-08-02T09:58:55.905Z" }, - { url = "https://files.pythonhosted.org/packages/43/11/43f872ef2c6c4a68c9c99fe0bce1fbfea50428dabbe813e94b89d3dcf60a/pi_heif-1.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24b3c29fe0edbba5419391d180a43bde72125a1a35ee1f073ef8b96eda65279d", size = 1416099, upload-time = "2025-08-02T09:58:57.183Z" }, - { url = "https://files.pythonhosted.org/packages/87/d5/65d97be2c56ff8bf4f6312be171ef737d5a120bbd3f63f69bfd848731ed7/pi_heif-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:019fb0eddabe713c7f01b1de78c438c393aff8c0b0823bccd47f40cf12f2f347", size = 2272591, upload-time = "2025-08-02T09:58:58.487Z" }, - { url = "https://files.pythonhosted.org/packages/f5/d9/aa4456eec621608110ee43d5e824381cc8e66b39cc182a333206f6918cc5/pi_heif-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c8ef5caad093589f7bc7ef07ccc33e5c3eafb3168c0fa5482befd34f344cd3ca", size = 2431520, upload-time = "2025-08-02T09:59:00.193Z" }, - { url = "https://files.pythonhosted.org/packages/72/d5/1c90b161b3012ce1d93a0ddb648734f62bb075af8d2a9f7e01c2e2ce745d/pi_heif-1.1.0-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:228abec71f87cd854ece251c84f28f025ab3d004b8f0ba731bd954b07fe9c14c", size = 714536, upload-time = "2025-08-02T09:59:02.684Z" }, - { url = "https://files.pythonhosted.org/packages/70/9c/43712a6c7d8a66e440aa8ebc651d1fa9eb640c41f35f88bf278a356c28b8/pi_heif-1.1.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:5a7bb04ce197a15aaa3b2e8a30243fdf9c257e676e74d09aef26053cfe615145", size = 642356, upload-time = "2025-08-02T09:59:03.778Z" }, - { url = "https://files.pythonhosted.org/packages/3e/21/0f733a1e62ef4099e2642dd8345e56388cee01edf2a6f25c9a088070593c/pi_heif-1.1.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:587d8de4a672a65e57e7c6c591ed75386c4f5d32b70d499c0dd1023152326890", size = 1291429, upload-time = "2025-08-02T09:59:04.86Z" }, - { url = "https://files.pythonhosted.org/packages/c3/72/08d3695966c096b110f21ff62e61fabd51b970b6a45cb964d27240192881/pi_heif-1.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4dfee3431a262eb83640ee19a5993840a863ed216bbd08b4c7f1214cf0bb5379", size = 1416190, upload-time = "2025-08-02T09:59:06.2Z" }, - { url = "https://files.pythonhosted.org/packages/9e/57/47911f5ac5fa179d51e3641dcda9491ca4a173756dfaf428151c035f098f/pi_heif-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3930f8dd8b1929ae704955611cb4431e2bea7c673b634c950cfc5657adcab485", size = 2272625, upload-time = "2025-08-02T09:59:07.937Z" }, - { url = "https://files.pythonhosted.org/packages/05/41/44ab8f98d79db9a2550260f5a5d3cf55bd2dee078563f38e46785f46d02a/pi_heif-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:056cd407f5af858491d68fd40b2072763fd9719c446e5c48cb3ad70b16829043", size = 2431590, upload-time = "2025-08-02T09:59:09.129Z" }, - { url = "https://files.pythonhosted.org/packages/8a/14/13e6ab10c62091d9d9d81b634ac7ebbde84556651a9cda9122ab345c418d/pi_heif-1.1.0-cp314-cp314-macosx_13_0_x86_64.whl", hash = "sha256:d2f1ecbadbed3afb2f3bf87db2a5276d773e57996052f2adc23b881b7eaef84f", size = 714495, upload-time = "2025-08-02T09:59:11.716Z" }, - { url = "https://files.pythonhosted.org/packages/ff/74/81a041640fde479c3503a713f007edab481301056cf04f52a46dd809cabb/pi_heif-1.1.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:6dcd89781e73d9035ed87c8e65bd7bfe164d3b5e9670e2ebf0df481a9ed73201", size = 642448, upload-time = "2025-08-02T09:59:13.262Z" }, - { url = "https://files.pythonhosted.org/packages/0d/59/58bb9cc23161d77259e111a5f1cb489003cd93ea9d6c72668162c2880e2f/pi_heif-1.1.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d667a91b710ec6af66fbd4e3cda28b08e9c32239e2a3547a29ab22b640e4cece", size = 1291620, upload-time = "2025-08-02T09:59:14.516Z" }, - { url = "https://files.pythonhosted.org/packages/03/66/d4ff09ee80bcf303a1c6a5a9553a723c4728b472358c44e62d5158df212e/pi_heif-1.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c39980385edd3ed946b79ca87b6c83418e09f483fa993ca02513ca6d9660895c", size = 1416269, upload-time = "2025-08-02T09:59:15.685Z" }, - { url = "https://files.pythonhosted.org/packages/00/09/07961a650e0b7ea3b2feeca7146d1617af42b409e700ddfa8603b32e0ccf/pi_heif-1.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:918d571b409b7c04e529a54ebdbb2fd9cec36347b49fc5e859211d3f574fa2be", size = 2272805, upload-time = "2025-08-02T09:59:17.383Z" }, - { url = "https://files.pythonhosted.org/packages/c2/f7/f7c62a9d9907daaa555de18fc394cff86d251eb15d89fa88d19a4aff0a37/pi_heif-1.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d1ddf59f91eac869c2602e7418f876096433c069ee3bc4d41df8e8008c876d14", size = 2431604, upload-time = "2025-08-02T09:59:18.816Z" }, - { url = "https://files.pythonhosted.org/packages/03/42/81e456fc5db44ae99a1e4d6efd2c467349797ff775b99ce147db8c9bf0b2/pi_heif-1.1.0-pp310-pypy310_pp73-macosx_13_0_x86_64.whl", hash = "sha256:63fec82b6f1d1f50d67fdfdca9b2cc9fc241ebcb79dea5aa08915e47441289de", size = 703066, upload-time = "2025-08-02T09:59:31.684Z" }, - { url = "https://files.pythonhosted.org/packages/c9/39/e5a06bb0d8569539f4a729ca639c3641723ac87ddfa2f90077dfcff306da/pi_heif-1.1.0-pp310-pypy310_pp73-macosx_14_0_arm64.whl", hash = "sha256:9d3a1a583d3da53041205c1f6a241fdf027b64a06eeb223c597c8695b43c3850", size = 638910, upload-time = "2025-08-02T09:59:32.763Z" }, - { url = "https://files.pythonhosted.org/packages/cc/46/6876dad0898fe0bda652b9663371ce56dad25f0dc158dee1f49248479c8b/pi_heif-1.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1b9d2075336a7f8eb6dbccc1f3c3b5d3083e434a7f4bd918119eeee1a17e2e9", size = 1251437, upload-time = "2025-08-02T09:59:33.853Z" }, - { url = "https://files.pythonhosted.org/packages/e2/81/fb3a2e5669ea11706314856359972721b2996d4054ebd27cef0122c596aa/pi_heif-1.1.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bc2a8a14e92efee9e07ae5abe6635d5e30cb113fa172a14e3d77a580887285a", size = 1372541, upload-time = "2025-08-02T09:59:35.061Z" }, - { url = "https://files.pythonhosted.org/packages/43/3d/e11091f073060a2a3f6be2d4862cd4006f60c027d395a3d763784cb8e3d2/pi_heif-1.1.0-pp311-pypy311_pp73-macosx_13_0_x86_64.whl", hash = "sha256:cfbf1e5ea3364cd025e1d10c378a9e4a752e94041f108610625cb4d871822209", size = 702996, upload-time = "2025-08-02T09:59:37.932Z" }, - { url = "https://files.pythonhosted.org/packages/dc/c5/3118bc20dc876edd84ec1610af6fda175f7b5a233fcef68099017cbc78fb/pi_heif-1.1.0-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:eb7c5bc1437fed64f47cb8b6ac9f556aa2263372e17d7360b2269a8fbc7a5679", size = 638831, upload-time = "2025-08-02T09:59:39.048Z" }, - { url = "https://files.pythonhosted.org/packages/0b/4d/6a5455768b4d10df26ed30eead89d49965cde29661972117c4a67df831a1/pi_heif-1.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2bf9a8905cab92e436f0d67cad55478f18f8842a6856ee71bd85af9c25c48551", size = 1251456, upload-time = "2025-08-02T09:59:40.663Z" }, - { url = "https://files.pythonhosted.org/packages/5b/16/1ce9562c6bbd0cfe8efa72fc30c25972e7f8fd100e19d5c1fba9c4448a38/pi_heif-1.1.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f74889fa91614efa29a100ea2a6f85a58dd35a6bd256c087c344684b08be7299", size = 1372549, upload-time = "2025-08-02T09:59:42.255Z" }, + { url = "https://files.pythonhosted.org/packages/19/3c/d411e9a0d27449b0b26d23085a1ad477c2ca5e1fac73f0c2ef236cbf8134/pi_heif-1.2.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:ff8f0e5493f97973b5fef8da892f20a410527ed7a1820de4ff3ba2a0a640d458", size = 1046547, upload-time = "2026-01-23T07:35:27.699Z" }, + { url = "https://files.pythonhosted.org/packages/4b/eb/4fd075e780d54566ce82b11d2eb610d5a4ddea48d2b7fd789e3e4a52285f/pi_heif-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b40a717ed9635186236496c1e27dde60fcec9853786889af0029bafb13626dbf", size = 941937, upload-time = "2026-01-23T07:35:29.236Z" }, + { url = "https://files.pythonhosted.org/packages/41/a8/913759f31930ff19cfbb8080c64474d1dea537259a46f8f699e527b20b18/pi_heif-1.2.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d2f73ef203e19e690d4e6ed336c7970b5fee2a0f1e38885a3c7f465eac6af16", size = 1359938, upload-time = "2026-01-23T07:35:31.732Z" }, + { url = "https://files.pythonhosted.org/packages/85/95/80fa59ab89d8581cd6a77462257e0b4e01cce61ca472a5034170bf70096a/pi_heif-1.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1f97cc4f842993dc7ac26e3f15748c63aa875ef8d935cf7ab957e02e35ae90d", size = 1487717, upload-time = "2026-01-23T07:35:33.124Z" }, + { url = "https://files.pythonhosted.org/packages/9c/9a/915a00114621a242bf07e8915b452d68a2516098836ffa6b24f7a165054c/pi_heif-1.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4e9702e4300655b6063816c55c1bbd044b5cc4215a7fe31f9a0d41451b815b28", size = 2342524, upload-time = "2026-01-23T07:35:34.866Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/d9362d013644aa75924f5a63c6db64664bdde6a7ca22cb48c6f70c6f34c2/pi_heif-1.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4110b98593aa2bc140f1c74491739db75b2e06f87aad8eaf625aecc1fe8c34ed", size = 2506178, upload-time = "2026-01-23T07:35:36.244Z" }, + { url = "https://files.pythonhosted.org/packages/7e/26/a08d352e861153f6c61ead733cda4a1b237636ca8de3edfe79e0039ef1bf/pi_heif-1.2.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:680939024f6dd760925f0bffbe24a325845b8a4a6acf380ba6251b34485adc05", size = 1046544, upload-time = "2026-01-23T07:35:38.983Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/e3fb06ee73b262c1a2cb611dc1439e7b189b50cc5539c68fc743c68b1169/pi_heif-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f2cb2a102175b59acb3d61f93499553609ab07911284b30e6255fbee23c9347", size = 941939, upload-time = "2026-01-23T07:35:40.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/60/66d2de00df006b4e7eeb04d4e9cdce4cb26be3aa16b1db014323f5effd9f/pi_heif-1.2.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d486b40f71a57e401625ae853f7b0b70ce0027c2378a2f69af89aa5f49d96b72", size = 1361699, upload-time = "2026-01-23T07:35:41.411Z" }, + { url = "https://files.pythonhosted.org/packages/8d/e1/30143e60e0a51d1c7c8c73f920dbc90f1e46ae32195826b8690598657f28/pi_heif-1.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13e46b0850ef4b66e2fee9a3ad8b3c337ae1a93e963fe2180feb0b6159af0e03", size = 1489366, upload-time = "2026-01-23T07:35:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/66/b8/34276a703bc7a60deb1bb723cfeaac26720cbea530e9ef792183dc2d8c31/pi_heif-1.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d37c88e3da7c285e58de9b68c778ec241e2ad4722b4cc25e9068eb51e41d6fa2", size = 2344080, upload-time = "2026-01-23T07:35:44.57Z" }, + { url = "https://files.pythonhosted.org/packages/12/d3/b2113375e31eb878dc8958306689e19f630f216f2e0425a86b7fc6efa267/pi_heif-1.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dbc53e52f940394351f85c7fa1c7cabc845a18d924245806cafb91c29998804c", size = 2507697, upload-time = "2026-01-23T07:35:45.995Z" }, + { url = "https://files.pythonhosted.org/packages/22/fb/1b77865d7c003ca620a91faad2e66005817eb5cb35ddcd64c047693d3d16/pi_heif-1.2.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:e007c14570acf9e522e9a7e750bcfbd6924b2a9d86dd845857cce203ec5d696f", size = 1046786, upload-time = "2026-01-23T07:35:49.039Z" }, + { url = "https://files.pythonhosted.org/packages/e4/32/1ab6c52f1b7123c26be15851a8b10bb47d32961459895ca95c42b9f31a1a/pi_heif-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d74f70d60549f7198b1b1954bfceff48f5b527229cf211b83905c458819ed5a", size = 941880, upload-time = "2026-01-23T07:35:50.187Z" }, + { url = "https://files.pythonhosted.org/packages/48/73/090f23ed9f96e8f3f9af0dcdcb5eb05d49284932a3c32a14b05b4460c127/pi_heif-1.2.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:760d3ca420a46514cfd06a440d46c10eb0fbea5cc9c2c8fbd151c520a907a248", size = 1360268, upload-time = "2026-01-23T07:35:51.398Z" }, + { url = "https://files.pythonhosted.org/packages/ed/04/64b3777d44bec9b80d901ae2e67a4f13f221e43f947f24b26aca30d7e250/pi_heif-1.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:264e8e50835c2e7f835f92508580da5859b63fa61ffe9319b2b97feba2120ccc", size = 1488770, upload-time = "2026-01-23T07:35:52.576Z" }, + { url = "https://files.pythonhosted.org/packages/69/07/1503ae48aacbe6d2449c494f4a2324f13fd8ddbf5111cadc18bc559c7a5d/pi_heif-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:46e934a72f7baed86525d9e0511a234b687b6aa80a764b33b42eedbe3d56d860", size = 2342938, upload-time = "2026-01-23T07:35:54.003Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ae/2c548ea4e91ecb5c5d772362f13451dbc4e9de4a777c4cb220bef22beb76/pi_heif-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8307d668d40b156b9d19d13158a4a015540061f1694b4c6593a931e823c5959c", size = 2507029, upload-time = "2026-01-23T07:35:55.456Z" }, + { url = "https://files.pythonhosted.org/packages/36/a9/2c0cc22e4649055b95e5765dadaaff133177c40754ed5a0a434d8d88ceeb/pi_heif-1.2.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:06947b98598026cf71df9ab841a17bd2cf0da704789e4fff73311d79539d6cc5", size = 1046774, upload-time = "2026-01-23T07:35:58.182Z" }, + { url = "https://files.pythonhosted.org/packages/5e/71/a4774de12b5e9bf576b013d81f2e8001f2b1fd81b6368e0e20d220c92e6a/pi_heif-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3b7bdefbf3bec7dd644bc8e17810d1f658db2ee60f35ef3943fcb9b435aca479", size = 941879, upload-time = "2026-01-23T07:35:59.66Z" }, + { url = "https://files.pythonhosted.org/packages/66/c6/5c58d3083adfd9b6f9753431d398842120bdbc33d3815bb561aeea669da0/pi_heif-1.2.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ad88e50bcb0aa25f9febde017ce7fce6801d927f032c1983d6846ead106c50e", size = 1360283, upload-time = "2026-01-23T07:36:00.835Z" }, + { url = "https://files.pythonhosted.org/packages/0b/24/40ecac0eb3c046a4afcf30ef187f45bf640bd7a30506f2f678ee4b25f7f2/pi_heif-1.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c90cf1238c75fb118eaeedf573fca833fef9ace8788c527f76758ac038e262d", size = 1488814, upload-time = "2026-01-23T07:36:02.351Z" }, + { url = "https://files.pythonhosted.org/packages/06/73/85265720fd58c72cd1c96eac47a27bdf8d2c88845fa4644b92b16e8d3340/pi_heif-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b4c4fc7a877807ef7f156f26ae920074d1f40a96868a5962ead743049b7c96c8", size = 2342994, upload-time = "2026-01-23T07:36:03.782Z" }, + { url = "https://files.pythonhosted.org/packages/be/9f/d00b4466382ecdd06cb11695c75d434856cf58504b1bfc1c0899a838cdb5/pi_heif-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eb7f0fcdbc80ae75b0881ee1e63d1e8b873df72ff2bdd154d500d6d0644d22e2", size = 2507054, upload-time = "2026-01-23T07:36:05.085Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b5/fcf63fc20a1513062671a40348248d992b0c50c37112b2e155347ffbb5a1/pi_heif-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:cde6e4ebfdae0044d2852036d7f4f2399f8f89923501eceaecc564efc6a82899", size = 1046814, upload-time = "2026-01-23T07:36:08.071Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d9/bb7e517d96725bcc0fb16151b49d752f23d069440dcfa24e9acca55617e1/pi_heif-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3e51c6a56868be96534bd04c521637c71ad39b7a65a5aaa297adebe7e2d15ffb", size = 941968, upload-time = "2026-01-23T07:36:09.97Z" }, + { url = "https://files.pythonhosted.org/packages/41/82/b52a5f4e24dfb7b7a90841637b7668b13156e36bea31ecc8f0695bb53fc6/pi_heif-1.2.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b663f82cc3c87e315977577e6d267ecce2a17c96a766aae3fd807abc0ab45900", size = 1360470, upload-time = "2026-01-23T07:36:11.16Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1a/de369800616a00b1e1237563d8cea44da43912c31ec3ce7aa38b393ac0bd/pi_heif-1.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5e62c54ba42ca4d74fba84f92668e6bf825f4b827fb182b5e22244a2e2fb1b3", size = 1488949, upload-time = "2026-01-23T07:36:12.859Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ee/6fc4cec1a9fa71897b7664880d6f681e22bfddfb9364db847405fbee8fc3/pi_heif-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bc0fa16a0751aba3a3d5fb222fd5587a789dd79a675ddf0532de5ec090e0003a", size = 2343115, upload-time = "2026-01-23T07:36:14.147Z" }, + { url = "https://files.pythonhosted.org/packages/26/28/74736d7d0fc3efea3cc5b82a5a310fcbc0dc6157844e4015e1bcf7222498/pi_heif-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fe00abdb62faf1a37ef77d01ed7b0302196897a47da1fc14758ae1522a705733", size = 2507162, upload-time = "2026-01-23T07:36:15.634Z" }, + { url = "https://files.pythonhosted.org/packages/79/80/d86e455d9b001ab5064c80b66748e0666c1624a3889c7779727514ee562c/pi_heif-1.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0495913cfd4ddf726fd3dc12cc0af065218f682bfb091feb1641223e7563065b", size = 1034973, upload-time = "2026-01-23T07:36:18.37Z" }, + { url = "https://files.pythonhosted.org/packages/37/4c/2588670e2196760a9e8db079830f88b2112ad055c5ad2cd1b232efafb9b9/pi_heif-1.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5d8f049862694534eced877e438df0687e7cb3e037348ab5792bee8fc86f2633", size = 938424, upload-time = "2026-01-23T07:36:19.709Z" }, + { url = "https://files.pythonhosted.org/packages/94/99/ed05f4b9442c5078f94c627f0b79b5df5f5f8c9e4b01390cd7057f82044e/pi_heif-1.2.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b0407727fdda6d410481e3b2ccd1c9eb1eb0e762aeb40c87a368f7b1f5d9d44", size = 1320112, upload-time = "2026-01-23T07:36:21.141Z" }, + { url = "https://files.pythonhosted.org/packages/b6/5c/05b2db716de7b4b47d97e27e65130eb759a2fa035c770677b6be06bc491f/pi_heif-1.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c698bf9bf4e39be88e91f1c12de603fbf321034ba5aee9280b789dae13532a71", size = 1444880, upload-time = "2026-01-23T07:36:22.477Z" }, ] [[package]] name = "pikepdf" -version = "10.0.2" +version = "10.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "deprecated", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -2524,126 +3544,123 @@ dependencies = [ { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "pillow", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f7/79/9a63d5ccac66ace679cf93c84894db15074fe849d41cd39232cb09ec8819/pikepdf-10.0.2.tar.gz", hash = "sha256:7c85a2526253e35575edb2e28cdc740d004be4b7c5fda954f0e721ee1c423a52", size = 4548116, upload-time = "2025-11-10T18:10:08.765Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/e9/a1462d6160805ca80c8f4aafc941aaf410a92d0fcc683706e94f499c2fac/pikepdf-10.2.0.tar.gz", hash = "sha256:0f398b0daeb2ffd2358f75c06f1dd47b9ba76f1a77dfe938cccf7080c58227d7", size = 4568506, upload-time = "2026-01-09T22:54:25.847Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/fa/ab2b88c097b4542663065631f0a1f693ed2a6e585cf92d6226267d0f66ad/pikepdf-10.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:2698975488753fd0d8d06bf2d15c809f2b0be7f34eb75f068161d7313f331b3b", size = 4673132, upload-time = "2025-11-10T18:08:54.191Z" }, - { url = "https://files.pythonhosted.org/packages/a7/04/20985de6520c5d7018fc7d2fd9d2804d9348d39df3c08d11f115c522fcde/pikepdf-10.0.2-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:e018fae8df61b2d5aa376ff381178f9d6930ee68d24b26a9a4409f8ff7c7cb1e", size = 4972194, upload-time = "2025-11-10T18:08:58.588Z" }, - { url = "https://files.pythonhosted.org/packages/97/8b/b32af8b69f475a736904d34e83e136bae0ca7fe221090cd36ee136112efc/pikepdf-10.0.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af565ad6ff5d96611a657c1e12e1429749f36271e7e368f82ba3a8a4635ac3dd", size = 2379539, upload-time = "2025-11-10T18:09:00.278Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ca/79f9886ad5322b603adc823b2663bcfb51a2729b9feb14dcb270e94528dd/pikepdf-10.0.2-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d150903852630b89d67832e56d7fb0b2bfd0e228f269d498de5d28b53078db9", size = 2596832, upload-time = "2025-11-10T18:09:01.889Z" }, - { url = "https://files.pythonhosted.org/packages/09/52/2793b5dd95611614b96cde0f78736910506abdab87a7038d126093c8f0ed/pikepdf-10.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:99c9cc66689120eba0aec858256ab4c3661e792a1d2a6c07575090c9a54d5bf3", size = 3574220, upload-time = "2025-11-10T18:09:04.25Z" }, - { url = "https://files.pythonhosted.org/packages/82/be/b47271b5e13bb0c678118a132066c29a90bfd6788224e1a73fecf2eb548d/pikepdf-10.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b75a29c2adeb8ae0277c76d232f75517c2cbd23f43212f8daac267d25f1cc656", size = 3757893, upload-time = "2025-11-10T18:09:05.962Z" }, - { url = "https://files.pythonhosted.org/packages/b4/bc/baff13dff8422c13e37bcb4b53bd55764cce88c7f6d8e7ff43f2dcb4f4ee/pikepdf-10.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:92a801d90cf7cab88c750d964de30cfe06dc04449cb51c38a863af75caa8b8cb", size = 4675949, upload-time = "2025-11-10T18:09:09.827Z" }, - { url = "https://files.pythonhosted.org/packages/d5/0d/158efe9a1a160b244c071e1893f154dfd905148c545d5f88d216b7f32f89/pikepdf-10.0.2-cp311-cp311-macosx_15_0_x86_64.whl", hash = "sha256:2f2c58f5b39e3e87d34d3a596922210f63e730a78c2aa6201667881b2c41a878", size = 4974326, upload-time = "2025-11-10T18:09:11.848Z" }, - { url = "https://files.pythonhosted.org/packages/48/0e/b2b6007d500dd6b76b6cffc8ec9f869395e55e19521a2f5bf988043c8302/pikepdf-10.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de987c50205a316a31bc46e5e6a32592244895cb9a95186ba5bcd398272e7d69", size = 2387154, upload-time = "2025-11-10T18:09:15.787Z" }, - { url = "https://files.pythonhosted.org/packages/ef/e5/094989c2db7d778fe47343d0a4dff610228e10eae2426be8ed6feb0f43b3/pikepdf-10.0.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d3736dfeba9eaf0aa710317d4dbdaee32d55be18d36a1b29fab2e4361cb7ae0", size = 2606870, upload-time = "2025-11-10T18:09:18.048Z" }, - { url = "https://files.pythonhosted.org/packages/dc/6d/5b6561a546e4036f6fa203cd8c5afe0874fe272b2b1c82b2350519ac9e8c/pikepdf-10.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d3b1fd46081dbc0302ef058f0bdadefe8f6db1de00b332759ad7cd32efa2705", size = 3583199, upload-time = "2025-11-10T18:09:19.781Z" }, - { url = "https://files.pythonhosted.org/packages/bc/b7/9794e2127eedbc01db1232bea4bfa86e38513fa3c78cba4f9d2837c6f230/pikepdf-10.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7ca982269f3fe084a9267d323c377f0afa6de33b3fd2dca4df67289001fce042", size = 3768564, upload-time = "2025-11-10T18:09:22.693Z" }, - { url = "https://files.pythonhosted.org/packages/42/03/ef096d5bccf70606fcd40ef3519e048028144ebdd5735092398d9cf0ee3f/pikepdf-10.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:0a5e798d6b0759bae1e30b8b05853b5c8d4016129db658f3a3e3320781ef2376", size = 4687898, upload-time = "2025-11-10T18:09:26.145Z" }, - { url = "https://files.pythonhosted.org/packages/40/27/cd69b14359772b3a447aa6687da3436fcdf16664be06ca88aef984453217/pikepdf-10.0.2-cp312-cp312-macosx_15_0_x86_64.whl", hash = "sha256:8d8ecc258f90cd1287f8527dd3b94aa178be7c0e04f313ae4182f21c24fd328d", size = 4985422, upload-time = "2025-11-10T18:09:27.955Z" }, - { url = "https://files.pythonhosted.org/packages/f9/cb/442ff1273ac155d1cd0f81fb7acf9848f8c4b595616ed8d2d846b9327e31/pikepdf-10.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ca4d5d1ca3f7af568e62cadf1c64238490b6503a512894c99e48042e3eb3648", size = 2395555, upload-time = "2025-11-10T18:09:30.129Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d0/9d4ed596e5419dd4bc8c162f0337398dc1ddc94e2d7bf6f3d0fb6eef561b/pikepdf-10.0.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fa22c5496e14fa3e89b94117fe56494b895cd02e53842f60331ac4bc078f04c7", size = 2631976, upload-time = "2025-11-10T18:09:32.214Z" }, - { url = "https://files.pythonhosted.org/packages/86/59/205f83746288590f22d1ff8b43fc93b193ddeca26261c61b5621d03048a1/pikepdf-10.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:df502e0c6d0731bd3f98523e5d501d4a21b36844f0061ef25f2500b18766fb51", size = 3588060, upload-time = "2025-11-10T18:09:34.252Z" }, - { url = "https://files.pythonhosted.org/packages/b2/54/4e9c8b53f22a4e527cf1087c5b26e127aa028c51d81cf53b10f34c34db8d/pikepdf-10.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:98464b4d8f0340ab38575cc41acf9061e2639c90e3a25c491c3ef3d01b92899f", size = 3791776, upload-time = "2025-11-10T18:09:36.055Z" }, - { url = "https://files.pythonhosted.org/packages/b3/f1/eabc9e780f9d0fe0316ce0185a1064d28b18c219fd8d1b0609205c18ac39/pikepdf-10.0.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f06c8dbf1f6cab87815b7ed0e4b1359da8665c4bb51a9fbdd71824c0b1bbb28c", size = 4687891, upload-time = "2025-11-10T18:09:40.003Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3a/ce1cf39d9eac09885efa69cc6bbe537ec2b7a24beded2fd0d9de779513f4/pikepdf-10.0.2-cp313-cp313-macosx_15_0_x86_64.whl", hash = "sha256:c3d421c6d4ef1aa394d30c683bb18c436817467c4db8279a4ff0f9d0a96aa323", size = 4985564, upload-time = "2025-11-10T18:09:42.624Z" }, - { url = "https://files.pythonhosted.org/packages/2f/f2/91664c4a7bda3fd3240e99a8ea602bb674ae88eea5dd798fa54c763da7e5/pikepdf-10.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0487fa6f64c26baa3f7131a917c37bb1183364a213678c155184944ca711881d", size = 2396504, upload-time = "2025-11-10T18:09:44.622Z" }, - { url = "https://files.pythonhosted.org/packages/12/8f/84717f30989f81ba94188a0185791039b683ed4ab43003ab5114aa07f154/pikepdf-10.0.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55378c0c1d7c32c32466809fc4c7a08efd2d6a0f75e22b25e3791ab33001382b", size = 2635392, upload-time = "2025-11-10T18:09:46.301Z" }, - { url = "https://files.pythonhosted.org/packages/0c/1c/960ff2fe0c11ab936db04202da6be136909757ecf131690b58d2aeef4d67/pikepdf-10.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cb5d7278cf9655eedde443f737ffab57eeaf81b5585094644759c368d28ac1bd", size = 3588629, upload-time = "2025-11-10T18:09:48.042Z" }, - { url = "https://files.pythonhosted.org/packages/04/56/9e6d87d20c520afb8afe28cddf37ddaa4a70aaf9aec2e25d53522a91d9c4/pikepdf-10.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e460726e0753b0196a241f11f80c90a30fcaf3e7bc7d908cf85594890cb981c", size = 3792852, upload-time = "2025-11-10T18:09:50.213Z" }, - { url = "https://files.pythonhosted.org/packages/f2/92/ddc07c58bb0e99f6c7ed5cdec63ed305bf22d29f875fb7a5b626f3eca177/pikepdf-10.0.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:821592290f33943d0dbde294a56db2b777b7edf519dabbe77d1a8e99a96f96ea", size = 4683719, upload-time = "2025-11-10T18:09:53.991Z" }, - { url = "https://files.pythonhosted.org/packages/9b/05/f43a0dde38720c960910ea7c2916a608d6dcb42d4cdf00b6a202704a0e35/pikepdf-10.0.2-cp314-cp314-macosx_15_0_x86_64.whl", hash = "sha256:64f46f9db208cd4166b73da31e201258e47cb3c3645d9856782e9fa041503268", size = 4986050, upload-time = "2025-11-10T18:09:56.36Z" }, - { url = "https://files.pythonhosted.org/packages/2a/7d/2899179c383d6dd53c7f74a772f6df16f4d6dca84584f777ebf1e1fb3f34/pikepdf-10.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:434ae183af796288d918be2825f950947a9b329850008bb97743f3297529a537", size = 2402087, upload-time = "2025-11-10T18:09:58.877Z" }, - { url = "https://files.pythonhosted.org/packages/a8/9f/c6989364ffa8b44b2c581316a3e59dac4497591336e941171e80e27bf664/pikepdf-10.0.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e23617b12fceee49a5b17b0ad7921ef50eaf4e1d23babec893b8dbc498bb3f2", size = 2637508, upload-time = "2025-11-10T18:10:00.684Z" }, - { url = "https://files.pythonhosted.org/packages/96/9c/72846bf3454637450cca8cc9620dbb6d7df035ed3ff41b656c4a516e5495/pikepdf-10.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:09da390be9d8558b77372a016498fc3d8edc9e5fd3928557f6012cb29f9114fb", size = 3595197, upload-time = "2025-11-10T18:10:02.688Z" }, - { url = "https://files.pythonhosted.org/packages/77/2c/b8221889158a748e3e74edbba9590fcb6516605fa169cb786b3d81f71129/pikepdf-10.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9165ef885b657a0602623a996ea230a450e7774b48e9d7bf463f46b0dbc738df", size = 3796398, upload-time = "2025-11-10T18:10:04.826Z" }, + { url = "https://files.pythonhosted.org/packages/40/e8/b2416ce9f0cd1073eee95cdfb619e9c3ad9f85cfa3be23ec2c1d36addf83/pikepdf-10.2.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:d3fa2b7c6d3144016ca67e27345dfc906b7024c0430bbcd95a6dbb8621eb1ff6", size = 4725874, upload-time = "2026-01-09T22:53:11.153Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c4/d4bdded64774159807638e38b01fd94406373b0d426116468a6d1de171a0/pikepdf-10.2.0-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:bb039c6ce74b25ab5d5134c517ee5393549313e84c632730334c3f92b40577af", size = 5028741, upload-time = "2026-01-09T22:53:14.493Z" }, + { url = "https://files.pythonhosted.org/packages/b3/03/0cca3a083a572f9053287a9527ba08896489230e9c02ded8a8da27e62015/pikepdf-10.2.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2756364f28c38aac591c57d67e0f0e665b2b7ef322d0cba63e6eff838b175b18", size = 2429917, upload-time = "2026-01-09T22:53:16.501Z" }, + { url = "https://files.pythonhosted.org/packages/52/5e/8f2b4e2ba2ec422c5badeb749520d8f3a54d3562782d906091ae9202d934/pikepdf-10.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:456b099efeec5b87ed4f41890e92e06404c6fd0f2d54fe279ea17575d1f074aa", size = 2659883, upload-time = "2026-01-09T22:53:18.113Z" }, + { url = "https://files.pythonhosted.org/packages/33/f7/79cd43342621c008a40d1cd89e757489c5eb2240104cc4aadd6e25ffbd9b/pikepdf-10.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e069a79b30f7bfed037d3a0afc11fd1d2b1f60d49ba70be6e084cd5583ea9508", size = 3629014, upload-time = "2026-01-09T22:53:19.764Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4f/3ccd822515903f4959b2bfc6288accaed8ecd9df646e887da876ca67fdaa/pikepdf-10.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a2b2c4132690015bb46b288bf0fd859debecd560c1ba133a61b23e8bb08427d8", size = 3822294, upload-time = "2026-01-09T22:53:21.949Z" }, + { url = "https://files.pythonhosted.org/packages/10/dc/aa7293763b603a9080ffab7ab87c7b571d637a389e9fb2ba839b864ca283/pikepdf-10.2.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:fb93732127d5183a91300af39e1cda5ded309e8439daec93536331a472b5e190", size = 4727891, upload-time = "2026-01-09T22:53:26.262Z" }, + { url = "https://files.pythonhosted.org/packages/d2/dc/700c31f2c14f94d92483b10e1918390948ed20f6f572d82beb78ac5f94d0/pikepdf-10.2.0-cp311-cp311-macosx_15_0_x86_64.whl", hash = "sha256:ab7bd4629539cf2136a799dc3eaa2dfda59937035a97b0c5e22a7a3a4033cc49", size = 5030510, upload-time = "2026-01-09T22:53:28.023Z" }, + { url = "https://files.pythonhosted.org/packages/23/46/dc63364b05aa1913f2d7480cad62676bfb473065ba4b02d314dfd482f7dd/pikepdf-10.2.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f5623a5ba456d69dfeb86dc3bb3ec31ec1d120382d8c24804d1b430fce715ea", size = 2439498, upload-time = "2026-01-09T22:53:30.122Z" }, + { url = "https://files.pythonhosted.org/packages/59/b6/1f9b8ca588fd34d9e3df49a80c62016e0b42ce6e580146c46d9728fdb6e8/pikepdf-10.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dec4d12f294df378d122ae441c27c1e76fb0d15b1e9d7374ae70c26604559bab", size = 2666945, upload-time = "2026-01-09T22:53:32.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/31/b1e61fac59f0b807edde655a821ff83bb041ae1500234c52ed1a2403c44a/pikepdf-10.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c0cebe3235232f1bd3c5f7956218ce92241c94223cb80eba837d372a40c61765", size = 3638109, upload-time = "2026-01-09T22:53:34.144Z" }, + { url = "https://files.pythonhosted.org/packages/02/e9/a99bbf503c9d55e54553edff84ec67cac49d335fc33f3d5516c4746b6340/pikepdf-10.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d0908e845c9140e245ad89a19fdfc6e5a6d82fcb505b8cc2c0ce81439ac4f064", size = 3829538, upload-time = "2026-01-09T22:53:36.341Z" }, + { url = "https://files.pythonhosted.org/packages/73/18/598383493a0f0f0c4eecd09b8fe06dddb9d326a89e2623a134d43e051485/pikepdf-10.2.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:18c35d00baff72bfae82d67028bedb02ea2b208e1af5545c23cd681f2487a279", size = 4737716, upload-time = "2026-01-09T22:53:39.973Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/bec04784ba07d44f03b52ea524bcb7409bf7185ee8abec7ae29e3ac9e9ae/pikepdf-10.2.0-cp312-cp312-macosx_15_0_x86_64.whl", hash = "sha256:dd849d033b95de15965c095ebc4d78983099a11bb7b7897801dfaf3cb4083a35", size = 5042152, upload-time = "2026-01-09T22:53:41.808Z" }, + { url = "https://files.pythonhosted.org/packages/38/3e/148b3c8e101c8ac3a33f41e86c5739413575495e471bce45ee228aafcbd6/pikepdf-10.2.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9910efdc7907af3da9e7b2a125a1f67d512165ffa623f62825deeb642669a7a", size = 2445796, upload-time = "2026-01-09T22:53:44.027Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ef/b06f8fd68c34fed631cb8e3520dd955e59987de0eee6960dbc94bed11711/pikepdf-10.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0ec947e6429d7a3306153d32a0142462fdd8f905c5fe08c8a8e8c53b9c28a5c", size = 2693908, upload-time = "2026-01-09T22:53:46.039Z" }, + { url = "https://files.pythonhosted.org/packages/3e/bf/e5c40e9210e2ae8da7cad2cf6ae7d1db3b63a2916e6040645958e9ab4054/pikepdf-10.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b6383219a1cd31400403a69737a4e2a0c5d2a2c4cb9f380bcf45e33e8de802ea", size = 3643423, upload-time = "2026-01-09T22:53:48.333Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1b/969dfb29dc9fd7b82fa7bc065df498e8a3e7ddb81e982140634ee539a8db/pikepdf-10.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:46d2f9ef5a84949bfc11152a323558f94cf85d9d97e9c510c061c7f803028f3f", size = 3854816, upload-time = "2026-01-09T22:53:50.158Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c5/e6f9e3407dd73ec570000a64747ff84e2f57b06b0477d1da6eaca5038162/pikepdf-10.2.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:09ff28d1de7fc7711a7ef8dfc40396d9243b64ee24c37cd1ab2a9f9827895caa", size = 4737680, upload-time = "2026-01-09T22:53:54.905Z" }, + { url = "https://files.pythonhosted.org/packages/eb/de/dffb785235ac2d930db86b215c1848d7258e625fa1949dd0633f8b72ab0a/pikepdf-10.2.0-cp313-cp313-macosx_15_0_x86_64.whl", hash = "sha256:62348b66e1401a4db0c64976b72dd74bb1a9eb3a33007a661500f4f8a64436bd", size = 5042150, upload-time = "2026-01-09T22:53:57.722Z" }, + { url = "https://files.pythonhosted.org/packages/1a/f0/4d883f57304d98650ade30a8c73fe593582d9afd9a7dada1f5f3f4cce362/pikepdf-10.2.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b9e91780cb9ea3c6a350ffbcf03d5d95c30084d238afbd1d4b927cdb9e3649d", size = 2445446, upload-time = "2026-01-09T22:53:59.993Z" }, + { url = "https://files.pythonhosted.org/packages/62/65/ffe2555812a152d616accacea7c1c617c27a75590379ea7d9cc3a26bd92d/pikepdf-10.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52360a49a22e9353ec9a08ff5713cec8aacaf3ef960c704bc0a89ca8f050bdad", size = 2696242, upload-time = "2026-01-09T22:54:01.687Z" }, + { url = "https://files.pythonhosted.org/packages/ed/8c/2f937b0e2867cd48b523122e08753571fc9847978e239d7b5db9bd46879c/pikepdf-10.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ba4c1046939eb22c24c396deb37f8e0500caaa66b73114be55377d5554b4167", size = 3643730, upload-time = "2026-01-09T22:54:04.177Z" }, + { url = "https://files.pythonhosted.org/packages/2e/17/f2919e4085c399e938bb945ea712dea70b3849e17cae6403f0cc1100e9ef/pikepdf-10.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3dcd8957a08e0a47f7a138904dca8cf73962fa17a096a47cd8bc33eb83a4f0a7", size = 3856645, upload-time = "2026-01-09T22:54:07.887Z" }, + { url = "https://files.pythonhosted.org/packages/1d/6e/846902abe8286d3b4ab70893e9ffbeec99aadd93ba1536cf471b222bb910/pikepdf-10.2.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:194c9a81ecb49e425a5cd5162621270b5e42cf05709d87eac018bd6f9ce98f80", size = 4733930, upload-time = "2026-01-09T22:54:11.931Z" }, + { url = "https://files.pythonhosted.org/packages/8b/6d/abdbb794d2a512d4e828ef2014cc47ca263ad3fbd1b65f25f791b9c0bb1e/pikepdf-10.2.0-cp314-cp314-macosx_15_0_x86_64.whl", hash = "sha256:5adcf87dbfff4e1cd0a850db487274f474c94a6bf6347f3842c53da8d0eaa8df", size = 5042477, upload-time = "2026-01-09T22:54:13.708Z" }, + { url = "https://files.pythonhosted.org/packages/52/6c/6c42694fe1574a37aa2a40b4ba29a6713b4226436155ef7aa0bef649c117/pikepdf-10.2.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5de3cecbb35c4bc651e9326932974217be1d450d4a9840d77a592062eb507e27", size = 2448419, upload-time = "2026-01-09T22:54:15.6Z" }, + { url = "https://files.pythonhosted.org/packages/45/f4/aca3286aa37ace581afc8e3e0644a0cc55b9f9ceb31f28219d12ca11536c/pikepdf-10.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:77868fd25182a45a4f3dec3c461aea8c696ef9565894c5cde4394bc8c32fb069", size = 2697600, upload-time = "2026-01-09T22:54:18.123Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a6/9135f9f0189634de61410573a0712d849e0157e3902e6b867339cc7dbf1b/pikepdf-10.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9a10e15e2f4d0bba36a2b4328342d00eff1a5a31399e1d1a93483c70d3c2b0e", size = 3647720, upload-time = "2026-01-09T22:54:20.158Z" }, + { url = "https://files.pythonhosted.org/packages/83/60/f282077773a3321fad4cbfb16fe73ee3f8dd93b408df65c24779f12227c5/pikepdf-10.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a8f80ecf00fb15a760f218432a1046e7797cd14eaa6ccb52c8814ae8852745d8", size = 3859133, upload-time = "2026-01-09T22:54:22.358Z" }, ] [[package]] name = "pillow" -version = "11.3.0" +version = "12.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860", size = 5316554, upload-time = "2025-07-01T09:13:39.342Z" }, - { url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad", size = 4686548, upload-time = "2025-07-01T09:13:41.835Z" }, - { url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0", size = 5859742, upload-time = "2025-07-03T13:09:47.439Z" }, - { url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b", size = 7633087, upload-time = "2025-07-03T13:09:51.796Z" }, - { url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50", size = 5963350, upload-time = "2025-07-01T09:13:43.865Z" }, - { url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae", size = 6631840, upload-time = "2025-07-01T09:13:46.161Z" }, - { url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9", size = 6074005, upload-time = "2025-07-01T09:13:47.829Z" }, - { url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e", size = 6708372, upload-time = "2025-07-01T09:13:52.145Z" }, - { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" }, - { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" }, - { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" }, - { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" }, - { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" }, - { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" }, - { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" }, - { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" }, - { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" }, - { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" }, - { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" }, - { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" }, - { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" }, - { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" }, - { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" }, - { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" }, - { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" }, - { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" }, - { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" }, - { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" }, - { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" }, - { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" }, - { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" }, - { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" }, - { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" }, - { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" }, - { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" }, - { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" }, - { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" }, - { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" }, - { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" }, - { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" }, - { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" }, - { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" }, - { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" }, - { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" }, - { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" }, - { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" }, - { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" }, - { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" }, - { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" }, - { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" }, - { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" }, - { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" }, - { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" }, - { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" }, - { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" }, - { url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967", size = 5270556, upload-time = "2025-07-01T09:16:09.961Z" }, - { url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe", size = 4654625, upload-time = "2025-07-01T09:16:11.913Z" }, - { url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c", size = 4874207, upload-time = "2025-07-03T13:11:10.201Z" }, - { url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25", size = 6583939, upload-time = "2025-07-03T13:11:15.68Z" }, - { url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27", size = 4957166, upload-time = "2025-07-01T09:16:13.74Z" }, - { url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a", size = 5581482, upload-time = "2025-07-01T09:16:16.107Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" }, - { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" }, - { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" }, - { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" }, - { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" }, + { url = "https://files.pythonhosted.org/packages/fe/41/f73d92b6b883a579e79600d391f2e21cb0df767b2714ecbd2952315dfeef/pillow-12.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd", size = 5304089, upload-time = "2026-01-02T09:10:24.953Z" }, + { url = "https://files.pythonhosted.org/packages/94/55/7aca2891560188656e4a91ed9adba305e914a4496800da6b5c0a15f09edf/pillow-12.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cad302dc10fac357d3467a74a9561c90609768a6f73a1923b0fd851b6486f8b0", size = 4657815, upload-time = "2026-01-02T09:10:27.063Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d2/b28221abaa7b4c40b7dba948f0f6a708bd7342c4d47ce342f0ea39643974/pillow-12.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a40905599d8079e09f25027423aed94f2823adaf2868940de991e53a449e14a8", size = 6222593, upload-time = "2026-01-02T09:10:29.115Z" }, + { url = "https://files.pythonhosted.org/packages/71/b8/7a61fb234df6a9b0b479f69e66901209d89ff72a435b49933f9122f94cac/pillow-12.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92a7fe4225365c5e3a8e598982269c6d6698d3e783b3b1ae979e7819f9cd55c1", size = 8027579, upload-time = "2026-01-02T09:10:31.182Z" }, + { url = "https://files.pythonhosted.org/packages/ea/51/55c751a57cc524a15a0e3db20e5cde517582359508d62305a627e77fd295/pillow-12.1.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f10c98f49227ed8383d28174ee95155a675c4ed7f85e2e573b04414f7e371bda", size = 6335760, upload-time = "2026-01-02T09:10:33.02Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7c/60e3e6f5e5891a1a06b4c910f742ac862377a6fe842f7184df4a274ce7bf/pillow-12.1.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8637e29d13f478bc4f153d8daa9ffb16455f0a6cb287da1b432fdad2bfbd66c7", size = 7027127, upload-time = "2026-01-02T09:10:35.009Z" }, + { url = "https://files.pythonhosted.org/packages/06/37/49d47266ba50b00c27ba63a7c898f1bb41a29627ced8c09e25f19ebec0ff/pillow-12.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:21e686a21078b0f9cb8c8a961d99e6a4ddb88e0fc5ea6e130172ddddc2e5221a", size = 6449896, upload-time = "2026-01-02T09:10:36.793Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e5/67fd87d2913902462cd9b79c6211c25bfe95fcf5783d06e1367d6d9a741f/pillow-12.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2415373395a831f53933c23ce051021e79c8cd7979822d8cc478547a3f4da8ef", size = 7151345, upload-time = "2026-01-02T09:10:39.064Z" }, + { url = "https://files.pythonhosted.org/packages/43/c4/bf8328039de6cc22182c3ef007a2abfbbdab153661c0a9aa78af8d706391/pillow-12.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3", size = 5304057, upload-time = "2026-01-02T09:10:46.627Z" }, + { url = "https://files.pythonhosted.org/packages/43/06/7264c0597e676104cc22ca73ee48f752767cd4b1fe084662620b17e10120/pillow-12.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0", size = 4657811, upload-time = "2026-01-02T09:10:49.548Z" }, + { url = "https://files.pythonhosted.org/packages/72/64/f9189e44474610daf83da31145fa56710b627b5c4c0b9c235e34058f6b31/pillow-12.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451", size = 6232243, upload-time = "2026-01-02T09:10:51.62Z" }, + { url = "https://files.pythonhosted.org/packages/ef/30/0df458009be6a4caca4ca2c52975e6275c387d4e5c95544e34138b41dc86/pillow-12.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e", size = 8037872, upload-time = "2026-01-02T09:10:53.446Z" }, + { url = "https://files.pythonhosted.org/packages/e4/86/95845d4eda4f4f9557e25381d70876aa213560243ac1a6d619c46caaedd9/pillow-12.1.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84", size = 6345398, upload-time = "2026-01-02T09:10:55.426Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1f/8e66ab9be3aaf1435bc03edd1ebdf58ffcd17f7349c1d970cafe87af27d9/pillow-12.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0", size = 7034667, upload-time = "2026-01-02T09:10:57.11Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f6/683b83cb9b1db1fb52b87951b1c0b99bdcfceaa75febf11406c19f82cb5e/pillow-12.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b", size = 6458743, upload-time = "2026-01-02T09:10:59.331Z" }, + { url = "https://files.pythonhosted.org/packages/9a/7d/de833d63622538c1d58ce5395e7c6cb7e7dce80decdd8bde4a484e095d9f/pillow-12.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18", size = 7159342, upload-time = "2026-01-02T09:11:01.82Z" }, + { url = "https://files.pythonhosted.org/packages/20/31/dc53fe21a2f2996e1b7d92bf671cdb157079385183ef7c1ae08b485db510/pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b", size = 5262642, upload-time = "2026-01-02T09:11:10.138Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c1/10e45ac9cc79419cedf5121b42dcca5a50ad2b601fa080f58c22fb27626e/pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551", size = 4657464, upload-time = "2026-01-02T09:11:12.319Z" }, + { url = "https://files.pythonhosted.org/packages/ad/26/7b82c0ab7ef40ebede7a97c72d473bda5950f609f8e0c77b04af574a0ddb/pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208", size = 6234878, upload-time = "2026-01-02T09:11:14.096Z" }, + { url = "https://files.pythonhosted.org/packages/76/25/27abc9792615b5e886ca9411ba6637b675f1b77af3104710ac7353fe5605/pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5", size = 8044868, upload-time = "2026-01-02T09:11:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ea/f200a4c36d836100e7bc738fc48cd963d3ba6372ebc8298a889e0cfc3359/pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661", size = 6349468, upload-time = "2026-01-02T09:11:17.631Z" }, + { url = "https://files.pythonhosted.org/packages/11/8f/48d0b77ab2200374c66d344459b8958c86693be99526450e7aee714e03e4/pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17", size = 7041518, upload-time = "2026-01-02T09:11:19.389Z" }, + { url = "https://files.pythonhosted.org/packages/1d/23/c281182eb986b5d31f0a76d2a2c8cd41722d6fb8ed07521e802f9bba52de/pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670", size = 6462829, upload-time = "2026-01-02T09:11:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/25/ef/7018273e0faac099d7b00982abdcc39142ae6f3bd9ceb06de09779c4a9d6/pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616", size = 7166756, upload-time = "2026-01-02T09:11:23.559Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c7/2530a4aa28248623e9d7f27316b42e27c32ec410f695929696f2e0e4a778/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1", size = 4062543, upload-time = "2026-01-02T09:11:31.566Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1f/40b8eae823dc1519b87d53c30ed9ef085506b05281d313031755c1705f73/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179", size = 4138373, upload-time = "2026-01-02T09:11:33.367Z" }, + { url = "https://files.pythonhosted.org/packages/d4/77/6fa60634cf06e52139fd0e89e5bbf055e8166c691c42fb162818b7fda31d/pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0", size = 3601241, upload-time = "2026-01-02T09:11:35.011Z" }, + { url = "https://files.pythonhosted.org/packages/4f/bf/28ab865de622e14b747f0cd7877510848252d950e43002e224fb1c9ababf/pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587", size = 5262410, upload-time = "2026-01-02T09:11:36.682Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/583420a1b55e715937a85bd48c5c0991598247a1fd2eb5423188e765ea02/pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac", size = 4657312, upload-time = "2026-01-02T09:11:38.535Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fd/f5a0896839762885b3376ff04878f86ab2b097c2f9a9cdccf4eda8ba8dc0/pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b", size = 6232605, upload-time = "2026-01-02T09:11:40.602Z" }, + { url = "https://files.pythonhosted.org/packages/98/aa/938a09d127ac1e70e6ed467bd03834350b33ef646b31edb7452d5de43792/pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea", size = 8041617, upload-time = "2026-01-02T09:11:42.721Z" }, + { url = "https://files.pythonhosted.org/packages/17/e8/538b24cb426ac0186e03f80f78bc8dc7246c667f58b540bdd57c71c9f79d/pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c", size = 6346509, upload-time = "2026-01-02T09:11:44.955Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/632e58ec89a32738cabfd9ec418f0e9898a2b4719afc581f07c04a05e3c9/pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc", size = 7038117, upload-time = "2026-01-02T09:11:46.736Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a2/d40308cf86eada842ca1f3ffa45d0ca0df7e4ab33c83f81e73f5eaed136d/pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644", size = 6460151, upload-time = "2026-01-02T09:11:48.625Z" }, + { url = "https://files.pythonhosted.org/packages/f1/88/f5b058ad6453a085c5266660a1417bdad590199da1b32fb4efcff9d33b05/pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c", size = 7164534, upload-time = "2026-01-02T09:11:50.445Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ba/970b7d85ba01f348dee4d65412476321d40ee04dcb51cd3735b9dc94eb58/pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d", size = 5264816, upload-time = "2026-01-02T09:11:58.227Z" }, + { url = "https://files.pythonhosted.org/packages/10/60/650f2fb55fdba7a510d836202aa52f0baac633e50ab1cf18415d332188fb/pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0", size = 4660472, upload-time = "2026-01-02T09:12:00.798Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/5273a99478956a099d533c4f46cbaa19fd69d606624f4334b85e50987a08/pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554", size = 6268974, upload-time = "2026-01-02T09:12:02.572Z" }, + { url = "https://files.pythonhosted.org/packages/b4/26/0bf714bc2e73d5267887d47931d53c4ceeceea6978148ed2ab2a4e6463c4/pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e", size = 8073070, upload-time = "2026-01-02T09:12:04.75Z" }, + { url = "https://files.pythonhosted.org/packages/43/cf/1ea826200de111a9d65724c54f927f3111dc5ae297f294b370a670c17786/pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82", size = 6380176, upload-time = "2026-01-02T09:12:06.626Z" }, + { url = "https://files.pythonhosted.org/packages/03/e0/7938dd2b2013373fd85d96e0f38d62b7a5a262af21ac274250c7ca7847c9/pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4", size = 7067061, upload-time = "2026-01-02T09:12:08.624Z" }, + { url = "https://files.pythonhosted.org/packages/86/ad/a2aa97d37272a929a98437a8c0ac37b3cf012f4f8721e1bd5154699b2518/pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0", size = 6491824, upload-time = "2026-01-02T09:12:10.488Z" }, + { url = "https://files.pythonhosted.org/packages/a4/44/80e46611b288d51b115826f136fb3465653c28f491068a72d3da49b54cd4/pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b", size = 7190911, upload-time = "2026-01-02T09:12:12.772Z" }, + { url = "https://files.pythonhosted.org/packages/8c/87/bdf971d8bbcf80a348cc3bacfcb239f5882100fe80534b0ce67a784181d8/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91", size = 4062533, upload-time = "2026-01-02T09:12:20.791Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4f/5eb37a681c68d605eb7034c004875c81f86ec9ef51f5be4a63eadd58859a/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796", size = 4138546, upload-time = "2026-01-02T09:12:23.664Z" }, + { url = "https://files.pythonhosted.org/packages/11/6d/19a95acb2edbace40dcd582d077b991646b7083c41b98da4ed7555b59733/pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd", size = 3601163, upload-time = "2026-01-02T09:12:26.338Z" }, + { url = "https://files.pythonhosted.org/packages/fc/36/2b8138e51cb42e4cc39c3297713455548be855a50558c3ac2beebdc251dd/pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13", size = 5266086, upload-time = "2026-01-02T09:12:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/649056e4d22e1caa90816bf99cef0884aed607ed38075bd75f091a607a38/pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e", size = 4657344, upload-time = "2026-01-02T09:12:31.117Z" }, + { url = "https://files.pythonhosted.org/packages/6c/6b/c5742cea0f1ade0cd61485dc3d81f05261fc2276f537fbdc00802de56779/pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643", size = 6232114, upload-time = "2026-01-02T09:12:32.936Z" }, + { url = "https://files.pythonhosted.org/packages/bf/8f/9f521268ce22d63991601aafd3d48d5ff7280a246a1ef62d626d67b44064/pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5", size = 8042708, upload-time = "2026-01-02T09:12:34.78Z" }, + { url = "https://files.pythonhosted.org/packages/1a/eb/257f38542893f021502a1bbe0c2e883c90b5cff26cc33b1584a841a06d30/pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de", size = 6347762, upload-time = "2026-01-02T09:12:36.748Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5a/8ba375025701c09b309e8d5163c5a4ce0102fa86bbf8800eb0d7ac87bc51/pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9", size = 7039265, upload-time = "2026-01-02T09:12:39.082Z" }, + { url = "https://files.pythonhosted.org/packages/cf/dc/cf5e4cdb3db533f539e88a7bbf9f190c64ab8a08a9bc7a4ccf55067872e4/pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a", size = 6462341, upload-time = "2026-01-02T09:12:40.946Z" }, + { url = "https://files.pythonhosted.org/packages/d0/47/0291a25ac9550677e22eda48510cfc4fa4b2ef0396448b7fbdc0a6946309/pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a", size = 7165395, upload-time = "2026-01-02T09:12:42.706Z" }, + { url = "https://files.pythonhosted.org/packages/82/54/2e1dd20c8749ff225080d6ba465a0cab4387f5db0d1c5fb1439e2d99923f/pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2", size = 5268571, upload-time = "2026-01-02T09:12:51.11Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/571163a5ef86ec0cf30d265ac2a70ae6fc9e28413d1dc94fa37fae6bda89/pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61", size = 4660426, upload-time = "2026-01-02T09:12:52.865Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e1/53ee5163f794aef1bf84243f755ee6897a92c708505350dd1923f4afec48/pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51", size = 6269908, upload-time = "2026-01-02T09:12:54.884Z" }, + { url = "https://files.pythonhosted.org/packages/bc/0b/b4b4106ff0ee1afa1dc599fde6ab230417f800279745124f6c50bcffed8e/pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc", size = 8074733, upload-time = "2026-01-02T09:12:56.802Z" }, + { url = "https://files.pythonhosted.org/packages/19/9f/80b411cbac4a732439e629a26ad3ef11907a8c7fc5377b7602f04f6fe4e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14", size = 6381431, upload-time = "2026-01-02T09:12:58.823Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b7/d65c45db463b66ecb6abc17c6ba6917a911202a07662247e1355ce1789e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8", size = 7068529, upload-time = "2026-01-02T09:13:00.885Z" }, + { url = "https://files.pythonhosted.org/packages/50/96/dfd4cd726b4a45ae6e3c669fc9e49deb2241312605d33aba50499e9d9bd1/pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924", size = 6492981, upload-time = "2026-01-02T09:13:03.314Z" }, + { url = "https://files.pythonhosted.org/packages/4d/1c/b5dc52cf713ae46033359c5ca920444f18a6359ce1020dd3e9c553ea5bc6/pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef", size = 7191878, upload-time = "2026-01-02T09:13:05.276Z" }, + { url = "https://files.pythonhosted.org/packages/8b/bc/224b1d98cffd7164b14707c91aac83c07b047fbd8f58eba4066a3e53746a/pillow-12.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377", size = 5228605, upload-time = "2026-01-02T09:13:14.084Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ca/49ca7769c4550107de049ed85208240ba0f330b3f2e316f24534795702ce/pillow-12.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72", size = 4622245, upload-time = "2026-01-02T09:13:15.964Z" }, + { url = "https://files.pythonhosted.org/packages/73/48/fac807ce82e5955bcc2718642b94b1bd22a82a6d452aea31cbb678cddf12/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c", size = 5247593, upload-time = "2026-01-02T09:13:17.913Z" }, + { url = "https://files.pythonhosted.org/packages/d2/95/3e0742fe358c4664aed4fd05d5f5373dcdad0b27af52aa0972568541e3f4/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd", size = 6989008, upload-time = "2026-01-02T09:13:20.083Z" }, + { url = "https://files.pythonhosted.org/packages/5a/74/fe2ac378e4e202e56d50540d92e1ef4ff34ed687f3c60f6a121bcf99437e/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc", size = 5313824, upload-time = "2026-01-02T09:13:22.405Z" }, + { url = "https://files.pythonhosted.org/packages/f3/77/2a60dee1adee4e2655ac328dd05c02a955c1cd683b9f1b82ec3feb44727c/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a", size = 5963278, upload-time = "2026-01-02T09:13:24.706Z" }, ] [[package]] name = "platformdirs" -version = "4.4.0" +version = "4.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, ] [[package]] @@ -2666,7 +3683,7 @@ wheels = [ [[package]] name = "pre-commit" -version = "4.4.0" +version = "4.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -2675,9 +3692,9 @@ dependencies = [ { name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "virtualenv", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a6/49/7845c2d7bf6474efd8e27905b51b11e6ce411708c91e829b93f324de9929/pre_commit-4.4.0.tar.gz", hash = "sha256:f0233ebab440e9f17cabbb558706eb173d19ace965c68cdce2c081042b4fab15", size = 197501, upload-time = "2025-11-08T21:12:11.607Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/11/574fe7d13acf30bfd0a8dd7fa1647040f2b8064f13f43e8c963b1e65093b/pre_commit-4.4.0-py2.py3-none-any.whl", hash = "sha256:b35ea52957cbf83dcc5d8ee636cbead8624e3a15fbfa61a370e42158ac8a5813", size = 226049, upload-time = "2025-11-08T21:12:10.228Z" }, + { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, ] [[package]] @@ -2695,11 +3712,11 @@ wheels = [ [[package]] name = "prometheus-client" -version = "0.23.1" +version = "0.24.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/53/3edb5d68ecf6b38fcbcc1ad28391117d2a322d9a1a3eff04bfdb184d8c3b/prometheus_client-0.23.1.tar.gz", hash = "sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce", size = 80481, upload-time = "2025-09-18T20:47:25.043Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/58/a794d23feb6b00fc0c72787d7e87d872a6730dd9ed7c7b3e954637d8f280/prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9", size = 85616, upload-time = "2026-01-14T15:26:26.965Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl", hash = "sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99", size = 61145, upload-time = "2025-09-18T20:47:23.875Z" }, + { url = "https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055", size = 64057, upload-time = "2026-01-14T15:26:24.42Z" }, ] [[package]] @@ -2714,23 +3731,116 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, ] +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/0e/934b541323035566a9af292dba85a195f7b78179114f2c6ebb24551118a9/propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", size = 79534, upload-time = "2025-10-08T19:46:02.083Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/db0d03d96726d995dc7171286c6ba9d8d14251f37433890f88368951a44e/propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", size = 45526, upload-time = "2025-10-08T19:46:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c3/82728404aea669e1600f304f2609cde9e665c18df5a11cdd57ed73c1dceb/propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", size = 47263, upload-time = "2025-10-08T19:46:05.405Z" }, + { url = "https://files.pythonhosted.org/packages/df/1b/39313ddad2bf9187a1432654c38249bab4562ef535ef07f5eb6eb04d0b1b/propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", size = 201012, upload-time = "2025-10-08T19:46:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/5b/01/f1d0b57d136f294a142acf97f4ed58c8e5b974c21e543000968357115011/propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", size = 209491, upload-time = "2025-10-08T19:46:08.909Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c8/038d909c61c5bb039070b3fb02ad5cccdb1dde0d714792e251cdb17c9c05/propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", size = 215319, upload-time = "2025-10-08T19:46:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/08/57/8c87e93142b2c1fa2408e45695205a7ba05fb5db458c0bf5c06ba0e09ea6/propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", size = 196856, upload-time = "2025-10-08T19:46:12.003Z" }, + { url = "https://files.pythonhosted.org/packages/42/df/5615fec76aa561987a534759b3686008a288e73107faa49a8ae5795a9f7a/propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", size = 193241, upload-time = "2025-10-08T19:46:13.495Z" }, + { url = "https://files.pythonhosted.org/packages/d5/21/62949eb3a7a54afe8327011c90aca7e03547787a88fb8bd9726806482fea/propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", size = 190552, upload-time = "2025-10-08T19:46:14.938Z" }, + { url = "https://files.pythonhosted.org/packages/30/ee/ab4d727dd70806e5b4de96a798ae7ac6e4d42516f030ee60522474b6b332/propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", size = 200113, upload-time = "2025-10-08T19:46:16.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0b/38b46208e6711b016aa8966a3ac793eee0d05c7159d8342aa27fc0bc365e/propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", size = 200778, upload-time = "2025-10-08T19:46:18.023Z" }, + { url = "https://files.pythonhosted.org/packages/cf/81/5abec54355ed344476bee711e9f04815d4b00a311ab0535599204eecc257/propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", size = 193047, upload-time = "2025-10-08T19:46:19.449Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, + { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, + { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, + { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + [[package]] name = "psycopg" -version = "3.2.12" +version = "3.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform == 'linux')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/77/c72d10262b872617e509a0c60445afcc4ce2cd5cd6bc1c97700246d69c85/psycopg-3.2.12.tar.gz", hash = "sha256:85c08d6f6e2a897b16280e0ff6406bef29b1327c045db06d21f364d7cd5da90b", size = 160642, upload-time = "2025-10-26T00:46:03.045Z" } +sdist = { url = "https://files.pythonhosted.org/packages/96/bd/06dc36aeda16ffff129d03d90e75fd5e24222a719adcef37cd07f1926b06/psycopg-3.3.0.tar.gz", hash = "sha256:68950107fb8979d34bfc16b61560a26afe5d8dab96617881c87dfff58221df09", size = 165593, upload-time = "2025-12-01T11:35:07.076Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/28/8c4f90e415411dc9c78d6ba10b549baa324659907c13f64bfe3779d4066c/psycopg-3.2.12-py3-none-any.whl", hash = "sha256:8a1611a2d4c16ae37eada46438be9029a35bb959bb50b3d0e1e93c0f3d54c9ee", size = 206765, upload-time = "2025-10-26T00:10:42.173Z" }, + { url = "https://files.pythonhosted.org/packages/d8/5d/3569bab5a92f33e4a1b3c3c16816718ef5cc306f55f3965a8cb630c496ac/psycopg-3.3.0-py3-none-any.whl", hash = "sha256:c9f070afeda682f6364f86cd77145f43feaf60648b2ce1f6e883e594d04cbea8", size = 212759, upload-time = "2025-12-01T11:21:15.91Z" }, ] [package.optional-dependencies] c = [ - { name = "psycopg-c", version = "3.2.12", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version != '3.12.*' and implementation_name != 'pypy' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version != '3.12.*' and implementation_name != 'pypy' and platform_machine == 'x86_64' and sys_platform == 'linux') or (implementation_name != 'pypy' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (implementation_name != 'pypy' and sys_platform == 'darwin')" }, - { name = "psycopg-c", version = "3.2.12", source = { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-bookworm-3.2.12/psycopg_c-3.2.12-cp312-cp312-linux_aarch64.whl" }, marker = "python_full_version == '3.12.*' and implementation_name != 'pypy' and platform_machine == 'aarch64' and sys_platform == 'linux'" }, - { name = "psycopg-c", version = "3.2.12", source = { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-bookworm-3.2.12/psycopg_c-3.2.12-cp312-cp312-linux_x86_64.whl" }, marker = "python_full_version == '3.12.*' and implementation_name != 'pypy' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "psycopg-c", version = "3.3.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version != '3.12.*' and implementation_name != 'pypy' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version != '3.12.*' and implementation_name != 'pypy' and platform_machine == 'x86_64' and sys_platform == 'linux') or (implementation_name != 'pypy' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (implementation_name != 'pypy' and sys_platform == 'darwin')" }, + { name = "psycopg-c", version = "3.3.0", source = { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-trixie-3.3.0/psycopg_c-3.3.0-cp312-cp312-linux_aarch64.whl" }, marker = "python_full_version == '3.12.*' and implementation_name != 'pypy' and platform_machine == 'aarch64' and sys_platform == 'linux'" }, + { name = "psycopg-c", version = "3.3.0", source = { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-trixie-3.3.0/psycopg_c-3.3.0-cp312-cp312-linux_x86_64.whl" }, marker = "python_full_version == '3.12.*' and implementation_name != 'pypy' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, ] pool = [ { name = "psycopg-pool", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -2738,57 +3848,65 @@ pool = [ [[package]] name = "psycopg-c" -version = "3.2.12" +version = "3.3.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.11' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", "python_full_version < '3.11' and sys_platform == 'darwin'", - "(python_full_version >= '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'linux'", "python_full_version < '3.11' and sys_platform == 'linux'", ] -sdist = { url = "https://files.pythonhosted.org/packages/68/27/33699874745d7bb195e78fd0a97349908b64d3ec5fea7b8e5e52f56df04c/psycopg_c-3.2.12.tar.gz", hash = "sha256:1c80042067d5df90d184c6fbd58661350b3620f99d87a01c882953c4d5dfa52b", size = 608386, upload-time = "2025-10-26T00:46:08.727Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/96/5a86ed5c23911ca62b4745529a6ead68f504f6ae717f98527c979290e823/psycopg_c-3.3.0.tar.gz", hash = "sha256:07372de01bf18b3ebec726d6bbeed73be1af40b342c364695e80e11ff75607e3", size = 623980, upload-time = "2025-12-01T11:34:31.923Z" } [[package]] name = "psycopg-c" -version = "3.2.12" -source = { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-bookworm-3.2.12/psycopg_c-3.2.12-cp312-cp312-linux_aarch64.whl" } +version = "3.3.0" +source = { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-trixie-3.3.0/psycopg_c-3.3.0-cp312-cp312-linux_aarch64.whl" } resolution-markers = [ "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", ] wheels = [ - { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-bookworm-3.2.12/psycopg_c-3.2.12-cp312-cp312-linux_aarch64.whl", hash = "sha256:3492dc3760133ce8b422786136f41763acaa2dc890d3ab3e58b49627fd1e4e92" }, + { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-trixie-3.3.0/psycopg_c-3.3.0-cp312-cp312-linux_aarch64.whl", hash = "sha256:07b3848db40beb9458fe033ae3d6d227af98cb4312b7caa4afc1f4e84663e2d4" }, ] [[package]] name = "psycopg-c" -version = "3.2.12" -source = { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-bookworm-3.2.12/psycopg_c-3.2.12-cp312-cp312-linux_x86_64.whl" } +version = "3.3.0" +source = { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-trixie-3.3.0/psycopg_c-3.3.0-cp312-cp312-linux_x86_64.whl" } resolution-markers = [ "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", ] wheels = [ - { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-bookworm-3.2.12/psycopg_c-3.2.12-cp312-cp312-linux_x86_64.whl", hash = "sha256:64ca43e3521a761c2ae5ea883c71bae619abe0e6392d388424776c93e55de8a8" }, + { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-trixie-3.3.0/psycopg_c-3.3.0-cp312-cp312-linux_x86_64.whl", hash = "sha256:9fe4cd5e57c6aea8de5298d47024db7485809eed64d0e92e653c732576ed451f" }, ] [[package]] name = "psycopg-pool" -version = "3.2.7" +version = "3.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/8f/3ec52b17087c2ed5fa32b64fd4814dde964c9aa4bd49d0d30fc24725ca6d/psycopg_pool-3.2.7.tar.gz", hash = "sha256:a77d531bfca238e49e5fb5832d65b98e69f2c62bfda3d2d4d833696bdc9ca54b", size = 29765, upload-time = "2025-10-26T00:46:10.379Z" } +sdist = { url = "https://files.pythonhosted.org/packages/56/9a/9470d013d0d50af0da9c4251614aeb3c1823635cab3edc211e3839db0bcf/psycopg_pool-3.3.0.tar.gz", hash = "sha256:fa115eb2860bd88fce1717d75611f41490dec6135efb619611142b24da3f6db5", size = 31606, upload-time = "2025-12-01T11:34:33.11Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/59/74e752f605c6f0e351d4cf1c54fb9a1616dc800db4572b95bbfbb1a6225f/psycopg_pool-3.2.7-py3-none-any.whl", hash = "sha256:4b47bb59d887ef5da522eb63746b9f70e2faf967d34aac4f56ffc65e9606728f", size = 38232, upload-time = "2025-10-26T00:46:00.496Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c3/26b8a0908a9db249de3b4169692e1c7c19048a9bc41a4d3209cee7dbb758/psycopg_pool-3.3.0-py3-none-any.whl", hash = "sha256:2e44329155c410b5e8666372db44276a8b1ebd8c90f1c3026ebba40d4bc81063", size = 39995, upload-time = "2025-12-01T11:34:29.761Z" }, ] [[package]] -name = "pyasn1" -version = "0.6.1" +name = "py-ubjson" +version = "0.16.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/c7/28220d37e041fe1df03e857fe48f768dcd30cd151480bf6f00da8713214a/py-ubjson-0.16.1.tar.gz", hash = "sha256:b9bfb8695a1c7e3632e800fb83c943bf67ed45ddd87cd0344851610c69a5a482", size = 50316, upload-time = "2020-04-18T15:05:57.698Z" } + +[[package]] +name = "pyasn1" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/6e630dff89739fcd427e3f72b3d905ce0acb85a45d4ec3e2678718a3487f/pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b", size = 146586, upload-time = "2026-01-16T18:04:18.534Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" }, ] [[package]] @@ -2805,11 +3923,125 @@ wheels = [ [[package]] name = "pycparser" -version = "2.23" +version = "3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pydantic-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-inspection", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, ] [[package]] @@ -2837,33 +4069,33 @@ crypto = [ [[package]] name = "pymdown-extensions" -version = "10.16.1" +version = "10.20.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/b3/6d2b3f149bc5413b0a29761c2c5832d8ce904a1d7f621e86616d96f505cc/pymdown_extensions-10.16.1.tar.gz", hash = "sha256:aace82bcccba3efc03e25d584e6a22d27a8e17caa3f4dd9f207e49b787aa9a91", size = 853277, upload-time = "2025-07-28T16:19:34.167Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/6c/9e370934bfa30e889d12e61d0dae009991294f40055c238980066a7fbd83/pymdown_extensions-10.20.1.tar.gz", hash = "sha256:e7e39c865727338d434b55f1dd8da51febcffcaebd6e1a0b9c836243f660740a", size = 852860, upload-time = "2026-01-24T05:56:56.758Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/06/43084e6cbd4b3bc0e80f6be743b2e79fbc6eed8de9ad8c629939fa55d972/pymdown_extensions-10.16.1-py3-none-any.whl", hash = "sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d", size = 266178, upload-time = "2025-07-28T16:19:31.401Z" }, + { url = "https://files.pythonhosted.org/packages/40/6d/b6ee155462a0156b94312bdd82d2b92ea56e909740045a87ccb98bf52405/pymdown_extensions-10.20.1-py3-none-any.whl", hash = "sha256:24af7feacbca56504b313b7b418c4f5e1317bb5fea60f03d57be7fcc40912aa0", size = 268768, upload-time = "2026-01-24T05:56:54.537Z" }, ] [[package]] name = "pyopenssl" -version = "25.1.0" +version = "25.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform == 'linux')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/8c/cd89ad05804f8e3c17dea8f178c3f40eeab5694c30e0c9f5bcd49f576fc3/pyopenssl-25.1.0.tar.gz", hash = "sha256:8d031884482e0c67ee92bf9a4d8cceb08d92aba7136432ffb0703c5280fc205b", size = 179937, upload-time = "2025-05-17T16:28:31.31Z" } +sdist = { url = "https://files.pythonhosted.org/packages/80/be/97b83a464498a79103036bc74d1038df4a7ef0e402cfaf4d5e113fb14759/pyopenssl-25.3.0.tar.gz", hash = "sha256:c981cb0a3fd84e8602d7afc209522773b94c1c2446a3c710a75b06fe1beae329", size = 184073, upload-time = "2025-09-17T00:32:21.037Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/80/28/2659c02301b9500751f8d42f9a6632e1508aa5120de5e43042b8b30f8d5d/pyopenssl-25.1.0-py3-none-any.whl", hash = "sha256:2b11f239acc47ac2e5aca04fd7fa829800aeee22a2eb30d744572a157bd8a1ab", size = 56771, upload-time = "2025-05-17T16:28:29.197Z" }, + { url = "https://files.pythonhosted.org/packages/d1/81/ef2b1dfd1862567d573a4fdbc9f969067621764fbb74338496840a1d2977/pyopenssl-25.3.0-py3-none-any.whl", hash = "sha256:1fda6fc034d5e3d179d39e59c1895c9faeaf40a79de5fc4cbbfbe0d36f4a77b6", size = 57268, upload-time = "2025-09-17T00:32:19.474Z" }, ] [[package]] name = "pytest" -version = "8.4.2" +version = "9.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, @@ -2873,9 +4105,9 @@ dependencies = [ { name = "pygments", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "tomli", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] [[package]] @@ -2919,15 +4151,15 @@ wheels = [ [[package]] name = "pytest-httpx" -version = "0.35.0" +version = "0.36.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/89/5b12b7b29e3d0af3a4b9c071ee92fa25a9017453731a38f08ba01c280f4c/pytest_httpx-0.35.0.tar.gz", hash = "sha256:d619ad5d2e67734abfbb224c3d9025d64795d4b8711116b1a13f72a251ae511f", size = 54146, upload-time = "2024-11-28T19:16:54.237Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/5574834da9499066fa1a5ea9c336f94dba2eae02298d36dab192fcf95c86/pytest_httpx-0.36.0.tar.gz", hash = "sha256:9edb66a5fd4388ce3c343189bc67e7e1cb50b07c2e3fc83b97d511975e8a831b", size = 56793, upload-time = "2025-12-02T16:34:57.414Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/ed/026d467c1853dd83102411a78126b4842618e86c895f93528b0528c7a620/pytest_httpx-0.35.0-py3-none-any.whl", hash = "sha256:ee11a00ffcea94a5cbff47af2114d34c5b231c326902458deed73f9c459fd744", size = 19442, upload-time = "2024-11-28T19:16:52.787Z" }, + { url = "https://files.pythonhosted.org/packages/e2/d2/1eb1ea9c84f0d2033eb0b49675afdc71aa4ea801b74615f00f3c33b725e3/pytest_httpx-0.36.0-py3-none-any.whl", hash = "sha256:bd4c120bb80e142df856e825ec9f17981effb84d159f9fa29ed97e2357c3a9c8", size = 20229, upload-time = "2025-12-02T16:34:56.45Z" }, ] [[package]] @@ -3004,11 +4236,11 @@ wheels = [ [[package]] name = "python-gnupg" -version = "0.5.5" +version = "0.5.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/42/d0/72a14a79f26c6119b281f6ccc475a787432ef155560278e60df97ce68a86/python-gnupg-0.5.5.tar.gz", hash = "sha256:3fdcaf76f60a1b948ff8e37dc398d03cf9ce7427065d583082b92da7a4ff5a63", size = 66467, upload-time = "2025-08-04T19:26:55.778Z" } +sdist = { url = "https://files.pythonhosted.org/packages/98/2c/6cd2c7cff4bdbb434be5429ef6b8e96ee6b50155551361f30a1bb2ea3c1d/python_gnupg-0.5.6.tar.gz", hash = "sha256:5743e96212d38923fc19083812dc127907e44dbd3bcf0db4d657e291d3c21eac", size = 66825, upload-time = "2025-12-31T17:19:33.19Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/19/c147f78cc18c8788f54d4a16a22f6c05deba85ead5672d3ddf6dcba5a5fe/python_gnupg-0.5.5-py2.py3-none-any.whl", hash = "sha256:51fa7b8831ff0914bc73d74c59b99c613de7247b91294323c39733bb85ac3fc1", size = 21916, upload-time = "2025-08-04T19:26:54.307Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ab/0ea9de971caf3cd2e268d2b05dfe9883b21cfe686a59249bd2dccb4bae33/python_gnupg-0.5.6-py2.py3-none-any.whl", hash = "sha256:b5050a55663d8ab9fcc8d97556d229af337a87a3ebebd7054cbd8b7e2043394a", size = 22082, upload-time = "2025-12-31T17:16:22.743Z" }, ] [[package]] @@ -3085,13 +4317,15 @@ name = "pywavelets" version = "1.9.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.11' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'linux'", ] dependencies = [ - { name = "numpy", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux')" }, + { name = "numpy", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5a/75/50581633d199812205ea8cdd0f6d52f12a624886b74bf1486335b67f01ff/pywavelets-1.9.0.tar.gz", hash = "sha256:148d12203377772bea452a59211d98649c8ee4a05eff019a9021853a36babdc8", size = 3938340, upload-time = "2025-08-04T16:20:04.978Z" } wheels = [ @@ -3299,102 +4533,116 @@ hiredis = [ [[package]] name = "referencing" -version = "0.36.2" +version = "0.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "rpds-py", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform == 'linux')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, ] [[package]] name = "regex" -version = "2025.9.18" +version = "2026.1.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/49/d3/eaa0d28aba6ad1827ad1e716d9a93e1ba963ada61887498297d3da715133/regex-2025.9.18.tar.gz", hash = "sha256:c5ba23274c61c6fef447ba6a39333297d0c247f53059dba0bca415cac511edc4", size = 400917, upload-time = "2025-09-19T00:38:35.79Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/86/07d5056945f9ec4590b518171c4254a5925832eb727b56d3c38a7476f316/regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5", size = 414811, upload-time = "2026-01-14T23:18:02.775Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d8/7e06171db8e55f917c5b8e89319cea2d86982e3fc46b677f40358223dece/regex-2025.9.18-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:12296202480c201c98a84aecc4d210592b2f55e200a1d193235c4db92b9f6788", size = 484829, upload-time = "2025-09-19T00:35:05.215Z" }, - { url = "https://files.pythonhosted.org/packages/8d/70/bf91bb39e5bedf75ce730ffbaa82ca585584d13335306d637458946b8b9f/regex-2025.9.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:220381f1464a581f2ea988f2220cf2a67927adcef107d47d6897ba5a2f6d51a4", size = 288993, upload-time = "2025-09-19T00:35:08.154Z" }, - { url = "https://files.pythonhosted.org/packages/fe/89/69f79b28365eda2c46e64c39d617d5f65a2aa451a4c94de7d9b34c2dc80f/regex-2025.9.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:87f681bfca84ebd265278b5daa1dcb57f4db315da3b5d044add7c30c10442e61", size = 286624, upload-time = "2025-09-19T00:35:09.717Z" }, - { url = "https://files.pythonhosted.org/packages/44/31/81e62955726c3a14fcc1049a80bc716765af6c055706869de5e880ddc783/regex-2025.9.18-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34d674cbba70c9398074c8a1fcc1a79739d65d1105de2a3c695e2b05ea728251", size = 780473, upload-time = "2025-09-19T00:35:11.013Z" }, - { url = "https://files.pythonhosted.org/packages/fb/23/07072b7e191fbb6e213dc03b2f5b96f06d3c12d7deaded84679482926fc7/regex-2025.9.18-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:385c9b769655cb65ea40b6eea6ff763cbb6d69b3ffef0b0db8208e1833d4e746", size = 849290, upload-time = "2025-09-19T00:35:12.348Z" }, - { url = "https://files.pythonhosted.org/packages/b3/f0/aec7f6a01f2a112210424d77c6401b9015675fb887ced7e18926df4ae51e/regex-2025.9.18-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8900b3208e022570ae34328712bef6696de0804c122933414014bae791437ab2", size = 897335, upload-time = "2025-09-19T00:35:14.058Z" }, - { url = "https://files.pythonhosted.org/packages/cc/90/2e5f9da89d260de7d0417ead91a1bc897f19f0af05f4f9323313b76c47f2/regex-2025.9.18-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c204e93bf32cd7a77151d44b05eb36f469d0898e3fba141c026a26b79d9914a0", size = 789946, upload-time = "2025-09-19T00:35:15.403Z" }, - { url = "https://files.pythonhosted.org/packages/2b/d5/1c712c7362f2563d389be66bae131c8bab121a3fabfa04b0b5bfc9e73c51/regex-2025.9.18-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3acc471d1dd7e5ff82e6cacb3b286750decd949ecd4ae258696d04f019817ef8", size = 780787, upload-time = "2025-09-19T00:35:17.061Z" }, - { url = "https://files.pythonhosted.org/packages/4f/92/c54cdb4aa41009632e69817a5aa452673507f07e341076735a2f6c46a37c/regex-2025.9.18-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6479d5555122433728760e5f29edb4c2b79655a8deb681a141beb5c8a025baea", size = 773632, upload-time = "2025-09-19T00:35:18.57Z" }, - { url = "https://files.pythonhosted.org/packages/db/99/75c996dc6a2231a8652d7ad0bfbeaf8a8c77612d335580f520f3ec40e30b/regex-2025.9.18-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:431bd2a8726b000eb6f12429c9b438a24062a535d06783a93d2bcbad3698f8a8", size = 844104, upload-time = "2025-09-19T00:35:20.259Z" }, - { url = "https://files.pythonhosted.org/packages/1c/f7/25aba34cc130cb6844047dbfe9716c9b8f9629fee8b8bec331aa9241b97b/regex-2025.9.18-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0cc3521060162d02bd36927e20690129200e5ac9d2c6d32b70368870b122db25", size = 834794, upload-time = "2025-09-19T00:35:22.002Z" }, - { url = "https://files.pythonhosted.org/packages/51/eb/64e671beafa0ae29712268421597596d781704973551312b2425831d4037/regex-2025.9.18-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a021217b01be2d51632ce056d7a837d3fa37c543ede36e39d14063176a26ae29", size = 778535, upload-time = "2025-09-19T00:35:23.298Z" }, - { url = "https://files.pythonhosted.org/packages/58/61/80eda662fc4eb32bfedc331f42390974c9e89c7eac1b79cd9eea4d7c458c/regex-2025.9.18-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:51076980cd08cd13c88eb7365427ae27f0d94e7cebe9ceb2bb9ffdae8fc4d82a", size = 484832, upload-time = "2025-09-19T00:35:30.011Z" }, - { url = "https://files.pythonhosted.org/packages/a6/d9/33833d9abddf3f07ad48504ddb53fe3b22f353214bbb878a72eee1e3ddbf/regex-2025.9.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:828446870bd7dee4e0cbeed767f07961aa07f0ea3129f38b3ccecebc9742e0b8", size = 288994, upload-time = "2025-09-19T00:35:31.733Z" }, - { url = "https://files.pythonhosted.org/packages/2a/b3/526ee96b0d70ea81980cbc20c3496fa582f775a52e001e2743cc33b2fa75/regex-2025.9.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c28821d5637866479ec4cc23b8c990f5bc6dd24e5e4384ba4a11d38a526e1414", size = 286619, upload-time = "2025-09-19T00:35:33.221Z" }, - { url = "https://files.pythonhosted.org/packages/65/4f/c2c096b02a351b33442aed5895cdd8bf87d372498d2100927c5a053d7ba3/regex-2025.9.18-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:726177ade8e481db669e76bf99de0b278783be8acd11cef71165327abd1f170a", size = 792454, upload-time = "2025-09-19T00:35:35.361Z" }, - { url = "https://files.pythonhosted.org/packages/24/15/b562c9d6e47c403c4b5deb744f8b4bf6e40684cf866c7b077960a925bdff/regex-2025.9.18-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5cca697da89b9f8ea44115ce3130f6c54c22f541943ac8e9900461edc2b8bd4", size = 858723, upload-time = "2025-09-19T00:35:36.949Z" }, - { url = "https://files.pythonhosted.org/packages/f2/01/dba305409849e85b8a1a681eac4c03ed327d8de37895ddf9dc137f59c140/regex-2025.9.18-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dfbde38f38004703c35666a1e1c088b778e35d55348da2b7b278914491698d6a", size = 905899, upload-time = "2025-09-19T00:35:38.723Z" }, - { url = "https://files.pythonhosted.org/packages/fe/d0/c51d1e6a80eab11ef96a4cbad17fc0310cf68994fb01a7283276b7e5bbd6/regex-2025.9.18-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f2f422214a03fab16bfa495cfec72bee4aaa5731843b771860a471282f1bf74f", size = 798981, upload-time = "2025-09-19T00:35:40.416Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5e/72db90970887bbe02296612bd61b0fa31e6d88aa24f6a4853db3e96c575e/regex-2025.9.18-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a295916890f4df0902e4286bc7223ee7f9e925daa6dcdec4192364255b70561a", size = 781900, upload-time = "2025-09-19T00:35:42.077Z" }, - { url = "https://files.pythonhosted.org/packages/50/ff/596be45eea8e9bc31677fde243fa2904d00aad1b32c31bce26c3dbba0b9e/regex-2025.9.18-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:5db95ff632dbabc8c38c4e82bf545ab78d902e81160e6e455598014f0abe66b9", size = 852952, upload-time = "2025-09-19T00:35:43.751Z" }, - { url = "https://files.pythonhosted.org/packages/e5/1b/2dfa348fa551e900ed3f5f63f74185b6a08e8a76bc62bc9c106f4f92668b/regex-2025.9.18-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fb967eb441b0f15ae610b7069bdb760b929f267efbf522e814bbbfffdf125ce2", size = 844355, upload-time = "2025-09-19T00:35:45.309Z" }, - { url = "https://files.pythonhosted.org/packages/f4/bf/aefb1def27fe33b8cbbb19c75c13aefccfbef1c6686f8e7f7095705969c7/regex-2025.9.18-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f04d2f20da4053d96c08f7fde6e1419b7ec9dbcee89c96e3d731fca77f411b95", size = 787254, upload-time = "2025-09-19T00:35:46.904Z" }, - { url = "https://files.pythonhosted.org/packages/b0/99/05859d87a66ae7098222d65748f11ef7f2dff51bfd7482a4e2256c90d72b/regex-2025.9.18-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:436e1b31d7efd4dcd52091d076482031c611dde58bf9c46ca6d0a26e33053a7e", size = 486335, upload-time = "2025-09-19T00:36:03.661Z" }, - { url = "https://files.pythonhosted.org/packages/97/7e/d43d4e8b978890932cf7b0957fce58c5b08c66f32698f695b0c2c24a48bf/regex-2025.9.18-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c190af81e5576b9c5fdc708f781a52ff20f8b96386c6e2e0557a78402b029f4a", size = 289720, upload-time = "2025-09-19T00:36:05.471Z" }, - { url = "https://files.pythonhosted.org/packages/bb/3b/ff80886089eb5dcf7e0d2040d9aaed539e25a94300403814bb24cc775058/regex-2025.9.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e4121f1ce2b2b5eec4b397cc1b277686e577e658d8f5870b7eb2d726bd2300ab", size = 287257, upload-time = "2025-09-19T00:36:07.072Z" }, - { url = "https://files.pythonhosted.org/packages/ee/66/243edf49dd8720cba8d5245dd4d6adcb03a1defab7238598c0c97cf549b8/regex-2025.9.18-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:300e25dbbf8299d87205e821a201057f2ef9aa3deb29caa01cd2cac669e508d5", size = 797463, upload-time = "2025-09-19T00:36:08.399Z" }, - { url = "https://files.pythonhosted.org/packages/df/71/c9d25a1142c70432e68bb03211d4a82299cd1c1fbc41db9409a394374ef5/regex-2025.9.18-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b47fcf9f5316c0bdaf449e879407e1b9937a23c3b369135ca94ebc8d74b1742", size = 862670, upload-time = "2025-09-19T00:36:10.101Z" }, - { url = "https://files.pythonhosted.org/packages/f8/8f/329b1efc3a64375a294e3a92d43372bf1a351aa418e83c21f2f01cf6ec41/regex-2025.9.18-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:57a161bd3acaa4b513220b49949b07e252165e6b6dc910ee7617a37ff4f5b425", size = 910881, upload-time = "2025-09-19T00:36:12.223Z" }, - { url = "https://files.pythonhosted.org/packages/35/9e/a91b50332a9750519320ed30ec378b74c996f6befe282cfa6bb6cea7e9fd/regex-2025.9.18-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f130c3a7845ba42de42f380fff3c8aebe89a810747d91bcf56d40a069f15352", size = 802011, upload-time = "2025-09-19T00:36:13.901Z" }, - { url = "https://files.pythonhosted.org/packages/a4/1d/6be3b8d7856b6e0d7ee7f942f437d0a76e0d5622983abbb6d21e21ab9a17/regex-2025.9.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5f96fa342b6f54dcba928dd452e8d8cb9f0d63e711d1721cd765bb9f73bb048d", size = 786668, upload-time = "2025-09-19T00:36:15.391Z" }, - { url = "https://files.pythonhosted.org/packages/cb/ce/4a60e53df58bd157c5156a1736d3636f9910bdcc271d067b32b7fcd0c3a8/regex-2025.9.18-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0f0d676522d68c207828dcd01fb6f214f63f238c283d9f01d85fc664c7c85b56", size = 856578, upload-time = "2025-09-19T00:36:16.845Z" }, - { url = "https://files.pythonhosted.org/packages/86/e8/162c91bfe7217253afccde112868afb239f94703de6580fb235058d506a6/regex-2025.9.18-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:40532bff8a1a0621e7903ae57fce88feb2e8a9a9116d341701302c9302aef06e", size = 849017, upload-time = "2025-09-19T00:36:18.597Z" }, - { url = "https://files.pythonhosted.org/packages/35/34/42b165bc45289646ea0959a1bc7531733e90b47c56a72067adfe6b3251f6/regex-2025.9.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:039f11b618ce8d71a1c364fdee37da1012f5a3e79b1b2819a9f389cd82fd6282", size = 788150, upload-time = "2025-09-19T00:36:20.464Z" }, - { url = "https://files.pythonhosted.org/packages/d2/c7/5c48206a60ce33711cf7dcaeaed10dd737733a3569dc7e1dce324dd48f30/regex-2025.9.18-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2a40f929cd907c7e8ac7566ac76225a77701a6221bca937bdb70d56cb61f57b2", size = 485955, upload-time = "2025-09-19T00:36:26.822Z" }, - { url = "https://files.pythonhosted.org/packages/e9/be/74fc6bb19a3c491ec1ace943e622b5a8539068771e8705e469b2da2306a7/regex-2025.9.18-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c90471671c2cdf914e58b6af62420ea9ecd06d1554d7474d50133ff26ae88feb", size = 289583, upload-time = "2025-09-19T00:36:28.577Z" }, - { url = "https://files.pythonhosted.org/packages/25/c4/9ceaa433cb5dc515765560f22a19578b95b92ff12526e5a259321c4fc1a0/regex-2025.9.18-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a351aff9e07a2dabb5022ead6380cff17a4f10e4feb15f9100ee56c4d6d06af", size = 287000, upload-time = "2025-09-19T00:36:30.161Z" }, - { url = "https://files.pythonhosted.org/packages/7d/e6/68bc9393cb4dc68018456568c048ac035854b042bc7c33cb9b99b0680afa/regex-2025.9.18-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc4b8e9d16e20ddfe16430c23468a8707ccad3365b06d4536142e71823f3ca29", size = 797535, upload-time = "2025-09-19T00:36:31.876Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1c/ebae9032d34b78ecfe9bd4b5e6575b55351dc8513485bb92326613732b8c/regex-2025.9.18-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4b8cdbddf2db1c5e80338ba2daa3cfa3dec73a46fff2a7dda087c8efbf12d62f", size = 862603, upload-time = "2025-09-19T00:36:33.344Z" }, - { url = "https://files.pythonhosted.org/packages/3b/74/12332c54b3882557a4bcd2b99f8be581f5c6a43cf1660a85b460dd8ff468/regex-2025.9.18-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a276937d9d75085b2c91fb48244349c6954f05ee97bba0963ce24a9d915b8b68", size = 910829, upload-time = "2025-09-19T00:36:34.826Z" }, - { url = "https://files.pythonhosted.org/packages/86/70/ba42d5ed606ee275f2465bfc0e2208755b06cdabd0f4c7c4b614d51b57ab/regex-2025.9.18-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92a8e375ccdc1256401c90e9dc02b8642894443d549ff5e25e36d7cf8a80c783", size = 802059, upload-time = "2025-09-19T00:36:36.664Z" }, - { url = "https://files.pythonhosted.org/packages/da/c5/fcb017e56396a7f2f8357412638d7e2963440b131a3ca549be25774b3641/regex-2025.9.18-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0dc6893b1f502d73037cf807a321cdc9be29ef3d6219f7970f842475873712ac", size = 786781, upload-time = "2025-09-19T00:36:38.168Z" }, - { url = "https://files.pythonhosted.org/packages/c6/ee/21c4278b973f630adfb3bcb23d09d83625f3ab1ca6e40ebdffe69901c7a1/regex-2025.9.18-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a61e85bfc63d232ac14b015af1261f826260c8deb19401c0597dbb87a864361e", size = 856578, upload-time = "2025-09-19T00:36:40.129Z" }, - { url = "https://files.pythonhosted.org/packages/87/0b/de51550dc7274324435c8f1539373ac63019b0525ad720132866fff4a16a/regex-2025.9.18-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1ef86a9ebc53f379d921fb9a7e42b92059ad3ee800fcd9e0fe6181090e9f6c23", size = 849119, upload-time = "2025-09-19T00:36:41.651Z" }, - { url = "https://files.pythonhosted.org/packages/60/52/383d3044fc5154d9ffe4321696ee5b2ee4833a28c29b137c22c33f41885b/regex-2025.9.18-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d3bc882119764ba3a119fbf2bd4f1b47bc56c1da5d42df4ed54ae1e8e66fdf8f", size = 788219, upload-time = "2025-09-19T00:36:43.575Z" }, - { url = "https://files.pythonhosted.org/packages/e8/83/6887e16a187c6226cb85d8301e47d3b73ecc4505a3a13d8da2096b44fd76/regex-2025.9.18-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:fe5d50572bc885a0a799410a717c42b1a6b50e2f45872e2b40f4f288f9bce8a2", size = 489765, upload-time = "2025-09-19T00:36:49.996Z" }, - { url = "https://files.pythonhosted.org/packages/51/c5/e2f7325301ea2916ff301c8d963ba66b1b2c1b06694191df80a9c4fea5d0/regex-2025.9.18-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b9d9a2d6cda6621551ca8cf7a06f103adf72831153f3c0d982386110870c4d3", size = 291228, upload-time = "2025-09-19T00:36:51.654Z" }, - { url = "https://files.pythonhosted.org/packages/91/60/7d229d2bc6961289e864a3a3cfebf7d0d250e2e65323a8952cbb7e22d824/regex-2025.9.18-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:13202e4c4ac0ef9a317fff817674b293c8f7e8c68d3190377d8d8b749f566e12", size = 289270, upload-time = "2025-09-19T00:36:53.118Z" }, - { url = "https://files.pythonhosted.org/packages/3c/d7/b4f06868ee2958ff6430df89857fbf3d43014bbf35538b6ec96c2704e15d/regex-2025.9.18-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:874ff523b0fecffb090f80ae53dc93538f8db954c8bb5505f05b7787ab3402a0", size = 806326, upload-time = "2025-09-19T00:36:54.631Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e4/bca99034a8f1b9b62ccf337402a8e5b959dd5ba0e5e5b2ead70273df3277/regex-2025.9.18-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d13ab0490128f2bb45d596f754148cd750411afc97e813e4b3a61cf278a23bb6", size = 871556, upload-time = "2025-09-19T00:36:56.208Z" }, - { url = "https://files.pythonhosted.org/packages/6d/df/e06ffaf078a162f6dd6b101a5ea9b44696dca860a48136b3ae4a9caf25e2/regex-2025.9.18-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:05440bc172bc4b4b37fb9667e796597419404dbba62e171e1f826d7d2a9ebcef", size = 913817, upload-time = "2025-09-19T00:36:57.807Z" }, - { url = "https://files.pythonhosted.org/packages/9e/05/25b05480b63292fd8e84800b1648e160ca778127b8d2367a0a258fa2e225/regex-2025.9.18-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5514b8e4031fdfaa3d27e92c75719cbe7f379e28cacd939807289bce76d0e35a", size = 811055, upload-time = "2025-09-19T00:36:59.762Z" }, - { url = "https://files.pythonhosted.org/packages/70/97/7bc7574655eb651ba3a916ed4b1be6798ae97af30104f655d8efd0cab24b/regex-2025.9.18-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:65d3c38c39efce73e0d9dc019697b39903ba25b1ad45ebbd730d2cf32741f40d", size = 794534, upload-time = "2025-09-19T00:37:01.405Z" }, - { url = "https://files.pythonhosted.org/packages/b4/c2/d5da49166a52dda879855ecdba0117f073583db2b39bb47ce9a3378a8e9e/regex-2025.9.18-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ae77e447ebc144d5a26d50055c6ddba1d6ad4a865a560ec7200b8b06bc529368", size = 866684, upload-time = "2025-09-19T00:37:03.441Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2d/0a5c4e6ec417de56b89ff4418ecc72f7e3feca806824c75ad0bbdae0516b/regex-2025.9.18-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e3ef8cf53dc8df49d7e28a356cf824e3623764e9833348b655cfed4524ab8a90", size = 853282, upload-time = "2025-09-19T00:37:04.985Z" }, - { url = "https://files.pythonhosted.org/packages/f4/8e/d656af63e31a86572ec829665d6fa06eae7e144771e0330650a8bb865635/regex-2025.9.18-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9feb29817df349c976da9a0debf775c5c33fc1c8ad7b9f025825da99374770b7", size = 797830, upload-time = "2025-09-19T00:37:06.697Z" }, - { url = "https://files.pythonhosted.org/packages/44/b7/3b4663aa3b4af16819f2ab6a78c4111c7e9b066725d8107753c2257448a5/regex-2025.9.18-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c6db75b51acf277997f3adcd0ad89045d856190d13359f15ab5dda21581d9129", size = 486130, upload-time = "2025-09-19T00:37:14.527Z" }, - { url = "https://files.pythonhosted.org/packages/80/5b/4533f5d7ac9c6a02a4725fe8883de2aebc713e67e842c04cf02626afb747/regex-2025.9.18-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8f9698b6f6895d6db810e0bda5364f9ceb9e5b11328700a90cae573574f61eea", size = 289539, upload-time = "2025-09-19T00:37:16.356Z" }, - { url = "https://files.pythonhosted.org/packages/b8/8d/5ab6797c2750985f79e9995fad3254caa4520846580f266ae3b56d1cae58/regex-2025.9.18-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29cd86aa7cb13a37d0f0d7c21d8d949fe402ffa0ea697e635afedd97ab4b69f1", size = 287233, upload-time = "2025-09-19T00:37:18.025Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/95afcb02ba8d3a64e6ffeb801718ce73471ad6440c55d993f65a4a5e7a92/regex-2025.9.18-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c9f285a071ee55cd9583ba24dde006e53e17780bb309baa8e4289cd472bcc47", size = 797876, upload-time = "2025-09-19T00:37:19.609Z" }, - { url = "https://files.pythonhosted.org/packages/c8/fb/720b1f49cec1f3b5a9fea5b34cd22b88b5ebccc8c1b5de9cc6f65eed165a/regex-2025.9.18-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5adf266f730431e3be9021d3e5b8d5ee65e563fec2883ea8093944d21863b379", size = 863385, upload-time = "2025-09-19T00:37:21.65Z" }, - { url = "https://files.pythonhosted.org/packages/a9/ca/e0d07ecf701e1616f015a720dc13b84c582024cbfbb3fc5394ae204adbd7/regex-2025.9.18-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1137cabc0f38807de79e28d3f6e3e3f2cc8cfb26bead754d02e6d1de5f679203", size = 910220, upload-time = "2025-09-19T00:37:23.723Z" }, - { url = "https://files.pythonhosted.org/packages/b6/45/bba86413b910b708eca705a5af62163d5d396d5f647ed9485580c7025209/regex-2025.9.18-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cc9e5525cada99699ca9223cce2d52e88c52a3d2a0e842bd53de5497c604164", size = 801827, upload-time = "2025-09-19T00:37:25.684Z" }, - { url = "https://files.pythonhosted.org/packages/b8/a6/740fbd9fcac31a1305a8eed30b44bf0f7f1e042342be0a4722c0365ecfca/regex-2025.9.18-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bbb9246568f72dce29bcd433517c2be22c7791784b223a810225af3b50d1aafb", size = 786843, upload-time = "2025-09-19T00:37:27.62Z" }, - { url = "https://files.pythonhosted.org/packages/80/a7/0579e8560682645906da640c9055506465d809cb0f5415d9976f417209a6/regex-2025.9.18-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6a52219a93dd3d92c675383efff6ae18c982e2d7651c792b1e6d121055808743", size = 857430, upload-time = "2025-09-19T00:37:29.362Z" }, - { url = "https://files.pythonhosted.org/packages/8d/9b/4dc96b6c17b38900cc9fee254fc9271d0dde044e82c78c0811b58754fde5/regex-2025.9.18-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:ae9b3840c5bd456780e3ddf2f737ab55a79b790f6409182012718a35c6d43282", size = 848612, upload-time = "2025-09-19T00:37:31.42Z" }, - { url = "https://files.pythonhosted.org/packages/b3/6a/6f659f99bebb1775e5ac81a3fb837b85897c1a4ef5acffd0ff8ffe7e67fb/regex-2025.9.18-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d488c236ac497c46a5ac2005a952c1a0e22a07be9f10c3e735bc7d1209a34773", size = 787967, upload-time = "2025-09-19T00:37:34.019Z" }, - { url = "https://files.pythonhosted.org/packages/99/cb/8a1ab05ecf404e18b54348e293d9b7a60ec2bd7aa59e637020c5eea852e8/regex-2025.9.18-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:b176326bcd544b5e9b17d6943f807697c0cb7351f6cfb45bf5637c95ff7e6306", size = 489773, upload-time = "2025-09-19T00:37:40.968Z" }, - { url = "https://files.pythonhosted.org/packages/93/3b/6543c9b7f7e734d2404fa2863d0d710c907bef99d4598760ed4563d634c3/regex-2025.9.18-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:0ffd9e230b826b15b369391bec167baed57c7ce39efc35835448618860995946", size = 291221, upload-time = "2025-09-19T00:37:42.901Z" }, - { url = "https://files.pythonhosted.org/packages/cd/91/e9fdee6ad6bf708d98c5d17fded423dcb0661795a49cba1b4ffb8358377a/regex-2025.9.18-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec46332c41add73f2b57e2f5b642f991f6b15e50e9f86285e08ffe3a512ac39f", size = 289268, upload-time = "2025-09-19T00:37:44.823Z" }, - { url = "https://files.pythonhosted.org/packages/94/a6/bc3e8a918abe4741dadeaeb6c508e3a4ea847ff36030d820d89858f96a6c/regex-2025.9.18-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b80fa342ed1ea095168a3f116637bd1030d39c9ff38dc04e54ef7c521e01fc95", size = 806659, upload-time = "2025-09-19T00:37:46.684Z" }, - { url = "https://files.pythonhosted.org/packages/2b/71/ea62dbeb55d9e6905c7b5a49f75615ea1373afcad95830047e4e310db979/regex-2025.9.18-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4d97071c0ba40f0cf2a93ed76e660654c399a0a04ab7d85472239460f3da84b", size = 871701, upload-time = "2025-09-19T00:37:48.882Z" }, - { url = "https://files.pythonhosted.org/packages/6a/90/fbe9dedb7dad24a3a4399c0bae64bfa932ec8922a0a9acf7bc88db30b161/regex-2025.9.18-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0ac936537ad87cef9e0e66c5144484206c1354224ee811ab1519a32373e411f3", size = 913742, upload-time = "2025-09-19T00:37:51.015Z" }, - { url = "https://files.pythonhosted.org/packages/f0/1c/47e4a8c0e73d41eb9eb9fdeba3b1b810110a5139a2526e82fd29c2d9f867/regex-2025.9.18-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dec57f96d4def58c422d212d414efe28218d58537b5445cf0c33afb1b4768571", size = 811117, upload-time = "2025-09-19T00:37:52.686Z" }, - { url = "https://files.pythonhosted.org/packages/2a/da/435f29fddfd015111523671e36d30af3342e8136a889159b05c1d9110480/regex-2025.9.18-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:48317233294648bf7cd068857f248e3a57222259a5304d32c7552e2284a1b2ad", size = 794647, upload-time = "2025-09-19T00:37:54.626Z" }, - { url = "https://files.pythonhosted.org/packages/23/66/df5e6dcca25c8bc57ce404eebc7342310a0d218db739d7882c9a2b5974a3/regex-2025.9.18-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:274687e62ea3cf54846a9b25fc48a04459de50af30a7bd0b61a9e38015983494", size = 866747, upload-time = "2025-09-19T00:37:56.367Z" }, - { url = "https://files.pythonhosted.org/packages/82/42/94392b39b531f2e469b2daa40acf454863733b674481fda17462a5ffadac/regex-2025.9.18-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a78722c86a3e7e6aadf9579e3b0ad78d955f2d1f1a8ca4f67d7ca258e8719d4b", size = 853434, upload-time = "2025-09-19T00:37:58.39Z" }, - { url = "https://files.pythonhosted.org/packages/a8/f8/dcc64c7f7bbe58842a8f89622b50c58c3598fbbf4aad0a488d6df2c699f1/regex-2025.9.18-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:06104cd203cdef3ade989a1c45b6215bf42f8b9dd705ecc220c173233f7cba41", size = 798024, upload-time = "2025-09-19T00:38:00.397Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d2/e6ee96b7dff201a83f650241c52db8e5bd080967cb93211f57aa448dc9d6/regex-2026.1.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e3dd93c8f9abe8aa4b6c652016da9a3afa190df5ad822907efe6b206c09896e", size = 488166, upload-time = "2026-01-14T23:13:46.408Z" }, + { url = "https://files.pythonhosted.org/packages/23/8a/819e9ce14c9f87af026d0690901b3931f3101160833e5d4c8061fa3a1b67/regex-2026.1.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97499ff7862e868b1977107873dd1a06e151467129159a6ffd07b66706ba3a9f", size = 290632, upload-time = "2026-01-14T23:13:48.688Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c3/23dfe15af25d1d45b07dfd4caa6003ad710dcdcb4c4b279909bdfe7a2de8/regex-2026.1.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bda75ebcac38d884240914c6c43d8ab5fb82e74cde6da94b43b17c411aa4c2b", size = 288500, upload-time = "2026-01-14T23:13:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/c6/31/1adc33e2f717df30d2f4d973f8776d2ba6ecf939301efab29fca57505c95/regex-2026.1.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7dcc02368585334f5bc81fc73a2a6a0bbade60e7d83da21cead622faf408f32c", size = 781670, upload-time = "2026-01-14T23:13:52.453Z" }, + { url = "https://files.pythonhosted.org/packages/23/ce/21a8a22d13bc4adcb927c27b840c948f15fc973e21ed2346c1bd0eae22dc/regex-2026.1.15-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:693b465171707bbe882a7a05de5e866f33c76aa449750bee94a8d90463533cc9", size = 850820, upload-time = "2026-01-14T23:13:54.894Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/3eeacdf587a4705a44484cd0b30e9230a0e602811fb3e2cc32268c70d509/regex-2026.1.15-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b0d190e6f013ea938623a58706d1469a62103fb2a241ce2873a9906e0386582c", size = 898777, upload-time = "2026-01-14T23:13:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/79/a9/1898a077e2965c35fc22796488141a22676eed2d73701e37c73ad7c0b459/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ff818702440a5878a81886f127b80127f5d50563753a28211482867f8318106", size = 791750, upload-time = "2026-01-14T23:13:58.527Z" }, + { url = "https://files.pythonhosted.org/packages/4c/84/e31f9d149a178889b3817212827f5e0e8c827a049ff31b4b381e76b26e2d/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f052d1be37ef35a54e394de66136e30fa1191fab64f71fc06ac7bc98c9a84618", size = 782674, upload-time = "2026-01-14T23:13:59.874Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ff/adf60063db24532add6a1676943754a5654dcac8237af024ede38244fd12/regex-2026.1.15-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6bfc31a37fd1592f0c4fc4bfc674b5c42e52efe45b4b7a6a14f334cca4bcebe4", size = 767906, upload-time = "2026-01-14T23:14:01.298Z" }, + { url = "https://files.pythonhosted.org/packages/af/3e/e6a216cee1e2780fec11afe7fc47b6f3925d7264e8149c607ac389fd9b1a/regex-2026.1.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d6ce5ae80066b319ae3bc62fd55a557c9491baa5efd0d355f0de08c4ba54e79", size = 774798, upload-time = "2026-01-14T23:14:02.715Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/23a4a8378a9208514ed3efc7e7850c27fa01e00ed8557c958df0335edc4a/regex-2026.1.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1704d204bd42b6bb80167df0e4554f35c255b579ba99616def38f69e14a5ccb9", size = 845861, upload-time = "2026-01-14T23:14:04.824Z" }, + { url = "https://files.pythonhosted.org/packages/f8/57/d7605a9d53bd07421a8785d349cd29677fe660e13674fa4c6cbd624ae354/regex-2026.1.15-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3174a5ed4171570dc8318afada56373aa9289eb6dc0d96cceb48e7358b0e220", size = 755648, upload-time = "2026-01-14T23:14:06.371Z" }, + { url = "https://files.pythonhosted.org/packages/6f/76/6f2e24aa192da1e299cc1101674a60579d3912391867ce0b946ba83e2194/regex-2026.1.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:87adf5bd6d72e3e17c9cb59ac4096b1faaf84b7eb3037a5ffa61c4b4370f0f13", size = 836250, upload-time = "2026-01-14T23:14:08.343Z" }, + { url = "https://files.pythonhosted.org/packages/11/3a/1f2a1d29453299a7858eab7759045fc3d9d1b429b088dec2dc85b6fa16a2/regex-2026.1.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e85dc94595f4d766bd7d872a9de5ede1ca8d3063f3bdf1e2c725f5eb411159e3", size = 779919, upload-time = "2026-01-14T23:14:09.954Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c9/0c80c96eab96948363d270143138d671d5731c3a692b417629bf3492a9d6/regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a", size = 488168, upload-time = "2026-01-14T23:14:16.129Z" }, + { url = "https://files.pythonhosted.org/packages/17/f0/271c92f5389a552494c429e5cc38d76d1322eb142fb5db3c8ccc47751468/regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f", size = 290636, upload-time = "2026-01-14T23:14:17.715Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f9/5f1fd077d106ca5655a0f9ff8f25a1ab55b92128b5713a91ed7134ff688e/regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1", size = 288496, upload-time = "2026-01-14T23:14:19.326Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e1/8f43b03a4968c748858ec77f746c286d81f896c2e437ccf050ebc5d3128c/regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b", size = 793503, upload-time = "2026-01-14T23:14:20.922Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4e/a39a5e8edc5377a46a7c875c2f9a626ed3338cb3bb06931be461c3e1a34a/regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8", size = 860535, upload-time = "2026-01-14T23:14:22.405Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1c/9dce667a32a9477f7a2869c1c767dc00727284a9fa3ff5c09a5c6c03575e/regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413", size = 907225, upload-time = "2026-01-14T23:14:23.897Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3c/87ca0a02736d16b6262921425e84b48984e77d8e4e572c9072ce96e66c30/regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026", size = 800526, upload-time = "2026-01-14T23:14:26.039Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/647d5715aeea7c87bdcbd2f578f47b415f55c24e361e639fe8c0cc88878f/regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785", size = 773446, upload-time = "2026-01-14T23:14:28.109Z" }, + { url = "https://files.pythonhosted.org/packages/af/89/bf22cac25cb4ba0fe6bff52ebedbb65b77a179052a9d6037136ae93f42f4/regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e", size = 783051, upload-time = "2026-01-14T23:14:29.929Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f4/6ed03e71dca6348a5188363a34f5e26ffd5db1404780288ff0d79513bce4/regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763", size = 854485, upload-time = "2026-01-14T23:14:31.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/8e8560bd78caded8eb137e3e47612430a05b9a772caf60876435192d670a/regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb", size = 762195, upload-time = "2026-01-14T23:14:32.802Z" }, + { url = "https://files.pythonhosted.org/packages/38/6b/61fc710f9aa8dfcd764fe27d37edfaa023b1a23305a0d84fccd5adb346ea/regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2", size = 845986, upload-time = "2026-01-14T23:14:34.898Z" }, + { url = "https://files.pythonhosted.org/packages/fd/2e/fbee4cb93f9d686901a7ca8d94285b80405e8c34fe4107f63ffcbfb56379/regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1", size = 788992, upload-time = "2026-01-14T23:14:37.116Z" }, + { url = "https://files.pythonhosted.org/packages/92/81/10d8cf43c807d0326efe874c1b79f22bfb0fb226027b0b19ebc26d301408/regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1", size = 489398, upload-time = "2026-01-14T23:14:43.741Z" }, + { url = "https://files.pythonhosted.org/packages/90/b0/7c2a74e74ef2a7c32de724658a69a862880e3e4155cba992ba04d1c70400/regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681", size = 291339, upload-time = "2026-01-14T23:14:45.183Z" }, + { url = "https://files.pythonhosted.org/packages/19/4d/16d0773d0c818417f4cc20aa0da90064b966d22cd62a8c46765b5bd2d643/regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f", size = 289003, upload-time = "2026-01-14T23:14:47.25Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e4/1fc4599450c9f0863d9406e944592d968b8d6dfd0d552a7d569e43bceada/regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa", size = 798656, upload-time = "2026-01-14T23:14:48.77Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e6/59650d73a73fa8a60b3a590545bfcf1172b4384a7df2e7fe7b9aab4e2da9/regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804", size = 864252, upload-time = "2026-01-14T23:14:50.528Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ab/1d0f4d50a1638849a97d731364c9a80fa304fec46325e48330c170ee8e80/regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c", size = 912268, upload-time = "2026-01-14T23:14:52.952Z" }, + { url = "https://files.pythonhosted.org/packages/dd/df/0d722c030c82faa1d331d1921ee268a4e8fb55ca8b9042c9341c352f17fa/regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5", size = 803589, upload-time = "2026-01-14T23:14:55.182Z" }, + { url = "https://files.pythonhosted.org/packages/66/23/33289beba7ccb8b805c6610a8913d0131f834928afc555b241caabd422a9/regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3", size = 775700, upload-time = "2026-01-14T23:14:56.707Z" }, + { url = "https://files.pythonhosted.org/packages/e7/65/bf3a42fa6897a0d3afa81acb25c42f4b71c274f698ceabd75523259f6688/regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb", size = 787928, upload-time = "2026-01-14T23:14:58.312Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f5/13bf65864fc314f68cdd6d8ca94adcab064d4d39dbd0b10fef29a9da48fc/regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410", size = 858607, upload-time = "2026-01-14T23:15:00.657Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/040e589834d7a439ee43fb0e1e902bc81bd58a5ba81acffe586bb3321d35/regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4", size = 763729, upload-time = "2026-01-14T23:15:02.248Z" }, + { url = "https://files.pythonhosted.org/packages/9b/84/6921e8129687a427edf25a34a5594b588b6d88f491320b9de5b6339a4fcb/regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d", size = 850697, upload-time = "2026-01-14T23:15:03.878Z" }, + { url = "https://files.pythonhosted.org/packages/8a/87/3d06143d4b128f4229158f2de5de6c8f2485170c7221e61bf381313314b2/regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22", size = 789849, upload-time = "2026-01-14T23:15:06.102Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2e/6870bb16e982669b674cce3ee9ff2d1d46ab80528ee6bcc20fb2292efb60/regex-2026.1.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e69d0deeb977ffe7ed3d2e4439360089f9c3f217ada608f0f88ebd67afb6385e", size = 489164, upload-time = "2026-01-14T23:15:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/9774542e203849b0286badf67199970a44ebdb0cc5fb739f06e47ada72f8/regex-2026.1.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3601ffb5375de85a16f407854d11cca8fe3f5febbe3ac78fb2866bb220c74d10", size = 291218, upload-time = "2026-01-14T23:15:15.647Z" }, + { url = "https://files.pythonhosted.org/packages/b2/87/b0cda79f22b8dee05f774922a214da109f9a4c0eca5da2c9d72d77ea062c/regex-2026.1.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c5ef43b5c2d4114eb8ea424bb8c9cec01d5d17f242af88b2448f5ee81caadbc", size = 288895, upload-time = "2026-01-14T23:15:17.788Z" }, + { url = "https://files.pythonhosted.org/packages/3b/6a/0041f0a2170d32be01ab981d6346c83a8934277d82c780d60b127331f264/regex-2026.1.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:968c14d4f03e10b2fd960f1d5168c1f0ac969381d3c1fcc973bc45fb06346599", size = 798680, upload-time = "2026-01-14T23:15:19.342Z" }, + { url = "https://files.pythonhosted.org/packages/58/de/30e1cfcdbe3e891324aa7568b7c968771f82190df5524fabc1138cb2d45a/regex-2026.1.15-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56a5595d0f892f214609c9f76b41b7428bed439d98dc961efafdd1354d42baae", size = 864210, upload-time = "2026-01-14T23:15:22.005Z" }, + { url = "https://files.pythonhosted.org/packages/64/44/4db2f5c5ca0ccd40ff052ae7b1e9731352fcdad946c2b812285a7505ca75/regex-2026.1.15-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf650f26087363434c4e560011f8e4e738f6f3e029b85d4904c50135b86cfa5", size = 912358, upload-time = "2026-01-14T23:15:24.569Z" }, + { url = "https://files.pythonhosted.org/packages/79/b6/e6a5665d43a7c42467138c8a2549be432bad22cbd206f5ec87162de74bd7/regex-2026.1.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18388a62989c72ac24de75f1449d0fb0b04dfccd0a1a7c1c43af5eb503d890f6", size = 803583, upload-time = "2026-01-14T23:15:26.526Z" }, + { url = "https://files.pythonhosted.org/packages/e7/53/7cd478222169d85d74d7437e74750005e993f52f335f7c04ff7adfda3310/regex-2026.1.15-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d220a2517f5893f55daac983bfa9fe998a7dbcaee4f5d27a88500f8b7873788", size = 775782, upload-time = "2026-01-14T23:15:29.352Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b5/75f9a9ee4b03a7c009fe60500fe550b45df94f0955ca29af16333ef557c5/regex-2026.1.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9c08c2fbc6120e70abff5d7f28ffb4d969e14294fb2143b4b5c7d20e46d1714", size = 787978, upload-time = "2026-01-14T23:15:31.295Z" }, + { url = "https://files.pythonhosted.org/packages/72/b3/79821c826245bbe9ccbb54f6eadb7879c722fd3e0248c17bfc90bf54e123/regex-2026.1.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ef7d5d4bd49ec7364315167a4134a015f61e8266c6d446fc116a9ac4456e10d", size = 858550, upload-time = "2026-01-14T23:15:33.558Z" }, + { url = "https://files.pythonhosted.org/packages/4a/85/2ab5f77a1c465745bfbfcb3ad63178a58337ae8d5274315e2cc623a822fa/regex-2026.1.15-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e42844ad64194fa08d5ccb75fe6a459b9b08e6d7296bd704460168d58a388f3", size = 763747, upload-time = "2026-01-14T23:15:35.206Z" }, + { url = "https://files.pythonhosted.org/packages/6d/84/c27df502d4bfe2873a3e3a7cf1bdb2b9cc10284d1a44797cf38bed790470/regex-2026.1.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cfecdaa4b19f9ca534746eb3b55a5195d5c95b88cac32a205e981ec0a22b7d31", size = 850615, upload-time = "2026-01-14T23:15:37.523Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b7/658a9782fb253680aa8ecb5ccbb51f69e088ed48142c46d9f0c99b46c575/regex-2026.1.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08df9722d9b87834a3d701f3fca570b2be115654dbfd30179f30ab2f39d606d3", size = 789951, upload-time = "2026-01-14T23:15:39.582Z" }, + { url = "https://files.pythonhosted.org/packages/3c/38/0cfd5a78e5c6db00e6782fdae70458f89850ce95baa5e8694ab91d89744f/regex-2026.1.15-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ec94c04149b6a7b8120f9f44565722c7ae31b7a6d2275569d2eefa76b83da3be", size = 492068, upload-time = "2026-01-14T23:15:47.616Z" }, + { url = "https://files.pythonhosted.org/packages/50/72/6c86acff16cb7c959c4355826bbf06aad670682d07c8f3998d9ef4fee7cd/regex-2026.1.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40c86d8046915bb9aeb15d3f3f15b6fd500b8ea4485b30e1bbc799dab3fe29f8", size = 292756, upload-time = "2026-01-14T23:15:49.307Z" }, + { url = "https://files.pythonhosted.org/packages/4e/58/df7fb69eadfe76526ddfce28abdc0af09ffe65f20c2c90932e89d705153f/regex-2026.1.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:726ea4e727aba21643205edad8f2187ec682d3305d790f73b7a51c7587b64bdd", size = 291114, upload-time = "2026-01-14T23:15:51.484Z" }, + { url = "https://files.pythonhosted.org/packages/ed/6c/a4011cd1cf96b90d2cdc7e156f91efbd26531e822a7fbb82a43c1016678e/regex-2026.1.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cb740d044aff31898804e7bf1181cc72c03d11dfd19932b9911ffc19a79070a", size = 807524, upload-time = "2026-01-14T23:15:53.102Z" }, + { url = "https://files.pythonhosted.org/packages/1d/25/a53ffb73183f69c3e9f4355c4922b76d2840aee160af6af5fac229b6201d/regex-2026.1.15-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05d75a668e9ea16f832390d22131fe1e8acc8389a694c8febc3e340b0f810b93", size = 873455, upload-time = "2026-01-14T23:15:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/66/0b/8b47fc2e8f97d9b4a851736f3890a5f786443aa8901061c55f24c955f45b/regex-2026.1.15-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d991483606f3dbec93287b9f35596f41aa2e92b7c2ebbb935b63f409e243c9af", size = 915007, upload-time = "2026-01-14T23:15:57.041Z" }, + { url = "https://files.pythonhosted.org/packages/c2/fa/97de0d681e6d26fabe71968dbee06dd52819e9a22fdce5dac7256c31ed84/regex-2026.1.15-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:194312a14819d3e44628a44ed6fea6898fdbecb0550089d84c403475138d0a09", size = 812794, upload-time = "2026-01-14T23:15:58.916Z" }, + { url = "https://files.pythonhosted.org/packages/22/38/e752f94e860d429654aa2b1c51880bff8dfe8f084268258adf9151cf1f53/regex-2026.1.15-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe2fda4110a3d0bc163c2e0664be44657431440722c5c5315c65155cab92f9e5", size = 781159, upload-time = "2026-01-14T23:16:00.817Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a7/d739ffaef33c378fc888302a018d7f81080393d96c476b058b8c64fd2b0d/regex-2026.1.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:124dc36c85d34ef2d9164da41a53c1c8c122cfb1f6e1ec377a1f27ee81deb794", size = 795558, upload-time = "2026-01-14T23:16:03.267Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c4/542876f9a0ac576100fc73e9c75b779f5c31e3527576cfc9cb3009dcc58a/regex-2026.1.15-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1774cd1981cd212506a23a14dba7fdeaee259f5deba2df6229966d9911e767a", size = 868427, upload-time = "2026-01-14T23:16:05.646Z" }, + { url = "https://files.pythonhosted.org/packages/fc/0f/d5655bea5b22069e32ae85a947aa564912f23758e112cdb74212848a1a1b/regex-2026.1.15-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b5f7d8d2867152cdb625e72a530d2ccb48a3d199159144cbdd63870882fb6f80", size = 769939, upload-time = "2026-01-14T23:16:07.542Z" }, + { url = "https://files.pythonhosted.org/packages/20/06/7e18a4fa9d326daeda46d471a44ef94201c46eaa26dbbb780b5d92cbfdda/regex-2026.1.15-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:492534a0ab925d1db998defc3c302dae3616a2fc3fe2e08db1472348f096ddf2", size = 854753, upload-time = "2026-01-14T23:16:10.395Z" }, + { url = "https://files.pythonhosted.org/packages/3b/67/dc8946ef3965e166f558ef3b47f492bc364e96a265eb4a2bb3ca765c8e46/regex-2026.1.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c661fc820cfb33e166bf2450d3dadbda47c8d8981898adb9b6fe24e5e582ba60", size = 799559, upload-time = "2026-01-14T23:16:12.347Z" }, + { url = "https://files.pythonhosted.org/packages/52/0a/47fa888ec7cbbc7d62c5f2a6a888878e76169170ead271a35239edd8f0e8/regex-2026.1.15-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d920392a6b1f353f4aa54328c867fec3320fa50657e25f64abf17af054fc97ac", size = 489170, upload-time = "2026-01-14T23:16:19.835Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c4/d000e9b7296c15737c9301708e9e7fbdea009f8e93541b6b43bdb8219646/regex-2026.1.15-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b5a28980a926fa810dbbed059547b02783952e2efd9c636412345232ddb87ff6", size = 291146, upload-time = "2026-01-14T23:16:21.541Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b6/921cc61982e538682bdf3bdf5b2c6ab6b34368da1f8e98a6c1ddc503c9cf/regex-2026.1.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:621f73a07595d83f28952d7bd1e91e9d1ed7625fb7af0064d3516674ec93a2a2", size = 288986, upload-time = "2026-01-14T23:16:23.381Z" }, + { url = "https://files.pythonhosted.org/packages/ca/33/eb7383dde0bbc93f4fb9d03453aab97e18ad4024ac7e26cef8d1f0a2cff0/regex-2026.1.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d7d92495f47567a9b1669c51fc8d6d809821849063d168121ef801bbc213846", size = 799098, upload-time = "2026-01-14T23:16:25.088Z" }, + { url = "https://files.pythonhosted.org/packages/27/56/b664dccae898fc8d8b4c23accd853f723bde0f026c747b6f6262b688029c/regex-2026.1.15-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dd16fba2758db7a3780a051f245539c4451ca20910f5a5e6ea1c08d06d4a76b", size = 864980, upload-time = "2026-01-14T23:16:27.297Z" }, + { url = "https://files.pythonhosted.org/packages/16/40/0999e064a170eddd237bae9ccfcd8f28b3aa98a38bf727a086425542a4fc/regex-2026.1.15-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e1808471fbe44c1a63e5f577a1d5f02fe5d66031dcbdf12f093ffc1305a858e", size = 911607, upload-time = "2026-01-14T23:16:29.235Z" }, + { url = "https://files.pythonhosted.org/packages/07/78/c77f644b68ab054e5a674fb4da40ff7bffb2c88df58afa82dbf86573092d/regex-2026.1.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0751a26ad39d4f2ade8fe16c59b2bf5cb19eb3d2cd543e709e583d559bd9efde", size = 803358, upload-time = "2026-01-14T23:16:31.369Z" }, + { url = "https://files.pythonhosted.org/packages/27/31/d4292ea8566eaa551fafc07797961c5963cf5235c797cc2ae19b85dfd04d/regex-2026.1.15-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0c7684c7f9ca241344ff95a1de964f257a5251968484270e91c25a755532c5", size = 775833, upload-time = "2026-01-14T23:16:33.141Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b2/cff3bf2fea4133aa6fb0d1e370b37544d18c8350a2fa118c7e11d1db0e14/regex-2026.1.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74f45d170a21df41508cb67165456538425185baaf686281fa210d7e729abc34", size = 788045, upload-time = "2026-01-14T23:16:35.005Z" }, + { url = "https://files.pythonhosted.org/packages/8d/99/2cb9b69045372ec877b6f5124bda4eb4253bc58b8fe5848c973f752bc52c/regex-2026.1.15-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1862739a1ffb50615c0fde6bae6569b5efbe08d98e59ce009f68a336f64da75", size = 859374, upload-time = "2026-01-14T23:16:36.919Z" }, + { url = "https://files.pythonhosted.org/packages/09/16/710b0a5abe8e077b1729a562d2f297224ad079f3a66dce46844c193416c8/regex-2026.1.15-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:453078802f1b9e2b7303fb79222c054cb18e76f7bdc220f7530fdc85d319f99e", size = 763940, upload-time = "2026-01-14T23:16:38.685Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/7585c8e744e40eb3d32f119191969b91de04c073fca98ec14299041f6e7e/regex-2026.1.15-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a30a68e89e5a218b8b23a52292924c1f4b245cb0c68d1cce9aec9bbda6e2c160", size = 850112, upload-time = "2026-01-14T23:16:40.646Z" }, + { url = "https://files.pythonhosted.org/packages/af/d6/43e1dd85df86c49a347aa57c1f69d12c652c7b60e37ec162e3096194a278/regex-2026.1.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9479cae874c81bf610d72b85bb681a94c95722c127b55445285fb0e2c82db8e1", size = 789586, upload-time = "2026-01-14T23:16:42.799Z" }, + { url = "https://files.pythonhosted.org/packages/ad/77/0b1e81857060b92b9cad239104c46507dd481b3ff1fa79f8e7f865aae38a/regex-2026.1.15-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ee6854c9000a10938c79238de2379bea30c82e4925a371711af45387df35cab8", size = 492073, upload-time = "2026-01-14T23:16:51.154Z" }, + { url = "https://files.pythonhosted.org/packages/70/f3/f8302b0c208b22c1e4f423147e1913fd475ddd6230565b299925353de644/regex-2026.1.15-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c2b80399a422348ce5de4fe40c418d6299a0fa2803dd61dc0b1a2f28e280fcf", size = 292757, upload-time = "2026-01-14T23:16:53.08Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f0/ef55de2460f3b4a6da9d9e7daacd0cb79d4ef75c64a2af316e68447f0df0/regex-2026.1.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dca3582bca82596609959ac39e12b7dad98385b4fefccb1151b937383cec547d", size = 291122, upload-time = "2026-01-14T23:16:55.383Z" }, + { url = "https://files.pythonhosted.org/packages/cf/55/bb8ccbacabbc3a11d863ee62a9f18b160a83084ea95cdfc5d207bfc3dd75/regex-2026.1.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71d476caa6692eea743ae5ea23cde3260677f70122c4d258ca952e5c2d4e84", size = 807761, upload-time = "2026-01-14T23:16:57.251Z" }, + { url = "https://files.pythonhosted.org/packages/8f/84/f75d937f17f81e55679a0509e86176e29caa7298c38bd1db7ce9c0bf6075/regex-2026.1.15-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c243da3436354f4af6c3058a3f81a97d47ea52c9bd874b52fd30274853a1d5df", size = 873538, upload-time = "2026-01-14T23:16:59.349Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d9/0da86327df70349aa8d86390da91171bd3ca4f0e7c1d1d453a9c10344da3/regex-2026.1.15-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8355ad842a7c7e9e5e55653eade3b7d1885ba86f124dd8ab1f722f9be6627434", size = 915066, upload-time = "2026-01-14T23:17:01.607Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5e/f660fb23fc77baa2a61aa1f1fe3a4eea2bbb8a286ddec148030672e18834/regex-2026.1.15-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f192a831d9575271a22d804ff1a5355355723f94f31d9eef25f0d45a152fdc1a", size = 812938, upload-time = "2026-01-14T23:17:04.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/a47a29bfecebbbfd1e5cd3f26b28020a97e4820f1c5148e66e3b7d4b4992/regex-2026.1.15-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:166551807ec20d47ceaeec380081f843e88c8949780cd42c40f18d16168bed10", size = 781314, upload-time = "2026-01-14T23:17:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/65/ec/7ec2bbfd4c3f4e494a24dec4c6943a668e2030426b1b8b949a6462d2c17b/regex-2026.1.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9ca1cbdc0fbfe5e6e6f8221ef2309988db5bcede52443aeaee9a4ad555e0dac", size = 795652, upload-time = "2026-01-14T23:17:08.521Z" }, + { url = "https://files.pythonhosted.org/packages/46/79/a5d8651ae131fe27d7c521ad300aa7f1c7be1dbeee4d446498af5411b8a9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b30bcbd1e1221783c721483953d9e4f3ab9c5d165aa709693d3f3946747b1aea", size = 868550, upload-time = "2026-01-14T23:17:10.573Z" }, + { url = "https://files.pythonhosted.org/packages/06/b7/25635d2809664b79f183070786a5552dd4e627e5aedb0065f4e3cf8ee37d/regex-2026.1.15-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2a8d7b50c34578d0d3bf7ad58cde9652b7d683691876f83aedc002862a35dc5e", size = 769981, upload-time = "2026-01-14T23:17:12.871Z" }, + { url = "https://files.pythonhosted.org/packages/16/8b/fc3fcbb2393dcfa4a6c5ffad92dc498e842df4581ea9d14309fcd3c55fb9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9d787e3310c6a6425eb346be4ff2ccf6eece63017916fd77fe8328c57be83521", size = 854780, upload-time = "2026-01-14T23:17:14.837Z" }, + { url = "https://files.pythonhosted.org/packages/d0/38/dde117c76c624713c8a2842530be9c93ca8b606c0f6102d86e8cd1ce8bea/regex-2026.1.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:619843841e220adca114118533a574a9cd183ed8a28b85627d2844c500a2b0db", size = 799778, upload-time = "2026-01-14T23:17:17.369Z" }, ] [[package]] @@ -3414,154 +4662,166 @@ wheels = [ [[package]] name = "rich" -version = "14.1.0" +version = "14.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "pygments", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/84/4831f881aa6ff3c976f6d6809b58cdfa350593ffc0dc3c58f5f6586780fb/rich-14.3.1.tar.gz", hash = "sha256:b8c5f568a3a749f9290ec6bddedf835cec33696bfc1e48bcfecb276c7386e4b8", size = 230125, upload-time = "2026-01-24T21:40:44.847Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, + { url = "https://files.pythonhosted.org/packages/87/2a/a1810c8627b9ec8c57ec5ec325d306701ae7be50235e8fd81266e002a3cc/rich-14.3.1-py3-none-any.whl", hash = "sha256:da750b1aebbff0b372557426fb3f35ba56de8ef954b3190315eb64076d6fb54e", size = 309952, upload-time = "2026-01-24T21:40:42.969Z" }, ] [[package]] name = "rpds-py" -version = "0.27.1" +version = "0.30.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479, upload-time = "2025-08-27T12:16:36.024Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/ed/3aef893e2dd30e77e35d20d4ddb45ca459db59cead748cad9796ad479411/rpds_py-0.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef", size = 371606, upload-time = "2025-08-27T12:12:25.189Z" }, - { url = "https://files.pythonhosted.org/packages/6d/82/9818b443e5d3eb4c83c3994561387f116aae9833b35c484474769c4a8faf/rpds_py-0.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be", size = 353452, upload-time = "2025-08-27T12:12:27.433Z" }, - { url = "https://files.pythonhosted.org/packages/99/c7/d2a110ffaaa397fc6793a83c7bd3545d9ab22658b7cdff05a24a4535cc45/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61", size = 381519, upload-time = "2025-08-27T12:12:28.719Z" }, - { url = "https://files.pythonhosted.org/packages/5a/bc/e89581d1f9d1be7d0247eaef602566869fdc0d084008ba139e27e775366c/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb", size = 394424, upload-time = "2025-08-27T12:12:30.207Z" }, - { url = "https://files.pythonhosted.org/packages/ac/2e/36a6861f797530e74bb6ed53495f8741f1ef95939eed01d761e73d559067/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657", size = 523467, upload-time = "2025-08-27T12:12:31.808Z" }, - { url = "https://files.pythonhosted.org/packages/c4/59/c1bc2be32564fa499f988f0a5c6505c2f4746ef96e58e4d7de5cf923d77e/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013", size = 402660, upload-time = "2025-08-27T12:12:33.444Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ec/ef8bf895f0628dd0a59e54d81caed6891663cb9c54a0f4bb7da918cb88cf/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a", size = 384062, upload-time = "2025-08-27T12:12:34.857Z" }, - { url = "https://files.pythonhosted.org/packages/69/f7/f47ff154be8d9a5e691c083a920bba89cef88d5247c241c10b9898f595a1/rpds_py-0.27.1-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1", size = 401289, upload-time = "2025-08-27T12:12:36.085Z" }, - { url = "https://files.pythonhosted.org/packages/3b/d9/ca410363efd0615814ae579f6829cafb39225cd63e5ea5ed1404cb345293/rpds_py-0.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10", size = 417718, upload-time = "2025-08-27T12:12:37.401Z" }, - { url = "https://files.pythonhosted.org/packages/e3/a0/8cb5c2ff38340f221cc067cc093d1270e10658ba4e8d263df923daa18e86/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808", size = 558333, upload-time = "2025-08-27T12:12:38.672Z" }, - { url = "https://files.pythonhosted.org/packages/6f/8c/1b0de79177c5d5103843774ce12b84caa7164dfc6cd66378768d37db11bf/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8", size = 589127, upload-time = "2025-08-27T12:12:41.48Z" }, - { url = "https://files.pythonhosted.org/packages/c8/5e/26abb098d5e01266b0f3a2488d299d19ccc26849735d9d2b95c39397e945/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9", size = 554899, upload-time = "2025-08-27T12:12:42.925Z" }, - { url = "https://files.pythonhosted.org/packages/b5/c1/7907329fbef97cbd49db6f7303893bd1dd5a4a3eae415839ffdfb0762cae/rpds_py-0.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881", size = 371063, upload-time = "2025-08-27T12:12:47.856Z" }, - { url = "https://files.pythonhosted.org/packages/11/94/2aab4bc86228bcf7c48760990273653a4900de89c7537ffe1b0d6097ed39/rpds_py-0.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5", size = 353210, upload-time = "2025-08-27T12:12:49.187Z" }, - { url = "https://files.pythonhosted.org/packages/3a/57/f5eb3ecf434342f4f1a46009530e93fd201a0b5b83379034ebdb1d7c1a58/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e", size = 381636, upload-time = "2025-08-27T12:12:50.492Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f4/ef95c5945e2ceb5119571b184dd5a1cc4b8541bbdf67461998cfeac9cb1e/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c", size = 394341, upload-time = "2025-08-27T12:12:52.024Z" }, - { url = "https://files.pythonhosted.org/packages/5a/7e/4bd610754bf492d398b61725eb9598ddd5eb86b07d7d9483dbcd810e20bc/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195", size = 523428, upload-time = "2025-08-27T12:12:53.779Z" }, - { url = "https://files.pythonhosted.org/packages/9f/e5/059b9f65a8c9149361a8b75094864ab83b94718344db511fd6117936ed2a/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52", size = 402923, upload-time = "2025-08-27T12:12:55.15Z" }, - { url = "https://files.pythonhosted.org/packages/f5/48/64cabb7daced2968dd08e8a1b7988bf358d7bd5bcd5dc89a652f4668543c/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed", size = 384094, upload-time = "2025-08-27T12:12:57.194Z" }, - { url = "https://files.pythonhosted.org/packages/ae/e1/dc9094d6ff566bff87add8a510c89b9e158ad2ecd97ee26e677da29a9e1b/rpds_py-0.27.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a", size = 401093, upload-time = "2025-08-27T12:12:58.985Z" }, - { url = "https://files.pythonhosted.org/packages/37/8e/ac8577e3ecdd5593e283d46907d7011618994e1d7ab992711ae0f78b9937/rpds_py-0.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde", size = 417969, upload-time = "2025-08-27T12:13:00.367Z" }, - { url = "https://files.pythonhosted.org/packages/66/6d/87507430a8f74a93556fe55c6485ba9c259949a853ce407b1e23fea5ba31/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21", size = 558302, upload-time = "2025-08-27T12:13:01.737Z" }, - { url = "https://files.pythonhosted.org/packages/3a/bb/1db4781ce1dda3eecc735e3152659a27b90a02ca62bfeea17aee45cc0fbc/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9", size = 589259, upload-time = "2025-08-27T12:13:03.127Z" }, - { url = "https://files.pythonhosted.org/packages/7b/0e/ae1c8943d11a814d01b482e1f8da903f88047a962dff9bbdadf3bd6e6fd1/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948", size = 554983, upload-time = "2025-08-27T12:13:04.516Z" }, - { url = "https://files.pythonhosted.org/packages/bd/fe/38de28dee5df58b8198c743fe2bea0c785c6d40941b9950bac4cdb71a014/rpds_py-0.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90", size = 361887, upload-time = "2025-08-27T12:13:10.233Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9a/4b6c7eedc7dd90986bf0fab6ea2a091ec11c01b15f8ba0a14d3f80450468/rpds_py-0.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5", size = 345795, upload-time = "2025-08-27T12:13:11.65Z" }, - { url = "https://files.pythonhosted.org/packages/6f/0e/e650e1b81922847a09cca820237b0edee69416a01268b7754d506ade11ad/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e", size = 385121, upload-time = "2025-08-27T12:13:13.008Z" }, - { url = "https://files.pythonhosted.org/packages/1b/ea/b306067a712988e2bff00dcc7c8f31d26c29b6d5931b461aa4b60a013e33/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881", size = 398976, upload-time = "2025-08-27T12:13:14.368Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0a/26dc43c8840cb8fe239fe12dbc8d8de40f2365e838f3d395835dde72f0e5/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec", size = 525953, upload-time = "2025-08-27T12:13:15.774Z" }, - { url = "https://files.pythonhosted.org/packages/22/14/c85e8127b573aaf3a0cbd7fbb8c9c99e735a4a02180c84da2a463b766e9e/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb", size = 407915, upload-time = "2025-08-27T12:13:17.379Z" }, - { url = "https://files.pythonhosted.org/packages/ed/7b/8f4fee9ba1fb5ec856eb22d725a4efa3deb47f769597c809e03578b0f9d9/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5", size = 386883, upload-time = "2025-08-27T12:13:18.704Z" }, - { url = "https://files.pythonhosted.org/packages/86/47/28fa6d60f8b74fcdceba81b272f8d9836ac0340570f68f5df6b41838547b/rpds_py-0.27.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a", size = 405699, upload-time = "2025-08-27T12:13:20.089Z" }, - { url = "https://files.pythonhosted.org/packages/d0/fd/c5987b5e054548df56953a21fe2ebed51fc1ec7c8f24fd41c067b68c4a0a/rpds_py-0.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444", size = 423713, upload-time = "2025-08-27T12:13:21.436Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ba/3c4978b54a73ed19a7d74531be37a8bcc542d917c770e14d372b8daea186/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a", size = 562324, upload-time = "2025-08-27T12:13:22.789Z" }, - { url = "https://files.pythonhosted.org/packages/b5/6c/6943a91768fec16db09a42b08644b960cff540c66aab89b74be6d4a144ba/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1", size = 593646, upload-time = "2025-08-27T12:13:24.122Z" }, - { url = "https://files.pythonhosted.org/packages/11/73/9d7a8f4be5f4396f011a6bb7a19fe26303a0dac9064462f5651ced2f572f/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998", size = 558137, upload-time = "2025-08-27T12:13:25.557Z" }, - { url = "https://files.pythonhosted.org/packages/cc/77/610aeee8d41e39080c7e14afa5387138e3c9fa9756ab893d09d99e7d8e98/rpds_py-0.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b", size = 361741, upload-time = "2025-08-27T12:13:31.039Z" }, - { url = "https://files.pythonhosted.org/packages/3a/fc/c43765f201c6a1c60be2043cbdb664013def52460a4c7adace89d6682bf4/rpds_py-0.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf", size = 345574, upload-time = "2025-08-27T12:13:32.902Z" }, - { url = "https://files.pythonhosted.org/packages/20/42/ee2b2ca114294cd9847d0ef9c26d2b0851b2e7e00bf14cc4c0b581df0fc3/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83", size = 385051, upload-time = "2025-08-27T12:13:34.228Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e8/1e430fe311e4799e02e2d1af7c765f024e95e17d651612425b226705f910/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf", size = 398395, upload-time = "2025-08-27T12:13:36.132Z" }, - { url = "https://files.pythonhosted.org/packages/82/95/9dc227d441ff2670651c27a739acb2535ccaf8b351a88d78c088965e5996/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2", size = 524334, upload-time = "2025-08-27T12:13:37.562Z" }, - { url = "https://files.pythonhosted.org/packages/87/01/a670c232f401d9ad461d9a332aa4080cd3cb1d1df18213dbd0d2a6a7ab51/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0", size = 407691, upload-time = "2025-08-27T12:13:38.94Z" }, - { url = "https://files.pythonhosted.org/packages/03/36/0a14aebbaa26fe7fab4780c76f2239e76cc95a0090bdb25e31d95c492fcd/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418", size = 386868, upload-time = "2025-08-27T12:13:40.192Z" }, - { url = "https://files.pythonhosted.org/packages/3b/03/8c897fb8b5347ff6c1cc31239b9611c5bf79d78c984430887a353e1409a1/rpds_py-0.27.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d", size = 405469, upload-time = "2025-08-27T12:13:41.496Z" }, - { url = "https://files.pythonhosted.org/packages/da/07/88c60edc2df74850d496d78a1fdcdc7b54360a7f610a4d50008309d41b94/rpds_py-0.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274", size = 422125, upload-time = "2025-08-27T12:13:42.802Z" }, - { url = "https://files.pythonhosted.org/packages/6b/86/5f4c707603e41b05f191a749984f390dabcbc467cf833769b47bf14ba04f/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd", size = 562341, upload-time = "2025-08-27T12:13:44.472Z" }, - { url = "https://files.pythonhosted.org/packages/b2/92/3c0cb2492094e3cd9baf9e49bbb7befeceb584ea0c1a8b5939dca4da12e5/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2", size = 592511, upload-time = "2025-08-27T12:13:45.898Z" }, - { url = "https://files.pythonhosted.org/packages/10/bb/82e64fbb0047c46a168faa28d0d45a7851cd0582f850b966811d30f67ad8/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002", size = 557736, upload-time = "2025-08-27T12:13:47.408Z" }, - { url = "https://files.pythonhosted.org/packages/01/76/1cdf1f91aed5c3a7bf2eba1f1c4e4d6f57832d73003919a20118870ea659/rpds_py-0.27.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228", size = 358355, upload-time = "2025-08-27T12:13:54.012Z" }, - { url = "https://files.pythonhosted.org/packages/c3/6f/bf142541229374287604caf3bb2a4ae17f0a580798fd72d3b009b532db4e/rpds_py-0.27.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92", size = 342138, upload-time = "2025-08-27T12:13:55.791Z" }, - { url = "https://files.pythonhosted.org/packages/1a/77/355b1c041d6be40886c44ff5e798b4e2769e497b790f0f7fd1e78d17e9a8/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2", size = 380247, upload-time = "2025-08-27T12:13:57.683Z" }, - { url = "https://files.pythonhosted.org/packages/d6/a4/d9cef5c3946ea271ce2243c51481971cd6e34f21925af2783dd17b26e815/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723", size = 390699, upload-time = "2025-08-27T12:13:59.137Z" }, - { url = "https://files.pythonhosted.org/packages/3a/06/005106a7b8c6c1a7e91b73169e49870f4af5256119d34a361ae5240a0c1d/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802", size = 521852, upload-time = "2025-08-27T12:14:00.583Z" }, - { url = "https://files.pythonhosted.org/packages/e5/3e/50fb1dac0948e17a02eb05c24510a8fe12d5ce8561c6b7b7d1339ab7ab9c/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f", size = 402582, upload-time = "2025-08-27T12:14:02.034Z" }, - { url = "https://files.pythonhosted.org/packages/cb/b0/f4e224090dc5b0ec15f31a02d746ab24101dd430847c4d99123798661bfc/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2", size = 384126, upload-time = "2025-08-27T12:14:03.437Z" }, - { url = "https://files.pythonhosted.org/packages/54/77/ac339d5f82b6afff1df8f0fe0d2145cc827992cb5f8eeb90fc9f31ef7a63/rpds_py-0.27.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21", size = 399486, upload-time = "2025-08-27T12:14:05.443Z" }, - { url = "https://files.pythonhosted.org/packages/d6/29/3e1c255eee6ac358c056a57d6d6869baa00a62fa32eea5ee0632039c50a3/rpds_py-0.27.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef", size = 414832, upload-time = "2025-08-27T12:14:06.902Z" }, - { url = "https://files.pythonhosted.org/packages/3f/db/6d498b844342deb3fa1d030598db93937a9964fcf5cb4da4feb5f17be34b/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081", size = 557249, upload-time = "2025-08-27T12:14:08.37Z" }, - { url = "https://files.pythonhosted.org/packages/60/f3/690dd38e2310b6f68858a331399b4d6dbb9132c3e8ef8b4333b96caf403d/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd", size = 587356, upload-time = "2025-08-27T12:14:10.034Z" }, - { url = "https://files.pythonhosted.org/packages/86/e3/84507781cccd0145f35b1dc32c72675200c5ce8d5b30f813e49424ef68fc/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7", size = 555300, upload-time = "2025-08-27T12:14:11.783Z" }, - { url = "https://files.pythonhosted.org/packages/70/36/b6e6066520a07cf029d385de869729a895917b411e777ab1cde878100a1d/rpds_py-0.27.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334", size = 362472, upload-time = "2025-08-27T12:14:16.333Z" }, - { url = "https://files.pythonhosted.org/packages/af/07/b4646032e0dcec0df9c73a3bd52f63bc6c5f9cda992f06bd0e73fe3fbebd/rpds_py-0.27.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33", size = 345676, upload-time = "2025-08-27T12:14:17.764Z" }, - { url = "https://files.pythonhosted.org/packages/b0/16/2f1003ee5d0af4bcb13c0cf894957984c32a6751ed7206db2aee7379a55e/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a", size = 385313, upload-time = "2025-08-27T12:14:19.829Z" }, - { url = "https://files.pythonhosted.org/packages/05/cd/7eb6dd7b232e7f2654d03fa07f1414d7dfc980e82ba71e40a7c46fd95484/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b", size = 399080, upload-time = "2025-08-27T12:14:21.531Z" }, - { url = "https://files.pythonhosted.org/packages/20/51/5829afd5000ec1cb60f304711f02572d619040aa3ec033d8226817d1e571/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7", size = 523868, upload-time = "2025-08-27T12:14:23.485Z" }, - { url = "https://files.pythonhosted.org/packages/05/2c/30eebca20d5db95720ab4d2faec1b5e4c1025c473f703738c371241476a2/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136", size = 408750, upload-time = "2025-08-27T12:14:24.924Z" }, - { url = "https://files.pythonhosted.org/packages/90/1a/cdb5083f043597c4d4276eae4e4c70c55ab5accec078da8611f24575a367/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff", size = 387688, upload-time = "2025-08-27T12:14:27.537Z" }, - { url = "https://files.pythonhosted.org/packages/7c/92/cf786a15320e173f945d205ab31585cc43969743bb1a48b6888f7a2b0a2d/rpds_py-0.27.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9", size = 407225, upload-time = "2025-08-27T12:14:28.981Z" }, - { url = "https://files.pythonhosted.org/packages/33/5c/85ee16df5b65063ef26017bef33096557a4c83fbe56218ac7cd8c235f16d/rpds_py-0.27.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60", size = 423361, upload-time = "2025-08-27T12:14:30.469Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8e/1c2741307fcabd1a334ecf008e92c4f47bb6f848712cf15c923becfe82bb/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e", size = 562493, upload-time = "2025-08-27T12:14:31.987Z" }, - { url = "https://files.pythonhosted.org/packages/04/03/5159321baae9b2222442a70c1f988cbbd66b9be0675dd3936461269be360/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212", size = 592623, upload-time = "2025-08-27T12:14:33.543Z" }, - { url = "https://files.pythonhosted.org/packages/ff/39/c09fd1ad28b85bc1d4554a8710233c9f4cefd03d7717a1b8fbfd171d1167/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675", size = 558800, upload-time = "2025-08-27T12:14:35.436Z" }, - { url = "https://files.pythonhosted.org/packages/62/7e/dc7931dc2fa4a6e46b2a4fa744a9fe5c548efd70e0ba74f40b39fa4a8c10/rpds_py-0.27.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2", size = 358944, upload-time = "2025-08-27T12:14:41.199Z" }, - { url = "https://files.pythonhosted.org/packages/e6/22/4af76ac4e9f336bfb1a5f240d18a33c6b2fcaadb7472ac7680576512b49a/rpds_py-0.27.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4", size = 342283, upload-time = "2025-08-27T12:14:42.699Z" }, - { url = "https://files.pythonhosted.org/packages/1c/15/2a7c619b3c2272ea9feb9ade67a45c40b3eeb500d503ad4c28c395dc51b4/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e", size = 380320, upload-time = "2025-08-27T12:14:44.157Z" }, - { url = "https://files.pythonhosted.org/packages/a2/7d/4c6d243ba4a3057e994bb5bedd01b5c963c12fe38dde707a52acdb3849e7/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817", size = 391760, upload-time = "2025-08-27T12:14:45.845Z" }, - { url = "https://files.pythonhosted.org/packages/b4/71/b19401a909b83bcd67f90221330bc1ef11bc486fe4e04c24388d28a618ae/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec", size = 522476, upload-time = "2025-08-27T12:14:47.364Z" }, - { url = "https://files.pythonhosted.org/packages/e4/44/1a3b9715c0455d2e2f0f6df5ee6d6f5afdc423d0773a8a682ed2b43c566c/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a", size = 403418, upload-time = "2025-08-27T12:14:49.991Z" }, - { url = "https://files.pythonhosted.org/packages/1c/4b/fb6c4f14984eb56673bc868a66536f53417ddb13ed44b391998100a06a96/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8", size = 384771, upload-time = "2025-08-27T12:14:52.159Z" }, - { url = "https://files.pythonhosted.org/packages/c0/56/d5265d2d28b7420d7b4d4d85cad8ef891760f5135102e60d5c970b976e41/rpds_py-0.27.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48", size = 400022, upload-time = "2025-08-27T12:14:53.859Z" }, - { url = "https://files.pythonhosted.org/packages/8f/e9/9f5fc70164a569bdd6ed9046486c3568d6926e3a49bdefeeccfb18655875/rpds_py-0.27.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb", size = 416787, upload-time = "2025-08-27T12:14:55.673Z" }, - { url = "https://files.pythonhosted.org/packages/d4/64/56dd03430ba491db943a81dcdef115a985aac5f44f565cd39a00c766d45c/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734", size = 557538, upload-time = "2025-08-27T12:14:57.245Z" }, - { url = "https://files.pythonhosted.org/packages/3f/36/92cc885a3129993b1d963a2a42ecf64e6a8e129d2c7cc980dbeba84e55fb/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb", size = 588512, upload-time = "2025-08-27T12:14:58.728Z" }, - { url = "https://files.pythonhosted.org/packages/dd/10/6b283707780a81919f71625351182b4f98932ac89a09023cb61865136244/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0", size = 555813, upload-time = "2025-08-27T12:15:00.334Z" }, - { url = "https://files.pythonhosted.org/packages/d5/63/b7cc415c345625d5e62f694ea356c58fb964861409008118f1245f8c3347/rpds_py-0.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf", size = 371360, upload-time = "2025-08-27T12:15:29.218Z" }, - { url = "https://files.pythonhosted.org/packages/e5/8c/12e1b24b560cf378b8ffbdb9dc73abd529e1adcfcf82727dfd29c4a7b88d/rpds_py-0.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3", size = 353933, upload-time = "2025-08-27T12:15:30.837Z" }, - { url = "https://files.pythonhosted.org/packages/9b/85/1bb2210c1f7a1b99e91fea486b9f0f894aa5da3a5ec7097cbad7dec6d40f/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636", size = 382962, upload-time = "2025-08-27T12:15:32.348Z" }, - { url = "https://files.pythonhosted.org/packages/cc/c9/a839b9f219cf80ed65f27a7f5ddbb2809c1b85c966020ae2dff490e0b18e/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8", size = 394412, upload-time = "2025-08-27T12:15:33.839Z" }, - { url = "https://files.pythonhosted.org/packages/02/2d/b1d7f928b0b1f4fc2e0133e8051d199b01d7384875adc63b6ddadf3de7e5/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc", size = 523972, upload-time = "2025-08-27T12:15:35.377Z" }, - { url = "https://files.pythonhosted.org/packages/a9/af/2cbf56edd2d07716df1aec8a726b3159deb47cb5c27e1e42b71d705a7c2f/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8", size = 403273, upload-time = "2025-08-27T12:15:37.051Z" }, - { url = "https://files.pythonhosted.org/packages/c0/93/425e32200158d44ff01da5d9612c3b6711fe69f606f06e3895511f17473b/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc", size = 385278, upload-time = "2025-08-27T12:15:38.571Z" }, - { url = "https://files.pythonhosted.org/packages/eb/1a/1a04a915ecd0551bfa9e77b7672d1937b4b72a0fc204a17deef76001cfb2/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71", size = 402084, upload-time = "2025-08-27T12:15:40.529Z" }, - { url = "https://files.pythonhosted.org/packages/51/f7/66585c0fe5714368b62951d2513b684e5215beaceab2c6629549ddb15036/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad", size = 419041, upload-time = "2025-08-27T12:15:42.191Z" }, - { url = "https://files.pythonhosted.org/packages/8e/7e/83a508f6b8e219bba2d4af077c35ba0e0cdd35a751a3be6a7cba5a55ad71/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab", size = 560084, upload-time = "2025-08-27T12:15:43.839Z" }, - { url = "https://files.pythonhosted.org/packages/66/66/bb945683b958a1b19eb0fe715594630d0f36396ebdef4d9b89c2fa09aa56/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059", size = 590115, upload-time = "2025-08-27T12:15:46.647Z" }, - { url = "https://files.pythonhosted.org/packages/12/00/ccfaafaf7db7e7adace915e5c2f2c2410e16402561801e9c7f96683002d3/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b", size = 556561, upload-time = "2025-08-27T12:15:48.219Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ed/e1fba02de17f4f76318b834425257c8ea297e415e12c68b4361f63e8ae92/rpds_py-0.27.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df", size = 371402, upload-time = "2025-08-27T12:15:51.561Z" }, - { url = "https://files.pythonhosted.org/packages/af/7c/e16b959b316048b55585a697e94add55a4ae0d984434d279ea83442e460d/rpds_py-0.27.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3", size = 354084, upload-time = "2025-08-27T12:15:53.219Z" }, - { url = "https://files.pythonhosted.org/packages/de/c1/ade645f55de76799fdd08682d51ae6724cb46f318573f18be49b1e040428/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9", size = 383090, upload-time = "2025-08-27T12:15:55.158Z" }, - { url = "https://files.pythonhosted.org/packages/1f/27/89070ca9b856e52960da1472efcb6c20ba27cfe902f4f23ed095b9cfc61d/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc", size = 394519, upload-time = "2025-08-27T12:15:57.238Z" }, - { url = "https://files.pythonhosted.org/packages/b3/28/be120586874ef906aa5aeeae95ae8df4184bc757e5b6bd1c729ccff45ed5/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4", size = 523817, upload-time = "2025-08-27T12:15:59.237Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ef/70cc197bc11cfcde02a86f36ac1eed15c56667c2ebddbdb76a47e90306da/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66", size = 403240, upload-time = "2025-08-27T12:16:00.923Z" }, - { url = "https://files.pythonhosted.org/packages/cf/35/46936cca449f7f518f2f4996e0e8344db4b57e2081e752441154089d2a5f/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e", size = 385194, upload-time = "2025-08-27T12:16:02.802Z" }, - { url = "https://files.pythonhosted.org/packages/e1/62/29c0d3e5125c3270b51415af7cbff1ec587379c84f55a5761cc9efa8cd06/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c", size = 402086, upload-time = "2025-08-27T12:16:04.806Z" }, - { url = "https://files.pythonhosted.org/packages/8f/66/03e1087679227785474466fdd04157fb793b3b76e3fcf01cbf4c693c1949/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf", size = 419272, upload-time = "2025-08-27T12:16:06.471Z" }, - { url = "https://files.pythonhosted.org/packages/6a/24/e3e72d265121e00b063aef3e3501e5b2473cf1b23511d56e529531acf01e/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf", size = 560003, upload-time = "2025-08-27T12:16:08.06Z" }, - { url = "https://files.pythonhosted.org/packages/26/ca/f5a344c534214cc2d41118c0699fffbdc2c1bc7046f2a2b9609765ab9c92/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6", size = 590482, upload-time = "2025-08-27T12:16:10.137Z" }, - { url = "https://files.pythonhosted.org/packages/ce/08/4349bdd5c64d9d193c360aa9db89adeee6f6682ab8825dca0a3f535f434f/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a", size = 556523, upload-time = "2025-08-27T12:16:12.188Z" }, + { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, ] [[package]] name = "ruff" -version = "0.14.5" +version = "0.14.14" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/fa/fbb67a5780ae0f704876cb8ac92d6d76da41da4dc72b7ed3565ab18f2f52/ruff-0.14.5.tar.gz", hash = "sha256:8d3b48d7d8aad423d3137af7ab6c8b1e38e4de104800f0d596990f6ada1a9fc1", size = 5615944, upload-time = "2025-11-13T19:58:51.155Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/31/c07e9c535248d10836a94e4f4e8c5a31a1beed6f169b31405b227872d4f4/ruff-0.14.5-py3-none-linux_armv6l.whl", hash = "sha256:f3b8248123b586de44a8018bcc9fefe31d23dda57a34e6f0e1e53bd51fd63594", size = 13171630, upload-time = "2025-11-13T19:57:54.894Z" }, - { url = "https://files.pythonhosted.org/packages/8e/5c/283c62516dca697cd604c2796d1487396b7a436b2f0ecc3fd412aca470e0/ruff-0.14.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f7a75236570318c7a30edd7f5491945f0169de738d945ca8784500b517163a72", size = 13413925, upload-time = "2025-11-13T19:57:59.181Z" }, - { url = "https://files.pythonhosted.org/packages/b6/f3/aa319f4afc22cb6fcba2b9cdfc0f03bbf747e59ab7a8c5e90173857a1361/ruff-0.14.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6d146132d1ee115f8802356a2dc9a634dbf58184c51bff21f313e8cd1c74899a", size = 12574040, upload-time = "2025-11-13T19:58:02.056Z" }, - { url = "https://files.pythonhosted.org/packages/f9/7f/cb5845fcc7c7e88ed57f58670189fc2ff517fe2134c3821e77e29fd3b0c8/ruff-0.14.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2380596653dcd20b057794d55681571a257a42327da8894b93bbd6111aa801f", size = 13009755, upload-time = "2025-11-13T19:58:05.172Z" }, - { url = "https://files.pythonhosted.org/packages/21/d2/bcbedbb6bcb9253085981730687ddc0cc7b2e18e8dc13cf4453de905d7a0/ruff-0.14.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2d1fa985a42b1f075a098fa1ab9d472b712bdb17ad87a8ec86e45e7fa6273e68", size = 12937641, upload-time = "2025-11-13T19:58:08.345Z" }, - { url = "https://files.pythonhosted.org/packages/a4/58/e25de28a572bdd60ffc6bb71fc7fd25a94ec6a076942e372437649cbb02a/ruff-0.14.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88f0770d42b7fa02bbefddde15d235ca3aa24e2f0137388cc15b2dcbb1f7c7a7", size = 13610854, upload-time = "2025-11-13T19:58:11.419Z" }, - { url = "https://files.pythonhosted.org/packages/7d/24/43bb3fd23ecee9861970978ea1a7a63e12a204d319248a7e8af539984280/ruff-0.14.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3676cb02b9061fee7294661071c4709fa21419ea9176087cb77e64410926eb78", size = 15061088, upload-time = "2025-11-13T19:58:14.551Z" }, - { url = "https://files.pythonhosted.org/packages/23/44/a022f288d61c2f8c8645b24c364b719aee293ffc7d633a2ca4d116b9c716/ruff-0.14.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b595bedf6bc9cab647c4a173a61acf4f1ac5f2b545203ba82f30fcb10b0318fb", size = 14734717, upload-time = "2025-11-13T19:58:17.518Z" }, - { url = "https://files.pythonhosted.org/packages/58/81/5c6ba44de7e44c91f68073e0658109d8373b0590940efe5bd7753a2585a3/ruff-0.14.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f55382725ad0bdb2e8ee2babcbbfb16f124f5a59496a2f6a46f1d9d99d93e6e2", size = 14028812, upload-time = "2025-11-13T19:58:20.533Z" }, - { url = "https://files.pythonhosted.org/packages/ad/ef/41a8b60f8462cb320f68615b00299ebb12660097c952c600c762078420f8/ruff-0.14.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7497d19dce23976bdaca24345ae131a1d38dcfe1b0850ad8e9e6e4fa321a6e19", size = 13825656, upload-time = "2025-11-13T19:58:23.345Z" }, - { url = "https://files.pythonhosted.org/packages/7c/00/207e5de737fdb59b39eb1fac806904fe05681981b46d6a6db9468501062e/ruff-0.14.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:410e781f1122d6be4f446981dd479470af86537fb0b8857f27a6e872f65a38e4", size = 13959922, upload-time = "2025-11-13T19:58:26.537Z" }, - { url = "https://files.pythonhosted.org/packages/bc/7e/fa1f5c2776db4be405040293618846a2dece5c70b050874c2d1f10f24776/ruff-0.14.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c01be527ef4c91a6d55e53b337bfe2c0f82af024cc1a33c44792d6844e2331e1", size = 12932501, upload-time = "2025-11-13T19:58:29.822Z" }, - { url = "https://files.pythonhosted.org/packages/67/d8/d86bf784d693a764b59479a6bbdc9515ae42c340a5dc5ab1dabef847bfaa/ruff-0.14.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f66e9bb762e68d66e48550b59c74314168ebb46199886c5c5aa0b0fbcc81b151", size = 12927319, upload-time = "2025-11-13T19:58:32.923Z" }, - { url = "https://files.pythonhosted.org/packages/ac/de/ee0b304d450ae007ce0cb3e455fe24fbcaaedae4ebaad6c23831c6663651/ruff-0.14.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d93be8f1fa01022337f1f8f3bcaa7ffee2d0b03f00922c45c2207954f351f465", size = 13206209, upload-time = "2025-11-13T19:58:35.952Z" }, - { url = "https://files.pythonhosted.org/packages/33/aa/193ca7e3a92d74f17d9d5771a765965d2cf42c86e6f0fd95b13969115723/ruff-0.14.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c135d4b681f7401fe0e7312017e41aba9b3160861105726b76cfa14bc25aa367", size = 13953709, upload-time = "2025-11-13T19:58:39.002Z" }, + { url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" }, + { url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" }, + { url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" }, + { url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" }, + { url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" }, + { url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" }, + { url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" }, + { url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" }, + { url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" }, +] + +[[package]] +name = "safetensors" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, + { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, + { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, + { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, + { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6a/4d08d89a6fcbe905c5ae68b8b34f0791850882fc19782d0d02c65abbdf3b/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4729811a6640d019a4b7ba8638ee2fd21fa5ca8c7e7bdf0fed62068fcaac737", size = 492430, upload-time = "2025-11-19T15:18:11.884Z" }, + { url = "https://files.pythonhosted.org/packages/dd/29/59ed8152b30f72c42d00d241e58eaca558ae9dbfa5695206e2e0f54c7063/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12f49080303fa6bb424b362149a12949dfbbf1e06811a88f2307276b0c131afd", size = 503977, upload-time = "2025-11-19T15:18:17.523Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/4811bfec67fa260e791369b16dab105e4bae82686120554cc484064e22b4/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0071bffba4150c2f46cae1432d31995d77acfd9f8db598b5d1a2ce67e8440ad2", size = 623890, upload-time = "2025-11-19T15:18:22.666Z" }, + { url = "https://files.pythonhosted.org/packages/58/5b/632a58724221ef03d78ab65062e82a1010e1bef8e8e0b9d7c6d7b8044841/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:473b32699f4200e69801bf5abf93f1a4ecd432a70984df164fc22ccf39c4a6f3", size = 531885, upload-time = "2025-11-19T15:18:27.146Z" }, ] [[package]] @@ -3571,9 +4831,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "joblib", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, - { name = "numpy", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux')" }, + { name = "numpy", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux')" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, - { name = "scipy", version = "1.16.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux')" }, + { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux')" }, { name = "threadpoolctl", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } @@ -3661,107 +4921,135 @@ wheels = [ [[package]] name = "scipy" -version = "1.16.2" +version = "1.17.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.11' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'linux'", ] dependencies = [ - { name = "numpy", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux')" }, + { name = "numpy", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4c/3b/546a6f0bfe791bbb7f8d591613454d15097e53f906308ec6f7c1ce588e8e/scipy-1.16.2.tar.gz", hash = "sha256:af029b153d243a80afb6eabe40b0a07f8e35c9adc269c019f364ad747f826a6b", size = 30580599, upload-time = "2025-09-11T17:48:08.271Z" } +sdist = { url = "https://files.pythonhosted.org/packages/56/3e/9cca699f3486ce6bc12ff46dc2031f1ec8eb9ccc9a320fdaf925f1417426/scipy-1.17.0.tar.gz", hash = "sha256:2591060c8e648d8b96439e111ac41fd8342fdeff1876be2e19dea3fe8930454e", size = 30396830, upload-time = "2026-01-10T21:34:23.009Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/ef/37ed4b213d64b48422df92560af7300e10fe30b5d665dd79932baebee0c6/scipy-1.16.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:6ab88ea43a57da1af33292ebd04b417e8e2eaf9d5aa05700be8d6e1b6501cd92", size = 36619956, upload-time = "2025-09-11T17:39:20.5Z" }, - { url = "https://files.pythonhosted.org/packages/85/ab/5c2eba89b9416961a982346a4d6a647d78c91ec96ab94ed522b3b6baf444/scipy-1.16.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c95e96c7305c96ede73a7389f46ccd6c659c4da5ef1b2789466baeaed3622b6e", size = 28931117, upload-time = "2025-09-11T17:39:29.06Z" }, - { url = "https://files.pythonhosted.org/packages/80/d1/eed51ab64d227fe60229a2d57fb60ca5898cfa50ba27d4f573e9e5f0b430/scipy-1.16.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:87eb178db04ece7c698220d523c170125dbffebb7af0345e66c3554f6f60c173", size = 20921997, upload-time = "2025-09-11T17:39:34.892Z" }, - { url = "https://files.pythonhosted.org/packages/be/7c/33ea3e23bbadde96726edba6bf9111fb1969d14d9d477ffa202c67bec9da/scipy-1.16.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:4e409eac067dcee96a57fbcf424c13f428037827ec7ee3cb671ff525ca4fc34d", size = 23523374, upload-time = "2025-09-11T17:39:40.846Z" }, - { url = "https://files.pythonhosted.org/packages/96/0b/7399dc96e1e3f9a05e258c98d716196a34f528eef2ec55aad651ed136d03/scipy-1.16.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e574be127bb760f0dad24ff6e217c80213d153058372362ccb9555a10fc5e8d2", size = 33583702, upload-time = "2025-09-11T17:39:49.011Z" }, - { url = "https://files.pythonhosted.org/packages/1a/bc/a5c75095089b96ea72c1bd37a4497c24b581ec73db4ef58ebee142ad2d14/scipy-1.16.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5db5ba6188d698ba7abab982ad6973265b74bb40a1efe1821b58c87f73892b9", size = 35883427, upload-time = "2025-09-11T17:39:57.406Z" }, - { url = "https://files.pythonhosted.org/packages/ab/66/e25705ca3d2b87b97fe0a278a24b7f477b4023a926847935a1a71488a6a6/scipy-1.16.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec6e74c4e884104ae006d34110677bfe0098203a3fec2f3faf349f4cb05165e3", size = 36212940, upload-time = "2025-09-11T17:40:06.013Z" }, - { url = "https://files.pythonhosted.org/packages/d6/fd/0bb911585e12f3abdd603d721d83fc1c7492835e1401a0e6d498d7822b4b/scipy-1.16.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:912f46667d2d3834bc3d57361f854226475f695eb08c08a904aadb1c936b6a88", size = 38865092, upload-time = "2025-09-11T17:40:15.143Z" }, - { url = "https://files.pythonhosted.org/packages/b7/8d/6396e00db1282279a4ddd507c5f5e11f606812b608ee58517ce8abbf883f/scipy-1.16.2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:89d6c100fa5c48472047632e06f0876b3c4931aac1f4291afc81a3644316bb0d", size = 36646259, upload-time = "2025-09-11T17:40:39.329Z" }, - { url = "https://files.pythonhosted.org/packages/3b/93/ea9edd7e193fceb8eef149804491890bde73fb169c896b61aa3e2d1e4e77/scipy-1.16.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:ca748936cd579d3f01928b30a17dc474550b01272d8046e3e1ee593f23620371", size = 28888976, upload-time = "2025-09-11T17:40:46.82Z" }, - { url = "https://files.pythonhosted.org/packages/91/4d/281fddc3d80fd738ba86fd3aed9202331180b01e2c78eaae0642f22f7e83/scipy-1.16.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:fac4f8ce2ddb40e2e3d0f7ec36d2a1e7f92559a2471e59aec37bd8d9de01fec0", size = 20879905, upload-time = "2025-09-11T17:40:52.545Z" }, - { url = "https://files.pythonhosted.org/packages/69/40/b33b74c84606fd301b2915f0062e45733c6ff5708d121dd0deaa8871e2d0/scipy-1.16.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:033570f1dcefd79547a88e18bccacff025c8c647a330381064f561d43b821232", size = 23553066, upload-time = "2025-09-11T17:40:59.014Z" }, - { url = "https://files.pythonhosted.org/packages/55/a7/22c739e2f21a42cc8f16bc76b47cff4ed54fbe0962832c589591c2abec34/scipy-1.16.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ea3421209bf00c8a5ef2227de496601087d8f638a2363ee09af059bd70976dc1", size = 33336407, upload-time = "2025-09-11T17:41:06.796Z" }, - { url = "https://files.pythonhosted.org/packages/53/11/a0160990b82999b45874dc60c0c183d3a3a969a563fffc476d5a9995c407/scipy-1.16.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f66bd07ba6f84cd4a380b41d1bf3c59ea488b590a2ff96744845163309ee8e2f", size = 35673281, upload-time = "2025-09-11T17:41:15.055Z" }, - { url = "https://files.pythonhosted.org/packages/96/53/7ef48a4cfcf243c3d0f1643f5887c81f29fdf76911c4e49331828e19fc0a/scipy-1.16.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e9feab931bd2aea4a23388c962df6468af3d808ddf2d40f94a81c5dc38f32ef", size = 36004222, upload-time = "2025-09-11T17:41:23.868Z" }, - { url = "https://files.pythonhosted.org/packages/49/7f/71a69e0afd460049d41c65c630c919c537815277dfea214031005f474d78/scipy-1.16.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03dfc75e52f72cf23ec2ced468645321407faad8f0fe7b1f5b49264adbc29cb1", size = 38664586, upload-time = "2025-09-11T17:41:31.021Z" }, - { url = "https://files.pythonhosted.org/packages/c1/27/c5b52f1ee81727a9fc457f5ac1e9bf3d6eab311805ea615c83c27ba06400/scipy-1.16.2-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:84f7bf944b43e20b8a894f5fe593976926744f6c185bacfcbdfbb62736b5cc70", size = 36604856, upload-time = "2025-09-11T17:41:47.695Z" }, - { url = "https://files.pythonhosted.org/packages/32/a9/15c20d08e950b540184caa8ced675ba1128accb0e09c653780ba023a4110/scipy-1.16.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5c39026d12edc826a1ef2ad35ad1e6d7f087f934bb868fc43fa3049c8b8508f9", size = 28864626, upload-time = "2025-09-11T17:41:52.642Z" }, - { url = "https://files.pythonhosted.org/packages/4c/fc/ea36098df653cca26062a627c1a94b0de659e97127c8491e18713ca0e3b9/scipy-1.16.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e52729ffd45b68777c5319560014d6fd251294200625d9d70fd8626516fc49f5", size = 20855689, upload-time = "2025-09-11T17:41:57.886Z" }, - { url = "https://files.pythonhosted.org/packages/dc/6f/d0b53be55727f3e6d7c72687ec18ea6d0047cf95f1f77488b99a2bafaee1/scipy-1.16.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:024dd4a118cccec09ca3209b7e8e614931a6ffb804b2a601839499cb88bdf925", size = 23512151, upload-time = "2025-09-11T17:42:02.303Z" }, - { url = "https://files.pythonhosted.org/packages/11/85/bf7dab56e5c4b1d3d8eef92ca8ede788418ad38a7dc3ff50262f00808760/scipy-1.16.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7a5dc7ee9c33019973a470556081b0fd3c9f4c44019191039f9769183141a4d9", size = 33329824, upload-time = "2025-09-11T17:42:07.549Z" }, - { url = "https://files.pythonhosted.org/packages/da/6a/1a927b14ddc7714111ea51f4e568203b2bb6ed59bdd036d62127c1a360c8/scipy-1.16.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c2275ff105e508942f99d4e3bc56b6ef5e4b3c0af970386ca56b777608ce95b7", size = 35681881, upload-time = "2025-09-11T17:42:13.255Z" }, - { url = "https://files.pythonhosted.org/packages/c1/5f/331148ea5780b4fcc7007a4a6a6ee0a0c1507a796365cc642d4d226e1c3a/scipy-1.16.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:af80196eaa84f033e48444d2e0786ec47d328ba00c71e4299b602235ffef9acb", size = 36006219, upload-time = "2025-09-11T17:42:18.765Z" }, - { url = "https://files.pythonhosted.org/packages/46/3a/e991aa9d2aec723b4a8dcfbfc8365edec5d5e5f9f133888067f1cbb7dfc1/scipy-1.16.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9fb1eb735fe3d6ed1f89918224e3385fbf6f9e23757cacc35f9c78d3b712dd6e", size = 38682147, upload-time = "2025-09-11T17:42:25.177Z" }, - { url = "https://files.pythonhosted.org/packages/09/d9/60679189bcebda55992d1a45498de6d080dcaf21ce0c8f24f888117e0c2d/scipy-1.16.2-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:53d8d2ee29b925344c13bda64ab51785f016b1b9617849dac10897f0701b20c1", size = 37012682, upload-time = "2025-09-11T17:42:30.677Z" }, - { url = "https://files.pythonhosted.org/packages/83/be/a99d13ee4d3b7887a96f8c71361b9659ba4ef34da0338f14891e102a127f/scipy-1.16.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:9e05e33657efb4c6a9d23bd8300101536abd99c85cca82da0bffff8d8764d08a", size = 29389926, upload-time = "2025-09-11T17:42:35.845Z" }, - { url = "https://files.pythonhosted.org/packages/bf/0a/130164a4881cec6ca8c00faf3b57926f28ed429cd6001a673f83c7c2a579/scipy-1.16.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:7fe65b36036357003b3ef9d37547abeefaa353b237e989c21027b8ed62b12d4f", size = 21381152, upload-time = "2025-09-11T17:42:40.07Z" }, - { url = "https://files.pythonhosted.org/packages/47/a6/503ffb0310ae77fba874e10cddfc4a1280bdcca1d13c3751b8c3c2996cf8/scipy-1.16.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6406d2ac6d40b861cccf57f49592f9779071655e9f75cd4f977fa0bdd09cb2e4", size = 23914410, upload-time = "2025-09-11T17:42:44.313Z" }, - { url = "https://files.pythonhosted.org/packages/fa/c7/1147774bcea50d00c02600aadaa919facbd8537997a62496270133536ed6/scipy-1.16.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff4dc42bd321991fbf611c23fc35912d690f731c9914bf3af8f417e64aca0f21", size = 33481880, upload-time = "2025-09-11T17:42:49.325Z" }, - { url = "https://files.pythonhosted.org/packages/6a/74/99d5415e4c3e46b2586f30cdbecb95e101c7192628a484a40dd0d163811a/scipy-1.16.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:654324826654d4d9133e10675325708fb954bc84dae6e9ad0a52e75c6b1a01d7", size = 35791425, upload-time = "2025-09-11T17:42:54.711Z" }, - { url = "https://files.pythonhosted.org/packages/1b/ee/a6559de7c1cc710e938c0355d9d4fbcd732dac4d0d131959d1f3b63eb29c/scipy-1.16.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63870a84cd15c44e65220eaed2dac0e8f8b26bbb991456a033c1d9abfe8a94f8", size = 36178622, upload-time = "2025-09-11T17:43:00.375Z" }, - { url = "https://files.pythonhosted.org/packages/4e/7b/f127a5795d5ba8ece4e0dce7d4a9fb7cb9e4f4757137757d7a69ab7d4f1a/scipy-1.16.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fa01f0f6a3050fa6a9771a95d5faccc8e2f5a92b4a2e5440a0fa7264a2398472", size = 38783985, upload-time = "2025-09-11T17:43:06.661Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ac/ad8951250516db71619f0bd3b2eb2448db04b720a003dd98619b78b692c0/scipy-1.16.2-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:567e77755019bb7461513c87f02bb73fb65b11f049aaaa8ca17cfaa5a5c45d77", size = 36595109, upload-time = "2025-09-11T17:43:35.713Z" }, - { url = "https://files.pythonhosted.org/packages/ff/f6/5779049ed119c5b503b0f3dc6d6f3f68eefc3a9190d4ad4c276f854f051b/scipy-1.16.2-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:17d9bb346194e8967296621208fcdfd39b55498ef7d2f376884d5ac47cec1a70", size = 28859110, upload-time = "2025-09-11T17:43:40.814Z" }, - { url = "https://files.pythonhosted.org/packages/82/09/9986e410ae38bf0a0c737ff8189ac81a93b8e42349aac009891c054403d7/scipy-1.16.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:0a17541827a9b78b777d33b623a6dcfe2ef4a25806204d08ead0768f4e529a88", size = 20850110, upload-time = "2025-09-11T17:43:44.981Z" }, - { url = "https://files.pythonhosted.org/packages/0d/ad/485cdef2d9215e2a7df6d61b81d2ac073dfacf6ae24b9ae87274c4e936ae/scipy-1.16.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:d7d4c6ba016ffc0f9568d012f5f1eb77ddd99412aea121e6fa8b4c3b7cbad91f", size = 23497014, upload-time = "2025-09-11T17:43:49.074Z" }, - { url = "https://files.pythonhosted.org/packages/a7/74/f6a852e5d581122b8f0f831f1d1e32fb8987776ed3658e95c377d308ed86/scipy-1.16.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9702c4c023227785c779cba2e1d6f7635dbb5b2e0936cdd3a4ecb98d78fd41eb", size = 33401155, upload-time = "2025-09-11T17:43:54.661Z" }, - { url = "https://files.pythonhosted.org/packages/d9/f5/61d243bbc7c6e5e4e13dde9887e84a5cbe9e0f75fd09843044af1590844e/scipy-1.16.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d1cdf0ac28948d225decdefcc45ad7dd91716c29ab56ef32f8e0d50657dffcc7", size = 35691174, upload-time = "2025-09-11T17:44:00.101Z" }, - { url = "https://files.pythonhosted.org/packages/03/99/59933956331f8cc57e406cdb7a483906c74706b156998f322913e789c7e1/scipy-1.16.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:70327d6aa572a17c2941cdfb20673f82e536e91850a2e4cb0c5b858b690e1548", size = 36070752, upload-time = "2025-09-11T17:44:05.619Z" }, - { url = "https://files.pythonhosted.org/packages/c6/7d/00f825cfb47ee19ef74ecf01244b43e95eae74e7e0ff796026ea7cd98456/scipy-1.16.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5221c0b2a4b58aa7c4ed0387d360fd90ee9086d383bb34d9f2789fafddc8a936", size = 38701010, upload-time = "2025-09-11T17:44:11.322Z" }, - { url = "https://files.pythonhosted.org/packages/51/b9/60929ce350c16b221928725d2d1d7f86cf96b8bc07415547057d1196dc92/scipy-1.16.2-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:9ea2a3fed83065d77367775d689401a703d0f697420719ee10c0780bcab594d8", size = 37013193, upload-time = "2025-09-11T17:44:16.757Z" }, - { url = "https://files.pythonhosted.org/packages/2a/41/ed80e67782d4bc5fc85a966bc356c601afddd175856ba7c7bb6d9490607e/scipy-1.16.2-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7280d926f11ca945c3ef92ba960fa924e1465f8d07ce3a9923080363390624c4", size = 29390172, upload-time = "2025-09-11T17:44:21.783Z" }, - { url = "https://files.pythonhosted.org/packages/c4/a3/2f673ace4090452696ccded5f5f8efffb353b8f3628f823a110e0170b605/scipy-1.16.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:8afae1756f6a1fe04636407ef7dbece33d826a5d462b74f3d0eb82deabefd831", size = 21381326, upload-time = "2025-09-11T17:44:25.982Z" }, - { url = "https://files.pythonhosted.org/packages/42/bf/59df61c5d51395066c35836b78136accf506197617c8662e60ea209881e1/scipy-1.16.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:5c66511f29aa8d233388e7416a3f20d5cae7a2744d5cee2ecd38c081f4e861b3", size = 23915036, upload-time = "2025-09-11T17:44:30.527Z" }, - { url = "https://files.pythonhosted.org/packages/91/c3/edc7b300dc16847ad3672f1a6f3f7c5d13522b21b84b81c265f4f2760d4a/scipy-1.16.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efe6305aeaa0e96b0ccca5ff647a43737d9a092064a3894e46c414db84bc54ac", size = 33484341, upload-time = "2025-09-11T17:44:35.981Z" }, - { url = "https://files.pythonhosted.org/packages/26/c7/24d1524e72f06ff141e8d04b833c20db3021020563272ccb1b83860082a9/scipy-1.16.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f3a337d9ae06a1e8d655ee9d8ecb835ea5ddcdcbd8d23012afa055ab014f374", size = 35790840, upload-time = "2025-09-11T17:44:41.76Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b7/5aaad984eeedd56858dc33d75efa59e8ce798d918e1033ef62d2708f2c3d/scipy-1.16.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bab3605795d269067d8ce78a910220262711b753de8913d3deeaedb5dded3bb6", size = 36174716, upload-time = "2025-09-11T17:44:47.316Z" }, - { url = "https://files.pythonhosted.org/packages/fd/c2/e276a237acb09824822b0ada11b028ed4067fdc367a946730979feacb870/scipy-1.16.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b0348d8ddb55be2a844c518cd8cc8deeeb8aeba707cf834db5758fc89b476a2c", size = 38790088, upload-time = "2025-09-11T17:44:53.011Z" }, + { url = "https://files.pythonhosted.org/packages/1e/4b/c89c131aa87cad2b77a54eb0fb94d633a842420fa7e919dc2f922037c3d8/scipy-1.17.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:2abd71643797bd8a106dff97894ff7869eeeb0af0f7a5ce02e4227c6a2e9d6fd", size = 31381316, upload-time = "2026-01-10T21:24:33.42Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5f/a6b38f79a07d74989224d5f11b55267714707582908a5f1ae854cf9a9b84/scipy-1.17.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:ef28d815f4d2686503e5f4f00edc387ae58dfd7a2f42e348bb53359538f01558", size = 27966760, upload-time = "2026-01-10T21:24:38.911Z" }, + { url = "https://files.pythonhosted.org/packages/c1/20/095ad24e031ee8ed3c5975954d816b8e7e2abd731e04f8be573de8740885/scipy-1.17.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:272a9f16d6bb4667e8b50d25d71eddcc2158a214df1b566319298de0939d2ab7", size = 20138701, upload-time = "2026-01-10T21:24:43.249Z" }, + { url = "https://files.pythonhosted.org/packages/89/11/4aad2b3858d0337756f3323f8960755704e530b27eb2a94386c970c32cbe/scipy-1.17.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:7204fddcbec2fe6598f1c5fdf027e9f259106d05202a959a9f1aecf036adc9f6", size = 22480574, upload-time = "2026-01-10T21:24:47.266Z" }, + { url = "https://files.pythonhosted.org/packages/85/bd/f5af70c28c6da2227e510875cadf64879855193a687fb19951f0f44cfd6b/scipy-1.17.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc02c37a5639ee67d8fb646ffded6d793c06c5622d36b35cfa8fe5ececb8f042", size = 32862414, upload-time = "2026-01-10T21:24:52.566Z" }, + { url = "https://files.pythonhosted.org/packages/ef/df/df1457c4df3826e908879fe3d76bc5b6e60aae45f4ee42539512438cfd5d/scipy-1.17.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dac97a27520d66c12a34fd90a4fe65f43766c18c0d6e1c0a80f114d2260080e4", size = 35112380, upload-time = "2026-01-10T21:24:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/5f/bb/88e2c16bd1dd4de19d80d7c5e238387182993c2fb13b4b8111e3927ad422/scipy-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb7446a39b3ae0fe8f416a9a3fdc6fba3f11c634f680f16a239c5187bc487c0", size = 34922676, upload-time = "2026-01-10T21:25:04.287Z" }, + { url = "https://files.pythonhosted.org/packages/02/ba/5120242cc735f71fc002cff0303d536af4405eb265f7c60742851e7ccfe9/scipy-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:474da16199f6af66601a01546144922ce402cb17362e07d82f5a6cf8f963e449", size = 37507599, upload-time = "2026-01-10T21:25:09.851Z" }, + { url = "https://files.pythonhosted.org/packages/0b/11/7241a63e73ba5a516f1930ac8d5b44cbbfabd35ac73a2d08ca206df007c4/scipy-1.17.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:0d5018a57c24cb1dd828bcf51d7b10e65986d549f52ef5adb6b4d1ded3e32a57", size = 31364580, upload-time = "2026-01-10T21:25:25.717Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1d/5057f812d4f6adc91a20a2d6f2ebcdb517fdbc87ae3acc5633c9b97c8ba5/scipy-1.17.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:88c22af9e5d5a4f9e027e26772cc7b5922fab8bcc839edb3ae33de404feebd9e", size = 27969012, upload-time = "2026-01-10T21:25:30.921Z" }, + { url = "https://files.pythonhosted.org/packages/e3/21/f6ec556c1e3b6ec4e088da667d9987bb77cc3ab3026511f427dc8451187d/scipy-1.17.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f3cd947f20fe17013d401b64e857c6b2da83cae567adbb75b9dcba865abc66d8", size = 20140691, upload-time = "2026-01-10T21:25:34.802Z" }, + { url = "https://files.pythonhosted.org/packages/7a/fe/5e5ad04784964ba964a96f16c8d4676aa1b51357199014dce58ab7ec5670/scipy-1.17.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e8c0b331c2c1f531eb51f1b4fc9ba709521a712cce58f1aa627bc007421a5306", size = 22463015, upload-time = "2026-01-10T21:25:39.277Z" }, + { url = "https://files.pythonhosted.org/packages/4a/69/7c347e857224fcaf32a34a05183b9d8a7aca25f8f2d10b8a698b8388561a/scipy-1.17.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5194c445d0a1c7a6c1a4a4681b6b7c71baad98ff66d96b949097e7513c9d6742", size = 32724197, upload-time = "2026-01-10T21:25:44.084Z" }, + { url = "https://files.pythonhosted.org/packages/d1/fe/66d73b76d378ba8cc2fe605920c0c75092e3a65ae746e1e767d9d020a75a/scipy-1.17.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9eeb9b5f5997f75507814ed9d298ab23f62cf79f5a3ef90031b1ee2506abdb5b", size = 35009148, upload-time = "2026-01-10T21:25:50.591Z" }, + { url = "https://files.pythonhosted.org/packages/af/07/07dec27d9dc41c18d8c43c69e9e413431d20c53a0339c388bcf72f353c4b/scipy-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:40052543f7bbe921df4408f46003d6f01c6af109b9e2c8a66dd1cf6cf57f7d5d", size = 34798766, upload-time = "2026-01-10T21:25:59.41Z" }, + { url = "https://files.pythonhosted.org/packages/81/61/0470810c8a093cdacd4ba7504b8a218fd49ca070d79eca23a615f5d9a0b0/scipy-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0cf46c8013fec9d3694dc572f0b54100c28405d55d3e2cb15e2895b25057996e", size = 37405953, upload-time = "2026-01-10T21:26:07.75Z" }, + { url = "https://files.pythonhosted.org/packages/0c/51/3468fdfd49387ddefee1636f5cf6d03ce603b75205bf439bbf0e62069bfd/scipy-1.17.0-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:65ec32f3d32dfc48c72df4291345dae4f048749bc8d5203ee0a3f347f96c5ce6", size = 31344101, upload-time = "2026-01-10T21:26:30.25Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9a/9406aec58268d437636069419e6977af953d1e246df941d42d3720b7277b/scipy-1.17.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:1f9586a58039d7229ce77b52f8472c972448cded5736eaf102d5658bbac4c269", size = 27950385, upload-time = "2026-01-10T21:26:36.801Z" }, + { url = "https://files.pythonhosted.org/packages/4f/98/e7342709e17afdfd1b26b56ae499ef4939b45a23a00e471dfb5375eea205/scipy-1.17.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9fad7d3578c877d606b1150135c2639e9de9cecd3705caa37b66862977cc3e72", size = 20122115, upload-time = "2026-01-10T21:26:42.107Z" }, + { url = "https://files.pythonhosted.org/packages/fd/0e/9eeeb5357a64fd157cbe0302c213517c541cc16b8486d82de251f3c68ede/scipy-1.17.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:423ca1f6584fc03936972b5f7c06961670dbba9f234e71676a7c7ccf938a0d61", size = 22442402, upload-time = "2026-01-10T21:26:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c9/10/be13397a0e434f98e0c79552b2b584ae5bb1c8b2be95db421533bbca5369/scipy-1.17.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe508b5690e9eaaa9467fc047f833af58f1152ae51a0d0aed67aa5801f4dd7d6", size = 32696338, upload-time = "2026-01-10T21:26:55.521Z" }, + { url = "https://files.pythonhosted.org/packages/63/1e/12fbf2a3bb240161651c94bb5cdd0eae5d4e8cc6eaeceb74ab07b12a753d/scipy-1.17.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6680f2dfd4f6182e7d6db161344537da644d1cf85cf293f015c60a17ecf08752", size = 34977201, upload-time = "2026-01-10T21:27:03.501Z" }, + { url = "https://files.pythonhosted.org/packages/19/5b/1a63923e23ccd20bd32156d7dd708af5bbde410daa993aa2500c847ab2d2/scipy-1.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eec3842ec9ac9de5917899b277428886042a93db0b227ebbe3a333b64ec7643d", size = 34777384, upload-time = "2026-01-10T21:27:11.423Z" }, + { url = "https://files.pythonhosted.org/packages/39/22/b5da95d74edcf81e540e467202a988c50fef41bd2011f46e05f72ba07df6/scipy-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d7425fcafbc09a03731e1bc05581f5fad988e48c6a861f441b7ab729a49a55ea", size = 37379586, upload-time = "2026-01-10T21:27:20.171Z" }, + { url = "https://files.pythonhosted.org/packages/20/b6/7feaa252c21cc7aff335c6c55e1b90ab3e3306da3f048109b8b639b94648/scipy-1.17.0-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:ec0827aa4d36cb79ff1b81de898e948a51ac0b9b1c43e4a372c0508c38c0f9a3", size = 31693194, upload-time = "2026-01-10T21:27:27.454Z" }, + { url = "https://files.pythonhosted.org/packages/76/bb/bbb392005abce039fb7e672cb78ac7d158700e826b0515cab6b5b60c26fb/scipy-1.17.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:819fc26862b4b3c73a60d486dbb919202f3d6d98c87cf20c223511429f2d1a97", size = 28365415, upload-time = "2026-01-10T21:27:34.26Z" }, + { url = "https://files.pythonhosted.org/packages/37/da/9d33196ecc99fba16a409c691ed464a3a283ac454a34a13a3a57c0d66f3a/scipy-1.17.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:363ad4ae2853d88ebcde3ae6ec46ccca903ea9835ee8ba543f12f575e7b07e4e", size = 20537232, upload-time = "2026-01-10T21:27:40.306Z" }, + { url = "https://files.pythonhosted.org/packages/56/9d/f4b184f6ddb28e9a5caea36a6f98e8ecd2a524f9127354087ce780885d83/scipy-1.17.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:979c3a0ff8e5ba254d45d59ebd38cde48fce4f10b5125c680c7a4bfe177aab07", size = 22791051, upload-time = "2026-01-10T21:27:46.539Z" }, + { url = "https://files.pythonhosted.org/packages/9b/9d/025cccdd738a72140efc582b1641d0dd4caf2e86c3fb127568dc80444e6e/scipy-1.17.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:130d12926ae34399d157de777472bf82e9061c60cc081372b3118edacafe1d00", size = 32815098, upload-time = "2026-01-10T21:27:54.389Z" }, + { url = "https://files.pythonhosted.org/packages/48/5f/09b879619f8bca15ce392bfc1894bd9c54377e01d1b3f2f3b595a1b4d945/scipy-1.17.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e886000eb4919eae3a44f035e63f0fd8b651234117e8f6f29bad1cd26e7bc45", size = 35031342, upload-time = "2026-01-10T21:28:03.012Z" }, + { url = "https://files.pythonhosted.org/packages/f2/9a/f0f0a9f0aa079d2f106555b984ff0fbb11a837df280f04f71f056ea9c6e4/scipy-1.17.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:13c4096ac6bc31d706018f06a49abe0485f96499deb82066b94d19b02f664209", size = 34893199, upload-time = "2026-01-10T21:28:10.832Z" }, + { url = "https://files.pythonhosted.org/packages/90/b8/4f0f5cf0c5ea4d7548424e6533e6b17d164f34a6e2fb2e43ffebb6697b06/scipy-1.17.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cacbaddd91fcffde703934897c5cd2c7cb0371fac195d383f4e1f1c5d3f3bd04", size = 37438061, upload-time = "2026-01-10T21:28:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2d/51006cd369b8e7879e1c630999a19d1fbf6f8b5ed3e33374f29dc87e53b3/scipy-1.17.0-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:c17514d11b78be8f7e6331b983a65a7f5ca1fd037b95e27b280921fe5606286a", size = 31346803, upload-time = "2026-01-10T21:28:57.24Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2e/2349458c3ce445f53a6c93d4386b1c4c5c0c540917304c01222ff95ff317/scipy-1.17.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:4e00562e519c09da34c31685f6acc3aa384d4d50604db0f245c14e1b4488bfa2", size = 27967182, upload-time = "2026-01-10T21:29:04.107Z" }, + { url = "https://files.pythonhosted.org/packages/5e/7c/df525fbfa77b878d1cfe625249529514dc02f4fd5f45f0f6295676a76528/scipy-1.17.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f7df7941d71314e60a481e02d5ebcb3f0185b8d799c70d03d8258f6c80f3d467", size = 20139125, upload-time = "2026-01-10T21:29:10.179Z" }, + { url = "https://files.pythonhosted.org/packages/33/11/fcf9d43a7ed1234d31765ec643b0515a85a30b58eddccc5d5a4d12b5f194/scipy-1.17.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:aabf057c632798832f071a8dde013c2e26284043934f53b00489f1773b33527e", size = 22443554, upload-time = "2026-01-10T21:29:15.888Z" }, + { url = "https://files.pythonhosted.org/packages/80/5c/ea5d239cda2dd3d31399424967a24d556cf409fbea7b5b21412b0fd0a44f/scipy-1.17.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a38c3337e00be6fd8a95b4ed66b5d988bac4ec888fd922c2ea9fe5fb1603dd67", size = 32757834, upload-time = "2026-01-10T21:29:23.406Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7e/8c917cc573310e5dc91cbeead76f1b600d3fb17cf0969db02c9cf92e3cfa/scipy-1.17.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00fb5f8ec8398ad90215008d8b6009c9db9fa924fd4c7d6be307c6f945f9cd73", size = 34995775, upload-time = "2026-01-10T21:29:31.915Z" }, + { url = "https://files.pythonhosted.org/packages/c5/43/176c0c3c07b3f7df324e7cdd933d3e2c4898ca202b090bd5ba122f9fe270/scipy-1.17.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f2a4942b0f5f7c23c7cd641a0ca1955e2ae83dedcff537e3a0259096635e186b", size = 34841240, upload-time = "2026-01-10T21:29:39.995Z" }, + { url = "https://files.pythonhosted.org/packages/44/8c/d1f5f4b491160592e7f084d997de53a8e896a3ac01cd07e59f43ca222744/scipy-1.17.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf133ced83889583156566d2bdf7a07ff89228fe0c0cb727f777de92092ec6b", size = 37394463, upload-time = "2026-01-10T21:29:48.723Z" }, + { url = "https://files.pythonhosted.org/packages/e9/01/f58916b9d9ae0112b86d7c3b10b9e685625ce6e8248df139d0fcb17f7397/scipy-1.17.0-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:2b531f57e09c946f56ad0b4a3b2abee778789097871fc541e267d2eca081cff1", size = 31706502, upload-time = "2026-01-10T21:29:56.326Z" }, + { url = "https://files.pythonhosted.org/packages/59/8e/2912a87f94a7d1f8b38aabc0faf74b82d3b6c9e22be991c49979f0eceed8/scipy-1.17.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:13e861634a2c480bd237deb69333ac79ea1941b94568d4b0efa5db5e263d4fd1", size = 28380854, upload-time = "2026-01-10T21:30:01.554Z" }, + { url = "https://files.pythonhosted.org/packages/bd/1c/874137a52dddab7d5d595c1887089a2125d27d0601fce8c0026a24a92a0b/scipy-1.17.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:eb2651271135154aa24f6481cbae5cc8af1f0dd46e6533fb7b56aa9727b6a232", size = 20552752, upload-time = "2026-01-10T21:30:05.93Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f0/7518d171cb735f6400f4576cf70f756d5b419a07fe1867da34e2c2c9c11b/scipy-1.17.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:c5e8647f60679790c2f5c76be17e2e9247dc6b98ad0d3b065861e082c56e078d", size = 22803972, upload-time = "2026-01-10T21:30:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/7c/74/3498563a2c619e8a3ebb4d75457486c249b19b5b04a30600dfd9af06bea5/scipy-1.17.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fb10d17e649e1446410895639f3385fd2bf4c3c7dfc9bea937bddcbc3d7b9ba", size = 32829770, upload-time = "2026-01-10T21:30:16.359Z" }, + { url = "https://files.pythonhosted.org/packages/48/d1/7b50cedd8c6c9d6f706b4b36fa8544d829c712a75e370f763b318e9638c1/scipy-1.17.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8547e7c57f932e7354a2319fab613981cde910631979f74c9b542bb167a8b9db", size = 35051093, upload-time = "2026-01-10T21:30:22.987Z" }, + { url = "https://files.pythonhosted.org/packages/e2/82/a2d684dfddb87ba1b3ea325df7c3293496ee9accb3a19abe9429bce94755/scipy-1.17.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33af70d040e8af9d5e7a38b5ed3b772adddd281e3062ff23fec49e49681c38cf", size = 34909905, upload-time = "2026-01-10T21:30:28.704Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5e/e565bd73991d42023eb82bb99e51c5b3d9e2c588ca9d4b3e2cc1d3ca62a6/scipy-1.17.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb55bb97d00f8b7ab95cb64f873eb0bf54d9446264d9f3609130381233483f", size = 37457743, upload-time = "2026-01-10T21:30:34.819Z" }, ] [[package]] name = "selectolax" -version = "0.3.29" +version = "0.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/b9/b5a23e29d5e54c590eaad18bdbb1ced13b869b111e03d12ee0ae9eecf9b8/selectolax-0.3.29.tar.gz", hash = "sha256:28696fa4581765c705e15d05dfba464334f5f9bcb3eac9f25045f815aec6fbc1", size = 4691626, upload-time = "2025-04-30T15:17:37.98Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/c5/959b8661d200d9fd3cf52061ce6f1d7087ec94823bb324fe1ca76c80b250/selectolax-0.4.6.tar.gz", hash = "sha256:bd9326cfc9bbd5bfcda980b0452b9761b3cf134015154e95d83fa32cbfbb751b", size = 4793248, upload-time = "2025-12-06T12:35:48.513Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/8f/bf3d58ecc0e187806299324e2ad77646e837ff20400880f6fc0cbd14fb66/selectolax-0.3.29-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:85aeae54f055cf5451828a21fbfecac99b8b5c27ec29fd10725b631593a7c9a3", size = 3643657, upload-time = "2025-04-30T15:15:40.734Z" }, - { url = "https://files.pythonhosted.org/packages/de/b0/6d90a4d0eacb8253d88a9fcbcb8758b667900f45dcdb4a11c5fbd0d31599/selectolax-0.3.29-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6ff48efe4364c8148a553a4105773a0accee9cc25e0f2a40ddac44d18a5a3000", size = 2089380, upload-time = "2025-04-30T15:15:42.928Z" }, - { url = "https://files.pythonhosted.org/packages/f4/21/394b51998ef99f13f98da063fc71b8edf7191bb30aca06bcbc8a55d5a9ad/selectolax-0.3.29-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25cfccfefc41361ab8a07f15a224524a4a8b77dfa7d253b34bbd397e45856734", size = 5505065, upload-time = "2025-04-30T15:15:44.986Z" }, - { url = "https://files.pythonhosted.org/packages/dd/57/e38775b672f910e80742cbf7c3def5c670c1b6f9b05e8587b2fa8dc044c3/selectolax-0.3.29-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f5c3523ad5199a4fb9b95b6e24ff9222d3605023ca394b23f7dd910e7536daf", size = 5529205, upload-time = "2025-04-30T15:15:47.149Z" }, - { url = "https://files.pythonhosted.org/packages/ec/0f/f6e3030107b486b6a4870f8471a675d435c4c34b8f9de3374652ed53004b/selectolax-0.3.29-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfb803d6bbe0ef3c8847cf5a01167cc428c0d9179946e1c994cc6178b5332d1a", size = 5146713, upload-time = "2025-04-30T15:15:49.332Z" }, - { url = "https://files.pythonhosted.org/packages/d8/8d/b4fd119c216e8615ca6747f8f336643572178241921f33f5ffa4b074dc44/selectolax-0.3.29-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:db734ba4ef44fa3b57ad9374fd7ccfc7815c0ae5cfcbd5ee25fe8587092618d1", size = 5416352, upload-time = "2025-04-30T15:15:50.909Z" }, - { url = "https://files.pythonhosted.org/packages/d7/e7/94e694d14ae44bddc0d9b144647d5adbec0210d8e2c57d72ad9a133d9469/selectolax-0.3.29-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2bfe4327215a20af4197c5b7e3729a9552fb324bb57250dc7e7abfa0f848a463", size = 5140689, upload-time = "2025-04-30T15:15:52.477Z" }, - { url = "https://files.pythonhosted.org/packages/90/62/79ba965daa1f12e5477b2ec08b289f8289dfc705928b08923d9c4b60c867/selectolax-0.3.29-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0a98c3f3d8fffb175456cb06096bc78103ddf6a209bea6392e0e4ea4e25aca71", size = 5481428, upload-time = "2025-04-30T15:15:54.371Z" }, - { url = "https://files.pythonhosted.org/packages/2a/5d/ca72f7adddae4b2b128394a7559739a6a12c156d29b55968cfcfe07fac4d/selectolax-0.3.29-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d6a1cd0518fa7656ea1683c4b2d3b5a98306753f364da9f673517847e1680a3e", size = 3649215, upload-time = "2025-04-30T15:15:59.57Z" }, - { url = "https://files.pythonhosted.org/packages/08/c6/ca984f90b12fb10790cc56c2670f1b5f09884ed2f2012a219094b38cbcb4/selectolax-0.3.29-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3e5354d805dd76b4b38002f58e6ae2e7b429ac311bf3601992a6662d2bc86911", size = 2091848, upload-time = "2025-04-30T15:16:01.73Z" }, - { url = "https://files.pythonhosted.org/packages/98/7f/c999ae6d9bfbaac3e8dea3dbb5ca6bdf61c220828e80a6c339e89f9db777/selectolax-0.3.29-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7073e3bcdc60ebdb5f8777c79b465471ec000ab556134da4e00f037d3321a2ec", size = 5638593, upload-time = "2025-04-30T15:16:03.594Z" }, - { url = "https://files.pythonhosted.org/packages/d6/32/ffd89376a888c24ecaf01fcffc5fe97b82ae03ab163158f51a559f1ebad5/selectolax-0.3.29-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47587db7cef411d22f8224cf2926aacdb326c4c838d386035229f16ccc2d8d26", size = 5668207, upload-time = "2025-04-30T15:16:05.564Z" }, - { url = "https://files.pythonhosted.org/packages/3a/5c/2de0c7b8be75ad52d44706c67946181b972f27641ab4f6a1f27f46d2a603/selectolax-0.3.29-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:21de62b5093b1cb6c5d4cab0bef5f708b9ee1483b640d42be9d955becfcd287a", size = 5276654, upload-time = "2025-04-30T15:16:07.143Z" }, - { url = "https://files.pythonhosted.org/packages/29/29/152bb745b24072d3eecd3b395c756e74763111b9bbd265604f5b96b9a1aa/selectolax-0.3.29-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:af5cd03298cd75cb0fbf712d6ae4f8aca9c13a226d2821ca82f51cc9b33b032f", size = 5543731, upload-time = "2025-04-30T15:16:09.733Z" }, - { url = "https://files.pythonhosted.org/packages/04/1d/df65baaf16ece393f9f1a7c55f015510634adbb163ce72adcafaddf5cf9c/selectolax-0.3.29-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3f58dca53d2d3dc18dfd2cb9210a5625f32598db24e3f857f5be58f21a8f3b88", size = 5275005, upload-time = "2025-04-30T15:16:11.958Z" }, - { url = "https://files.pythonhosted.org/packages/5d/74/e56fd6f9b3087947b812f3862df3265bf5e21396d9673d076e999b1086cf/selectolax-0.3.29-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d0a6d8e02c6b9ba951d7b5a5dd2788a1d4bbdedc89782a4de165f1a87c4168ac", size = 5617441, upload-time = "2025-04-30T15:16:14.15Z" }, - { url = "https://files.pythonhosted.org/packages/30/ac/ca4332eecc19124782f6f0d7cb28c331da2e9d9cf25287ba2b3b6a00cea1/selectolax-0.3.29-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6d3f373efd1db18ac9b2222de2668aaa366a1f0b560241eab128f3ca68e8add1", size = 3656166, upload-time = "2025-04-30T15:16:19.907Z" }, - { url = "https://files.pythonhosted.org/packages/b8/46/2dcae03a94f80f3e0d339c149de8110b5abe1230668b015fd338d9e71a27/selectolax-0.3.29-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:97b9971bb37b54ef4440134f22792d15c9ee12d890a526a7fe0b376502240143", size = 2095991, upload-time = "2025-04-30T15:16:21.654Z" }, - { url = "https://files.pythonhosted.org/packages/1e/bd/95f15396e5f30898227d84a7ec6a39d9a9b34005f0e9f8f38e7fee21ab66/selectolax-0.3.29-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd99ff0f5a6c017c471635d4ee45b61d25f24689331e407147b2cf5e36892480", size = 5844493, upload-time = "2025-04-30T15:16:23.268Z" }, - { url = "https://files.pythonhosted.org/packages/36/25/64c60da9aec81f2992355b0a3ce00ea1ed99e6f5499868016d6972bd4948/selectolax-0.3.29-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8377c317bf1d5fd6ccc56dfb5a0928bbcbea3e800b7af54761cfbbb99dc94cb9", size = 5881062, upload-time = "2025-04-30T15:16:24.891Z" }, - { url = "https://files.pythonhosted.org/packages/b6/81/94105217f91f7c6a98ac3164210cba0c6aa8da91cb85405292a6d70e39c3/selectolax-0.3.29-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5388c56456272b2c241fc1906db9cc993984cafdad936cb5e061e3af0c44144e", size = 5470368, upload-time = "2025-04-30T15:16:26.457Z" }, - { url = "https://files.pythonhosted.org/packages/51/6e/40bc259f13e5d3dd0bb8ddd1d55ef099244db2568ffb82fd9d489984d61a/selectolax-0.3.29-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9e4690894f406863e25ba49da27e1a6fda9bfc21b0b315c399d3093be080e81", size = 5693476, upload-time = "2025-04-30T15:16:28.386Z" }, - { url = "https://files.pythonhosted.org/packages/58/bd/2668ee1d5471ad88daf83ca484515ba46774fc9c951d6c4c0beffea89952/selectolax-0.3.29-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:deeab93386b6c9a75052515f5b9e7e3dd623c585871c0c2b3126970ff902603b", size = 5449747, upload-time = "2025-04-30T15:16:30.626Z" }, - { url = "https://files.pythonhosted.org/packages/a1/b5/1c61839ae5af70a8291c643982a99f051b543df90b220b98db1b26bd4899/selectolax-0.3.29-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6abdd8357f1c105c1add01a9f0373511fa832548b2e2778b00a8ba2a4508d6ed", size = 5786843, upload-time = "2025-04-30T15:16:32.231Z" }, - { url = "https://files.pythonhosted.org/packages/c0/a7/083a00aa9cb6bef0317baba4269841c366652558d77189275bed2da6aa81/selectolax-0.3.29-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e3112f05a34bf36d36ecc51520b1d98c4667b54a3f123dffef5072273e89a360", size = 3651407, upload-time = "2025-04-30T15:16:37.282Z" }, - { url = "https://files.pythonhosted.org/packages/7e/cd/6c89ac27961ef5f5e9b40eda0d0653b9c95c93485fb8a554bf093eac1c77/selectolax-0.3.29-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:38462ae369897f71da287f1282079c11f1b878b99a4d1d509d1116ce05226d88", size = 2092649, upload-time = "2025-04-30T15:16:38.817Z" }, - { url = "https://files.pythonhosted.org/packages/3e/12/82710124b7b52613fdb9d5c14494a41785eb83e1c93ec7e1d1814c2ce292/selectolax-0.3.29-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdd1e63735f2fb8485fb6b9f4fe30d6c030930f438f46a4a62bd9886ab3c7fd9", size = 5821738, upload-time = "2025-04-30T15:16:40.747Z" }, - { url = "https://files.pythonhosted.org/packages/8b/08/8ceb3eb7fee9743026a4481fccb771f257c82b2c853a1a30271902234eab/selectolax-0.3.29-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea52e0c128e8e89f98ab0ccaabbc853677de5730729a3351da595976131b66e0", size = 5856069, upload-time = "2025-04-30T15:16:42.496Z" }, - { url = "https://files.pythonhosted.org/packages/47/6c/ec2b7aff0f6202e4157415d76bd588108cc518374bf53afa81c122691780/selectolax-0.3.29-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0933659b4250b91317ccd78167e6804389cdaf7ed86c5d034b058a550d23110f", size = 5443255, upload-time = "2025-04-30T15:16:44.083Z" }, - { url = "https://files.pythonhosted.org/packages/cd/90/d5fea46ff191d02c2380a779b119ea6799751b79fcddb2bb230b21b38fc5/selectolax-0.3.29-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0c9005e9089a6b0c6fb6a9f691ddbbb10a3a23ebeff54393980340f3dbcdb99", size = 5637529, upload-time = "2025-04-30T15:16:46.175Z" }, - { url = "https://files.pythonhosted.org/packages/9d/83/7f876a515f5af31f7b948cf10951be896fe6deeff2b9b713640c8ec82fd3/selectolax-0.3.29-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac940963c52f13cdf5d7266a979744949b660d367ce669efa073b557f6e09a18", size = 5379121, upload-time = "2025-04-30T15:16:47.909Z" }, - { url = "https://files.pythonhosted.org/packages/57/cb/7dc739a484b1a17ccf92a23dfe558ae615c232bd81e78a72049c25d1ff66/selectolax-0.3.29-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:484274f73839f9a143f4c13ce1b0a0123b5d64be22f967a1dc202a9a78687d67", size = 5727944, upload-time = "2025-04-30T15:16:49.52Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b6/a4753cdde612caf292abb3519b8770a004cb696fac0aea40b403f5e27672/selectolax-0.4.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0e870cc935a36b8e37945c5e2055191df84e7af278dd386e5ff9f04552e5da6b", size = 2052715, upload-time = "2025-12-06T12:34:08.527Z" }, + { url = "https://files.pythonhosted.org/packages/3a/03/a3b21e54fbe5fee47d5e27fdb5696467c7d9adb7308d1bc85f1468a4d08e/selectolax-0.4.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b9008e4c5f708634baa6dbeb4cd8922edc8467eb842cf12da5cdf03399a969f3", size = 2051040, upload-time = "2025-12-06T12:34:10.365Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ad/3984fa06ece9c773acc7938ccbee922d437d5396fb331f22a122da6fea8d/selectolax-0.4.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c7120afee58104f9d708b62b4e14caa9dc88c23e3f752b62a43dac953963362", size = 2237979, upload-time = "2025-12-06T12:34:12.053Z" }, + { url = "https://files.pythonhosted.org/packages/00/f2/ed47a855ca70db65b1c5d90e6cbb63132687a0b623a2edca5de26a68cadc/selectolax-0.4.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ceb1af4892aafd26612b1974c49abf524b5f69b2a9d521381d97dc8ca5c0e3d7", size = 2274003, upload-time = "2025-12-06T12:34:13.807Z" }, + { url = "https://files.pythonhosted.org/packages/13/74/840b0cd0a178718c815610fbe704803b4cf98aa1a8eb218788b77c020811/selectolax-0.4.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3a7a28857b7bd72e904c3e2fce9a21ceca60093622b3ceaf8e231a9ec24a0ca1", size = 2251372, upload-time = "2025-12-06T12:34:15.611Z" }, + { url = "https://files.pythonhosted.org/packages/a6/56/07bdcadb0180541b36c3f5dccd1d5243503ab695c2df6030e533ea098529/selectolax-0.4.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59162e0b78d82c8b60e1318b3e72b49eed5858c2640f1df759d3f60584e4f215", size = 2280044, upload-time = "2025-12-06T12:34:17.382Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/9b5358d82353b68c5828cc8ceae4ff1073495462979d2035c1089f4421dd/selectolax-0.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:751f727c9963584fcfa101b2696e0dd31236142901bbe7866a8e39f2ec346cc4", size = 2052057, upload-time = "2025-12-06T12:34:24.448Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d1/9f8c8841f414a1b72174acef916d4ed38cc0fa041d3e933013e2b3213f30/selectolax-0.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d6f45737b638836b9fe2a4e7ccfbcd48a315ebb96da76a79a04b8227c1a967ee", size = 2050379, upload-time = "2025-12-06T12:34:26.383Z" }, + { url = "https://files.pythonhosted.org/packages/bc/c4/8e5ab9275a185a31d06b5ea58e3ba61d57e57700964cbd56fe074dbe458c/selectolax-0.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15a22c5cd9c23f09227b2b9c227d986396125a9b0eb21be655f22fe4ccc5679a", size = 2238005, upload-time = "2025-12-06T12:34:28.2Z" }, + { url = "https://files.pythonhosted.org/packages/ea/af/f4299d931a8e11ba1998cac70d407c5338436978325232eb252ac7f5aba2/selectolax-0.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e64f771807f1a7353f4d6878c303bfbd6c6fe58897e301aa6d0de7e5e60cef61", size = 2272927, upload-time = "2025-12-06T12:34:29.955Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5e/9afbb0e8495846bf97fa7725a6f97622ad85efc654cb3cbf4c881bd345de/selectolax-0.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0144a37fbf01695ccf2d0d24caaa058a28449cecb2c754bb9e616f339468d74", size = 2250901, upload-time = "2025-12-06T12:34:31.442Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6b/914cc9c977fd21949af5776d25b9c011b6e72fb38569161ab6ca83d6130a/selectolax-0.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c9bdd4a9b3f71f28a0ee558a105451debd33cbe1ed350ebba7ad6b68d62815c", size = 2279842, upload-time = "2025-12-06T12:34:32.739Z" }, + { url = "https://files.pythonhosted.org/packages/ee/81/1fdf6633df840afd9d7054a3441f04cfb1fc9d098c6c9f3bd46a64fb632e/selectolax-0.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:20615062d6062b197b61fd646e667591e987be3a894e8a8408d2a482ccddc747", size = 2051021, upload-time = "2025-12-06T12:34:38.495Z" }, + { url = "https://files.pythonhosted.org/packages/cc/54/d59738d090cb4df3a3a6297b7ec216b86d3ba7f320013c4bc8d4841c9f5d/selectolax-0.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c436006e2af6ade80b96411cf46652c11ced4f230032e25e1f5210b7522a4fe3", size = 2047409, upload-time = "2025-12-06T12:34:39.875Z" }, + { url = "https://files.pythonhosted.org/packages/fc/67/3b163ec18a128df3a3b59ce676a2dacfb26e714da81ba8a98e184b4ef187/selectolax-0.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:705a70b6f4e553e8c5299881269d3735a7df8a23711927a33caa16b4eaef580f", size = 2237052, upload-time = "2025-12-06T12:34:41.24Z" }, + { url = "https://files.pythonhosted.org/packages/f0/04/c3ae4a77e8cfa647b9177e727a7e80f64b160b65ad0db0dcb3738a4ef4a0/selectolax-0.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c04fd180689ed9450ad2453a3cba74cff2475a4281f76db9e18a658b7823e594", size = 2275615, upload-time = "2025-12-06T12:34:43.114Z" }, + { url = "https://files.pythonhosted.org/packages/12/de/aaa016c44e63a1efd5525f6da6eac807388a06c70671091c735d93f13b74/selectolax-0.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cb33eb0809e70ba4a475105d164c3f90a4bb711744ca69e20037298256b8e9d7", size = 2249186, upload-time = "2025-12-06T12:34:44.84Z" }, + { url = "https://files.pythonhosted.org/packages/76/9a/a9cf9f0158b0804c7ea404d790af031830eb6452a4948853f7582eea6c51/selectolax-0.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:97f30b7c731f9f3328e9c6aef7ca3c17fbcbc4495e286a2cdad9a77bcadfadf1", size = 2282041, upload-time = "2025-12-06T12:34:46.19Z" }, + { url = "https://files.pythonhosted.org/packages/4d/87/7ed053cce7de8b29746c602851c67654287e25ec404d575911c6f40b671f/selectolax-0.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8303af0eeef6ab2f06f2b18e3c825df4f6984405a862c8e9e654621c26558aca", size = 2050412, upload-time = "2025-12-06T12:34:53.086Z" }, + { url = "https://files.pythonhosted.org/packages/44/74/e8cd9b9b53da0e849b27a2ef68580915321ee52a662f015275a1cf6cca85/selectolax-0.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d8b1fb5507d90e719de705d93eaa3bdd5f2d69facf012fb67b7da8a9cd20ea6b", size = 2046513, upload-time = "2025-12-06T12:34:54.583Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ca/c1cc7f03b681d272dbb0dc47cf9d0df857ae156d7ea54d88cc25ec23c8e9/selectolax-0.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30ac51bd5bfcd6bffe58720b1fc5f97666681f0793d79d292069b3a3f8599ef0", size = 2234404, upload-time = "2025-12-06T12:34:55.971Z" }, + { url = "https://files.pythonhosted.org/packages/df/00/a6e5c4d65243147fbd8837951267f098d9bee66ada4dc0c99d97259e052c/selectolax-0.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfee4d1269fd462256641abdf6c2ee4dd799ba82c578acab0bcde07547637826", size = 2272678, upload-time = "2025-12-06T12:34:57.3Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a2/05351e0f0da62d1bc01b7820a990f1e4dec688206e0278ee8faeeb878940/selectolax-0.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fce09eeb5dd19fba672a829f63e3c40238af4a929ce1e5fd16dcbc4fd253e300", size = 2247471, upload-time = "2025-12-06T12:34:59.048Z" }, + { url = "https://files.pythonhosted.org/packages/ea/95/1ab9e3ad2d53dbcd7141af149c7259c693dc2dc46e27d72b7a68f17f3364/selectolax-0.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4e97ca71d545cab9c57ba3b18358cbc96ef0dcd01920ea903a531386ca1f849", size = 2278271, upload-time = "2025-12-06T12:35:00.487Z" }, + { url = "https://files.pythonhosted.org/packages/d2/38/93b4907c596487e13f8261129b1663559c96fc37ea2c973c98a393307484/selectolax-0.4.6-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:961c037ac41a12bf9db35a95d6076461f9b127d361c9ef26a77f76c45b1056eb", size = 2069131, upload-time = "2025-12-06T12:35:06.737Z" }, + { url = "https://files.pythonhosted.org/packages/f6/04/d29769a8313ccb9db6278401840e79662f341b716d268f7468b6270d15e1/selectolax-0.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f2be076a69a12657a38933b99d31c604d95d63911e0799f89305da8e89918874", size = 2066134, upload-time = "2025-12-06T12:35:08.163Z" }, + { url = "https://files.pythonhosted.org/packages/29/a2/1c26b4cc6a708d234e39199bd75acdc1cfdcf4f3138e16d25e3e7aa0295d/selectolax-0.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25ae5dd7c20035312211de43952934981e8ff4e1502467ce78665f57bc5eaf7f", size = 2240810, upload-time = "2025-12-06T12:35:09.556Z" }, + { url = "https://files.pythonhosted.org/packages/1f/de/b021342d306bd08c92d0d9e9072a3ed6a3038f78387187d37fb775196fa0/selectolax-0.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfe1274930affe2b986fd20cbf2916ddf4a760edf30a7eeb9151b31e8cbe6027", size = 2274401, upload-time = "2025-12-06T12:35:11.023Z" }, + { url = "https://files.pythonhosted.org/packages/29/77/b2816be2f4878f4e86fabca5932610401dc88d956948d97a82947e27d8bc/selectolax-0.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e52174a75236c1321f0f0353b5d97ba575c61428f16c2d78159cb154fa407e97", size = 2271054, upload-time = "2025-12-06T12:35:12.878Z" }, + { url = "https://files.pythonhosted.org/packages/1f/3a/ae48b9febf2af21a0fdd4fba07861ae3e13bd7235df3fa39918d33004058/selectolax-0.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5032187ed8f8d77da6dee5467a87fe9b1553c10c63671fe830e87a2d347ef8ae", size = 2297773, upload-time = "2025-12-06T12:35:14.383Z" }, + { url = "https://files.pythonhosted.org/packages/54/ce/f3fa904517745ecd289b6ce18845ae968cf7d0b17e65ab781259f2746254/selectolax-0.4.6-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:48a9bce389c281fec8bf3b5a36bd376f1ad1f36ff072dcedaa977188b3882be1", size = 2088107, upload-time = "2025-12-06T12:35:21.061Z" }, + { url = "https://files.pythonhosted.org/packages/4f/18/359da9723d3ec1235819cea0958bc417ce6a12699977920854b300ad4529/selectolax-0.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff1632c1767f6005adc2dff7b72ea9d0b6493702e86e04ee5cf934ab630172aa", size = 2090836, upload-time = "2025-12-06T12:35:22.421Z" }, + { url = "https://files.pythonhosted.org/packages/e2/20/199f2a05ca8dfd9bc146af03fbfaa1e2050601d3141267070a2a023ea68f/selectolax-0.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64a56dd91c22e809305e127bbd4cd75ad1016dd329a4e945a176c6f3376d00e2", size = 2248608, upload-time = "2025-12-06T12:35:23.946Z" }, + { url = "https://files.pythonhosted.org/packages/f9/67/261c06cdae29fad287008d39f51a80431f3fce66c2865b880e61d04cdfd2/selectolax-0.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:223471d2c9095406d69faf461aa217782575d62362d36809e350f6d3c2f69f4e", size = 2275653, upload-time = "2025-12-06T12:35:25.423Z" }, + { url = "https://files.pythonhosted.org/packages/10/80/f2519487a86b5366acadd9e07c212b38fb657bb62b9e01de9fb24c3ada27/selectolax-0.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c1043e61df29f4f8ef49f9f623b3d710f0c545d9a7939eee52c49987b06aef7", size = 2283495, upload-time = "2025-12-06T12:35:26.843Z" }, + { url = "https://files.pythonhosted.org/packages/66/cc/35a0fd896371e0b5a84365b03f88c7dbe8984862e34ce32baed81ee6f486/selectolax-0.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b323ad4ebcf2edad2488022402fbc73ee158ffe81607ec1ce5eb1039eab94086", size = 2300207, upload-time = "2025-12-06T12:35:28.23Z" }, +] + +[[package]] +name = "sentence-transformers" +version = "5.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, + { name = "numpy", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux')" }, + { name = "scikit-learn", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, + { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux')" }, + { name = "torch", version = "2.9.1", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, + { name = "torch", version = "2.9.1+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'linux'" }, + { name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "transformers", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/bc/0bc9c0ec1cf83ab2ec6e6f38667d167349b950fff6dd2086b79bd360eeca/sentence_transformers-5.2.2.tar.gz", hash = "sha256:7033ee0a24bc04c664fd490abf2ef194d387b3a58a97adcc528783ff505159fa", size = 381607, upload-time = "2026-01-27T11:11:02.658Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/21/7e925890636791386e81b52878134f114d63072e79fffe14cdcc5e7a5e6a/sentence_transformers-5.2.2-py3-none-any.whl", hash = "sha256:280ac54bffb84c110726b4d8848ba7b7c60813b9034547f8aea6e9a345cd1c23", size = 494106, upload-time = "2026-01-27T11:11:00.983Z" }, ] [[package]] @@ -3849,11 +5137,20 @@ wheels = [ [[package]] name = "setuptools" -version = "80.9.0" +version = "80.10.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/95/faf61eb8363f26aa7e1d762267a8d602a1b26d4f3a1e758e92cb3cb8b054/setuptools-80.10.2.tar.gz", hash = "sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70", size = 1200343, upload-time = "2026-01-25T22:38:17.252Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, + { url = "https://files.pythonhosted.org/packages/94/b8/f1f62a5e3c0ad2ff1d189590bfa4c46b4f3b6e49cef6f26c6ee4e575394d/setuptools-80.10.2-py3-none-any.whl", hash = "sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173", size = 1064234, upload-time = "2026-01-25T22:38:15.216Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, ] [[package]] @@ -3875,21 +5172,93 @@ wheels = [ ] [[package]] -name = "sqlparse" -version = "0.5.3" +name = "sqlalchemy" +version = "2.0.46" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e5/40/edede8dd6977b0d3da179a342c198ed100dd2aba4be081861ee5911e4da4/sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", size = 84999, upload-time = "2024-12-10T12:05:30.728Z" } +dependencies = [ + { name = "greenlet", marker = "(platform_machine == 'AMD64' and sys_platform == 'darwin') or (platform_machine == 'WIN32' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'darwin') or (platform_machine == 'amd64' and sys_platform == 'darwin') or (platform_machine == 'ppc64le' and sys_platform == 'darwin') or (platform_machine == 'win32' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'AMD64' and sys_platform == 'linux') or (platform_machine == 'WIN32' and sys_platform == 'linux') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'amd64' and sys_platform == 'linux') or (platform_machine == 'ppc64le' and sys_platform == 'linux') or (platform_machine == 'win32' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/aa/9ce0f3e7a9829ead5c8ce549392f33a12c4555a6c0609bb27d882e9c7ddf/sqlalchemy-2.0.46.tar.gz", hash = "sha256:cf36851ee7219c170bb0793dbc3da3e80c582e04a5437bc601bfe8c85c9216d7", size = 9865393, upload-time = "2026-01-21T18:03:45.119Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415, upload-time = "2024-12-10T12:05:27.824Z" }, + { url = "https://files.pythonhosted.org/packages/40/26/66ba59328dc25e523bfcb0f8db48bdebe2035e0159d600e1f01c0fc93967/sqlalchemy-2.0.46-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:895296687ad06dc9b11a024cf68e8d9d3943aa0b4964278d2553b86f1b267735", size = 2155051, upload-time = "2026-01-21T18:27:28.965Z" }, + { url = "https://files.pythonhosted.org/packages/21/cd/9336732941df972fbbfa394db9caa8bb0cf9fe03656ec728d12e9cbd6edc/sqlalchemy-2.0.46-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab65cb2885a9f80f979b85aa4e9c9165a31381ca322cbde7c638fe6eefd1ec39", size = 3234666, upload-time = "2026-01-21T18:32:28.72Z" }, + { url = "https://files.pythonhosted.org/packages/38/62/865ae8b739930ec433cd4123760bee7f8dafdc10abefd725a025604fb0de/sqlalchemy-2.0.46-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52fe29b3817bd191cc20bad564237c808967972c97fa683c04b28ec8979ae36f", size = 3232917, upload-time = "2026-01-21T18:44:54.064Z" }, + { url = "https://files.pythonhosted.org/packages/24/38/805904b911857f2b5e00fdea44e9570df62110f834378706939825579296/sqlalchemy-2.0.46-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:09168817d6c19954d3b7655da6ba87fcb3a62bb575fb396a81a8b6a9fadfe8b5", size = 3185790, upload-time = "2026-01-21T18:32:30.581Z" }, + { url = "https://files.pythonhosted.org/packages/69/4f/3260bb53aabd2d274856337456ea52f6a7eccf6cce208e558f870cec766b/sqlalchemy-2.0.46-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:be6c0466b4c25b44c5d82b0426b5501de3c424d7a3220e86cd32f319ba56798e", size = 3207206, upload-time = "2026-01-21T18:44:55.93Z" }, + { url = "https://files.pythonhosted.org/packages/69/ac/b42ad16800d0885105b59380ad69aad0cce5a65276e269ce2729a2343b6a/sqlalchemy-2.0.46-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:261c4b1f101b4a411154f1da2b76497d73abbfc42740029205d4d01fa1052684", size = 2154851, upload-time = "2026-01-21T18:27:30.54Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/d8710068cb79f64d002ebed62a7263c00c8fd95f4ebd4b5be8f7ca93f2bc/sqlalchemy-2.0.46-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:181903fe8c1b9082995325f1b2e84ac078b1189e2819380c2303a5f90e114a62", size = 3311241, upload-time = "2026-01-21T18:32:33.45Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/20c71487c7219ab3aa7421c7c62d93824c97c1460f2e8bb72404b0192d13/sqlalchemy-2.0.46-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:590be24e20e2424a4c3c1b0835e9405fa3d0af5823a1a9fc02e5dff56471515f", size = 3310741, upload-time = "2026-01-21T18:44:57.887Z" }, + { url = "https://files.pythonhosted.org/packages/65/80/d26d00b3b249ae000eee4db206fcfc564bf6ca5030e4747adf451f4b5108/sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7568fe771f974abadce52669ef3a03150ff03186d8eb82613bc8adc435a03f01", size = 3263116, upload-time = "2026-01-21T18:32:35.044Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/74dda7506640923821340541e8e45bd3edd8df78664f1f2e0aae8077192b/sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf7e1e78af38047e08836d33502c7a278915698b7c2145d045f780201679999", size = 3285327, upload-time = "2026-01-21T18:44:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/b6/35/d16bfa235c8b7caba3730bba43e20b1e376d2224f407c178fbf59559f23e/sqlalchemy-2.0.46-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a9a72b0da8387f15d5810f1facca8f879de9b85af8c645138cba61ea147968c", size = 2153405, upload-time = "2026-01-21T19:05:54.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/6c/3192e24486749862f495ddc6584ed730c0c994a67550ec395d872a2ad650/sqlalchemy-2.0.46-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2347c3f0efc4de367ba00218e0ae5c4ba2306e47216ef80d6e31761ac97cb0b9", size = 3334702, upload-time = "2026-01-21T18:46:45.384Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a2/b9f33c8d68a3747d972a0bb758c6b63691f8fb8a49014bc3379ba15d4274/sqlalchemy-2.0.46-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9094c8b3197db12aa6f05c51c05daaad0a92b8c9af5388569847b03b1007fb1b", size = 3347664, upload-time = "2026-01-21T18:40:09.979Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d2/3e59e2a91eaec9db7e8dc6b37b91489b5caeb054f670f32c95bcba98940f/sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37fee2164cf21417478b6a906adc1a91d69ae9aba8f9533e67ce882f4bb1de53", size = 3277372, upload-time = "2026-01-21T18:46:47.168Z" }, + { url = "https://files.pythonhosted.org/packages/dd/dd/67bc2e368b524e2192c3927b423798deda72c003e73a1e94c21e74b20a85/sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b1e14b2f6965a685c7128bd315e27387205429c2e339eeec55cb75ca4ab0ea2e", size = 3312425, upload-time = "2026-01-21T18:40:11.548Z" }, + { url = "https://files.pythonhosted.org/packages/b3/4b/fa7838fe20bb752810feed60e45625a9a8b0102c0c09971e2d1d95362992/sqlalchemy-2.0.46-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93a12da97cca70cea10d4b4fc602589c4511f96c1f8f6c11817620c021d21d00", size = 2150268, upload-time = "2026-01-21T19:05:56.621Z" }, + { url = "https://files.pythonhosted.org/packages/46/c1/b34dccd712e8ea846edf396e00973dda82d598cb93762e55e43e6835eba9/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af865c18752d416798dae13f83f38927c52f085c52e2f32b8ab0fef46fdd02c2", size = 3276511, upload-time = "2026-01-21T18:46:49.022Z" }, + { url = "https://files.pythonhosted.org/packages/96/48/a04d9c94753e5d5d096c628c82a98c4793b9c08ca0e7155c3eb7d7db9f24/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d679b5f318423eacb61f933a9a0f75535bfca7056daeadbf6bd5bcee6183aee", size = 3292881, upload-time = "2026-01-21T18:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/be/f4/06eda6e91476f90a7d8058f74311cb65a2fb68d988171aced81707189131/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64901e08c33462acc9ec3bad27fc7a5c2b6491665f2aa57564e57a4f5d7c52ad", size = 3224559, upload-time = "2026-01-21T18:46:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a2/d2af04095412ca6345ac22b33b89fe8d6f32a481e613ffcb2377d931d8d0/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8ac45e8f4eaac0f9f8043ea0e224158855c6a4329fd4ee37c45c61e3beb518e", size = 3262728, upload-time = "2026-01-21T18:40:14.883Z" }, + { url = "https://files.pythonhosted.org/packages/56/ba/9be4f97c7eb2b9d5544f2624adfc2853e796ed51d2bb8aec90bc94b7137e/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1e8cc6cc01da346dc92d9509a63033b9b1bda4fed7a7a7807ed385c7dccdc10", size = 3556533, upload-time = "2026-01-21T18:33:06.636Z" }, + { url = "https://files.pythonhosted.org/packages/20/a6/b1fc6634564dbb4415b7ed6419cdfeaadefd2c39cdab1e3aa07a5f2474c2/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96c7cca1a4babaaf3bfff3e4e606e38578856917e52f0384635a95b226c87764", size = 3523208, upload-time = "2026-01-21T18:45:08.436Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d8/41e0bdfc0f930ff236f86fccd12962d8fa03713f17ed57332d38af6a3782/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2a9f9aee38039cf4755891a1e50e1effcc42ea6ba053743f452c372c3152b1b", size = 3464292, upload-time = "2026-01-21T18:33:08.208Z" }, + { url = "https://files.pythonhosted.org/packages/f0/8b/9dcbec62d95bea85f5ecad9b8d65b78cc30fb0ffceeb3597961f3712549b/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db23b1bf8cfe1f7fda19018e7207b20cdb5168f83c437ff7e95d19e39289c447", size = 3473497, upload-time = "2026-01-21T18:45:10.552Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f8/5ecdfc73383ec496de038ed1614de9e740a82db9ad67e6e4514ebc0708a3/sqlalchemy-2.0.46-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:56bdd261bfd0895452006d5316cbf35739c53b9bb71a170a331fa0ea560b2ada", size = 2152079, upload-time = "2026-01-21T19:05:58.477Z" }, + { url = "https://files.pythonhosted.org/packages/e5/bf/eba3036be7663ce4d9c050bc3d63794dc29fbe01691f2bf5ccb64e048d20/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33e462154edb9493f6c3ad2125931e273bbd0be8ae53f3ecd1c161ea9a1dd366", size = 3272216, upload-time = "2026-01-21T18:46:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/05/45/1256fb597bb83b58a01ddb600c59fe6fdf0e5afe333f0456ed75c0f8d7bd/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcdce05f056622a632f1d44bb47dbdb677f58cad393612280406ce37530eb6d", size = 3277208, upload-time = "2026-01-21T18:40:16.38Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a0/2053b39e4e63b5d7ceb3372cface0859a067c1ddbd575ea7e9985716f771/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e84b09a9b0f19accedcbeff5c2caf36e0dd537341a33aad8d680336152dc34e", size = 3221994, upload-time = "2026-01-21T18:46:54.622Z" }, + { url = "https://files.pythonhosted.org/packages/1e/87/97713497d9502553c68f105a1cb62786ba1ee91dea3852ae4067ed956a50/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4f52f7291a92381e9b4de9050b0a65ce5d6a763333406861e33906b8aa4906bf", size = 3243990, upload-time = "2026-01-21T18:40:18.253Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f0/f96c8057c982d9d8a7a68f45d69c674bc6f78cad401099692fe16521640a/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4dafb537740eef640c4d6a7c254611dca2df87eaf6d14d6a5fca9d1f4c3fc0fa", size = 3561202, upload-time = "2026-01-21T18:33:10.337Z" }, + { url = "https://files.pythonhosted.org/packages/d7/53/3b37dda0a5b137f21ef608d8dfc77b08477bab0fe2ac9d3e0a66eaeab6fc/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42a1643dc5427b69aca967dae540a90b0fbf57eaf248f13a90ea5930e0966863", size = 3526296, upload-time = "2026-01-21T18:45:12.657Z" }, + { url = "https://files.pythonhosted.org/packages/33/75/f28622ba6dde79cd545055ea7bd4062dc934e0621f7b3be2891f8563f8de/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ff33c6e6ad006bbc0f34f5faf941cfc62c45841c64c0a058ac38c799f15b5ede", size = 3470008, upload-time = "2026-01-21T18:33:11.725Z" }, + { url = "https://files.pythonhosted.org/packages/a9/42/4afecbbc38d5e99b18acef446453c76eec6fbd03db0a457a12a056836e22/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:82ec52100ec1e6ec671563bbd02d7c7c8d0b9e71a0723c72f22ecf52d1755330", size = 3476137, upload-time = "2026-01-21T18:45:15.001Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a1/9c4efa03300926601c19c18582531b45aededfb961ab3c3585f1e24f120b/sqlalchemy-2.0.46-py3-none-any.whl", hash = "sha256:f9c11766e7e7c0a2767dda5acb006a118640c9fc0a4104214b96269bfb78399e", size = 1937882, upload-time = "2026-01-21T18:22:10.456Z" }, +] + +[package.optional-dependencies] +asyncio = [ + { name = "greenlet", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] + +[[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 = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "tenacity" +version = "9.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, ] [[package]] name = "termcolor" -version = "3.1.0" +version = "3.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/6c/3d75c196ac07ac8749600b60b03f4f6094d54e132c4d94ebac6ee0e0add0/termcolor-3.1.0.tar.gz", hash = "sha256:6a6dd7fbee581909eeec6a756cff1d7f7c376063b14e4a298dc4980309e55970", size = 14324, upload-time = "2025-04-30T11:37:53.791Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434, upload-time = "2025-12-29T12:55:21.882Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/bd/de8d508070629b6d84a30d01d57e4a65c69aa7f5abe7560b8fad3b50ea59/termcolor-3.1.0-py3-none-any.whl", hash = "sha256:591dd26b5c2ce03b9e43f391264626557873ce1d379019786f99b0c2bee140aa", size = 7684, upload-time = "2025-04-30T11:37:52.382Z" }, + { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" }, ] [[package]] @@ -3916,52 +5285,206 @@ wheels = [ ] [[package]] -name = "tomli" -version = "2.2.1" +name = "tiktoken" +version = "0.12.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +dependencies = [ + { name = "regex", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, - { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, - { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, - { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, - { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, - { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, - { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, - { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, - { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, - { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, - { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, - { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, - { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, - { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, - { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, - { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, - { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, - { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, - { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, - { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, - { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, - { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, - { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, - { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, + { url = "https://files.pythonhosted.org/packages/89/b3/2cb7c17b6c4cf8ca983204255d3f1d95eda7213e247e6947a0ee2c747a2c/tiktoken-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3de02f5a491cfd179aec916eddb70331814bd6bf764075d39e21d5862e533970", size = 1051991, upload-time = "2025-10-06T20:21:34.098Z" }, + { url = "https://files.pythonhosted.org/packages/27/0f/df139f1df5f6167194ee5ab24634582ba9a1b62c6b996472b0277ec80f66/tiktoken-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b6cfb6d9b7b54d20af21a912bfe63a2727d9cfa8fbda642fd8322c70340aad16", size = 995798, upload-time = "2025-10-06T20:21:35.579Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5d/26a691f28ab220d5edc09b9b787399b130f24327ef824de15e5d85ef21aa/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:cde24cdb1b8a08368f709124f15b36ab5524aac5fa830cc3fdce9c03d4fb8030", size = 1129865, upload-time = "2025-10-06T20:21:36.675Z" }, + { url = "https://files.pythonhosted.org/packages/b2/94/443fab3d4e5ebecac895712abd3849b8da93b7b7dec61c7db5c9c7ebe40c/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6de0da39f605992649b9cfa6f84071e3f9ef2cec458d08c5feb1b6f0ff62e134", size = 1152856, upload-time = "2025-10-06T20:21:37.873Z" }, + { url = "https://files.pythonhosted.org/packages/54/35/388f941251b2521c70dd4c5958e598ea6d2c88e28445d2fb8189eecc1dfc/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6faa0534e0eefbcafaccb75927a4a380463a2eaa7e26000f0173b920e98b720a", size = 1195308, upload-time = "2025-10-06T20:21:39.577Z" }, + { url = "https://files.pythonhosted.org/packages/f8/00/c6681c7f833dd410576183715a530437a9873fa910265817081f65f9105f/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:82991e04fc860afb933efb63957affc7ad54f83e2216fe7d319007dab1ba5892", size = 1255697, upload-time = "2025-10-06T20:21:41.154Z" }, + { url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", size = 1051565, upload-time = "2025-10-06T20:21:44.566Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284, upload-time = "2025-10-06T20:21:45.622Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201, upload-time = "2025-10-06T20:21:47.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444, upload-time = "2025-10-06T20:21:48.139Z" }, + { url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080, upload-time = "2025-10-06T20:21:49.246Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240, upload-time = "2025-10-06T20:21:50.274Z" }, + { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, + { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, + { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, + { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, + { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, + { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, + { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, + { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, + { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, + { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, + { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, + { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, + { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/84/04/655b79dbcc9b3ac5f1479f18e931a344af67e5b7d3b251d2dcdcd7558592/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4", size = 3282301, upload-time = "2026-01-05T10:40:34.858Z" }, + { url = "https://files.pythonhosted.org/packages/46/cd/e4851401f3d8f6f45d8480262ab6a5c8cb9c4302a790a35aa14eeed6d2fd/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c", size = 3161308, upload-time = "2026-01-05T10:40:40.737Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6e/55553992a89982cd12d4a66dddb5e02126c58677ea3931efcbe601d419db/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195", size = 3718964, upload-time = "2026-01-05T10:40:46.56Z" }, + { url = "https://files.pythonhosted.org/packages/59/8c/b1c87148aa15e099243ec9f0cf9d0e970cc2234c3257d558c25a2c5304e6/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5", size = 3373542, upload-time = "2026-01-05T10:40:52.803Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "torch" +version = "2.9.1" +source = { registry = "https://download.pytorch.org/whl/cpu" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", +] +dependencies = [ + { name = "filelock", marker = "sys_platform == 'darwin'" }, + { name = "fsspec", marker = "sys_platform == 'darwin'" }, + { name = "jinja2", marker = "sys_platform == 'darwin'" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and sys_platform == 'darwin'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and sys_platform == 'darwin'" }, + { name = "setuptools", marker = "python_full_version >= '3.12' and sys_platform == 'darwin'" }, + { name = "sympy", marker = "sys_platform == 'darwin'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin'" }, +] +wheels = [ + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:bf1e68cfb935ae2046374ff02a7aa73dda70351b46342846f557055b3a540bf0" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:a52952a8c90a422c14627ea99b9826b7557203b46b4d0772d3ca5c7699692425" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:287242dd1f830846098b5eca847f817aa5c6015ea57ab4c1287809efea7b77eb" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8924d10d36eac8fe0652a060a03fc2ae52980841850b9a1a2ddb0f27a4f181cd" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:bcee64ae7aa65876ceeae6dcaebe75109485b213528c74939602208a20706e3f" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:defadbeb055cfcf5def58f70937145aecbd7a4bc295238ded1d0e85ae2cf0e1d" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:886f84b181f766f53265ba0a1d503011e60f53fff9d569563ef94f24160e1072" }, +] + +[[package]] +name = "torch" +version = "2.9.1+cpu" +source = { registry = "https://download.pytorch.org/whl/cpu" } +resolution-markers = [ + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'linux'", +] +dependencies = [ + { name = "filelock", marker = "sys_platform == 'linux'" }, + { name = "fsspec", marker = "sys_platform == 'linux'" }, + { name = "jinja2", marker = "sys_platform == 'linux'" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and sys_platform == 'linux'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and sys_platform == 'linux'" }, + { name = "setuptools", marker = "python_full_version >= '3.12' and sys_platform == 'linux'" }, + { name = "sympy", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:10866c8a48c4aa5ae3f48538dc8a055b99c57d9c6af2bf5dd715374d9d6ddca3" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:7210713b66943fdbfcc237b2e782871b649123ac5d29f548ce8c85be4223ab38" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:0e611cfb16724e62252b67d31073bc5c490cb83e92ecdc1192762535e0e44487" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:3de2adb9b4443dc9210ef1f1b16da3647ace53553166d6360bbbd7edd6f16e4d" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3bf9b442a51a2948e41216a76d7ab00f0694cfcaaa51b6f9bcab57b7f89843e6" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7417d8c565f219d3455654cb431c6d892a3eb40246055e14d645422de13b9ea1" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:3e532e553b37ee859205a9b2d1c7977fd6922f53bbb1b9bfdd5bdc00d1a60ed4" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:39b3dff6d8fba240ae0d1bede4ca11c2531ae3b47329206512d99e17907ff74b" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:01b1884f724977a20c7da2f640f1c7b37f4a2c117a7f4a6c1c0424d14cb86322" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:031a597147fa81b1e6d79ccf1ad3ccc7fafa27941d6cf26ff5caaa384fb20e92" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:65010ab4aacce6c9a1ddfc935f986c003ca8638ded04348fd326c3e74346237c" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:88adf5157db5da1d54b1c9fe4a6c1d20ceef00e75d854e206a87dbf69e3037dc" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:3ac2b8df2c55430e836dcda31940d47f1f5f94b8731057b6f20300ebea394dd9" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:5b688445f928f13563b7418b17c57e97bf955ab559cf73cd8f2b961f8572dbb3" }, ] [[package]] name = "tornado" -version = "6.5.2" +version = "6.5.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/09/ce/1eb500eae19f4648281bb2186927bb062d2438c2e5093d1360391afd2f90/tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0", size = 510821, upload-time = "2025-08-08T18:27:00.78Z" } +sdist = { url = "https://files.pythonhosted.org/packages/37/1d/0a336abf618272d53f62ebe274f712e213f5a03c0b2339575430b8362ef2/tornado-6.5.4.tar.gz", hash = "sha256:a22fa9047405d03260b483980635f0b041989d8bcc9a313f8fe18b411d84b1d7", size = 513632, upload-time = "2025-12-15T19:21:03.836Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/48/6a7529df2c9cc12efd2e8f5dd219516184d703b34c06786809670df5b3bd/tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6", size = 442563, upload-time = "2025-08-08T18:26:42.945Z" }, - { url = "https://files.pythonhosted.org/packages/f2/b5/9b575a0ed3e50b00c40b08cbce82eb618229091d09f6d14bce80fc01cb0b/tornado-6.5.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef", size = 440729, upload-time = "2025-08-08T18:26:44.473Z" }, - { url = "https://files.pythonhosted.org/packages/1b/4e/619174f52b120efcf23633c817fd3fed867c30bff785e2cd5a53a70e483c/tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e", size = 444295, upload-time = "2025-08-08T18:26:46.021Z" }, - { url = "https://files.pythonhosted.org/packages/95/fa/87b41709552bbd393c85dd18e4e3499dcd8983f66e7972926db8d96aa065/tornado-6.5.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882", size = 443644, upload-time = "2025-08-08T18:26:47.625Z" }, - { url = "https://files.pythonhosted.org/packages/f9/41/fb15f06e33d7430ca89420283a8762a4e6b8025b800ea51796ab5e6d9559/tornado-6.5.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108", size = 443878, upload-time = "2025-08-08T18:26:50.599Z" }, - { url = "https://files.pythonhosted.org/packages/11/92/fe6d57da897776ad2e01e279170ea8ae726755b045fe5ac73b75357a5a3f/tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c", size = 444549, upload-time = "2025-08-08T18:26:51.864Z" }, - { url = "https://files.pythonhosted.org/packages/9b/02/c8f4f6c9204526daf3d760f4aa555a7a33ad0e60843eac025ccfd6ff4a93/tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4", size = 443973, upload-time = "2025-08-08T18:26:53.625Z" }, - { url = "https://files.pythonhosted.org/packages/ae/2d/f5f5707b655ce2317190183868cd0f6822a1121b4baeae509ceb9590d0bd/tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04", size = 443954, upload-time = "2025-08-08T18:26:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a9/e94a9d5224107d7ce3cc1fab8d5dc97f5ea351ccc6322ee4fb661da94e35/tornado-6.5.4-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d6241c1a16b1c9e4cc28148b1cda97dd1c6cb4fb7068ac1bedc610768dff0ba9", size = 443909, upload-time = "2025-12-15T19:20:48.382Z" }, + { url = "https://files.pythonhosted.org/packages/db/7e/f7b8d8c4453f305a51f80dbb49014257bb7d28ccb4bbb8dd328ea995ecad/tornado-6.5.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2d50f63dda1d2cac3ae1fa23d254e16b5e38153758470e9956cbc3d813d40843", size = 442163, upload-time = "2025-12-15T19:20:49.791Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b5/206f82d51e1bfa940ba366a8d2f83904b15942c45a78dd978b599870ab44/tornado-6.5.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cf66105dc6acb5af613c054955b8137e34a03698aa53272dbda4afe252be17", size = 445746, upload-time = "2025-12-15T19:20:51.491Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9d/1a3338e0bd30ada6ad4356c13a0a6c35fbc859063fa7eddb309183364ac1/tornado-6.5.4-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50ff0a58b0dc97939d29da29cd624da010e7f804746621c78d14b80238669335", size = 445083, upload-time = "2025-12-15T19:20:52.778Z" }, + { url = "https://files.pythonhosted.org/packages/50/d4/e51d52047e7eb9a582da59f32125d17c0482d065afd5d3bc435ff2120dc5/tornado-6.5.4-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5fb5e04efa54cf0baabdd10061eb4148e0be137166146fff835745f59ab9f7f", size = 445315, upload-time = "2025-12-15T19:20:53.996Z" }, + { url = "https://files.pythonhosted.org/packages/27/07/2273972f69ca63dbc139694a3fc4684edec3ea3f9efabf77ed32483b875c/tornado-6.5.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9c86b1643b33a4cd415f8d0fe53045f913bf07b4a3ef646b735a6a86047dda84", size = 446003, upload-time = "2025-12-15T19:20:56.101Z" }, + { url = "https://files.pythonhosted.org/packages/d1/83/41c52e47502bf7260044413b6770d1a48dda2f0246f95ee1384a3cd9c44a/tornado-6.5.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:6eb82872335a53dd063a4f10917b3efd28270b56a33db69009606a0312660a6f", size = 445412, upload-time = "2025-12-15T19:20:57.398Z" }, + { url = "https://files.pythonhosted.org/packages/10/c7/bc96917f06cbee182d44735d4ecde9c432e25b84f4c2086143013e7b9e52/tornado-6.5.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6076d5dda368c9328ff41ab5d9dd3608e695e8225d1cd0fd1e006f05da3635a8", size = 445392, upload-time = "2025-12-15T19:20:58.692Z" }, ] [[package]] @@ -3973,6 +5496,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, ] +[[package]] +name = "transformers" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "huggingface-hub", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, + { name = "numpy", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux')" }, + { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "regex", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "safetensors", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "tokenizers", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typer-slim", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/79/845941711811789c85fb7e2599cea425a14a07eda40f50896b9d3fda7492/transformers-5.0.0.tar.gz", hash = "sha256:5f5634efed6cf76ad068cc5834c7adbc32db78bbd6211fb70df2325a9c37dec8", size = 8424830, upload-time = "2026-01-26T10:46:46.813Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/f3/ac976fa8e305c9e49772527e09fbdc27cc6831b8a2f6b6063406626be5dd/transformers-5.0.0-py3-none-any.whl", hash = "sha256:587086f249ce64c817213cf36afdb318d087f790723e9b3d4500b97832afd52d", size = 10142091, upload-time = "2026-01-26T10:46:43.88Z" }, +] + [[package]] name = "twisted" version = "25.5.0" @@ -4002,11 +5547,45 @@ tls = [ name = "txaio" version = "25.9.2" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform == 'linux'", +] sdist = { url = "https://files.pythonhosted.org/packages/2b/20/2e7ccea9ab2dd824d0bd421d9364424afde3bb33863afb80cd9180335019/txaio-25.9.2.tar.gz", hash = "sha256:e42004a077c02eb5819ff004a4989e49db113836708430d59cb13d31bd309099", size = 50008, upload-time = "2025-09-25T22:21:07.958Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/2c/e276b80f73fc0411cefa1c1eeae6bc17955197a9c3e2b41b41f957322549/txaio-25.9.2-py3-none-any.whl", hash = "sha256:a23ce6e627d130e9b795cbdd46c9eaf8abd35e42d2401bb3fea63d38beda0991", size = 31293, upload-time = "2025-09-25T22:21:06.394Z" }, ] +[[package]] +name = "txaio" +version = "25.12.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'linux'", +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/67/ea9c9ddbaa3e0b4d53c91f8778a33e42045be352dc7200ed2b9aaa7dc229/txaio-25.12.2.tar.gz", hash = "sha256:9f232c21e12aa1ff52690e365b5a0ecfd42cc27a6ec86e1b92ece88f763f4b78", size = 117393, upload-time = "2025-12-09T15:03:26.527Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/05/bdb6318120cac9bf97779674f49035e0595d894b42d4c43b60637bafdb1f/txaio-25.12.2-py3-none-any.whl", hash = "sha256:5f6cd6c6b397fc3305790d15efd46a2d5b91cdbefa96543b4f8666aeb56ba026", size = 31208, upload-time = "2025-12-09T04:30:27.811Z" }, +] + +[[package]] +name = "typer-slim" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/d4/064570dec6358aa9049d4708e4a10407d74c99258f8b2136bb8702303f1a/typer_slim-0.21.1.tar.gz", hash = "sha256:73495dd08c2d0940d611c5a8c04e91c2a0a98600cbd4ee19192255a233b6dbfd", size = 110478, upload-time = "2026-01-06T11:21:11.176Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/0a/4aca634faf693e33004796b6cee0ae2e1dba375a800c16ab8d3eff4bb800/typer_slim-0.21.1-py3-none-any.whl", hash = "sha256:6e6c31047f171ac93cc5a973c9e617dbc5ab2bddc4d0a3135dc161b4e2020e0d", size = 47444, upload-time = "2026-01-06T11:21:12.441Z" }, +] + [[package]] name = "types-bleach" version = "6.3.0.20251115" @@ -4051,20 +5630,23 @@ wheels = [ [[package]] name = "types-docutils" -version = "0.22.2.20250924" +version = "0.22.3.20251115" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/6d/60326ba08f44629f778937d5021a342da996682d932261d48b4043c437f7/types_docutils-0.22.2.20250924.tar.gz", hash = "sha256:a13fb412676c164edec7c2f26fe52ab7b0b7c868168dacc4298f6a8069298f3d", size = 56679, upload-time = "2025-09-24T02:53:26.251Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/d7/576ec24bf61a280f571e1f22284793adc321610b9bcfba1bf468cf7b334f/types_docutils-0.22.3.20251115.tar.gz", hash = "sha256:0f79ea6a7bd4d12d56c9f824a0090ffae0ea4204203eb0006392906850913e16", size = 56828, upload-time = "2025-11-15T02:59:57.371Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/2b/844f3a6e972515ef0890fd8bf631890b6d74c8eacb1acbf31a72820c3b45/types_docutils-0.22.2.20250924-py3-none-any.whl", hash = "sha256:a6d52e21fa70998d34d13db6891ea35920bbb20f91459ca528a3845fd0b9ec03", size = 91873, upload-time = "2025-09-24T02:53:24.824Z" }, + { url = "https://files.pythonhosted.org/packages/9c/01/61ac9eb38f1f978b47443dc6fd2e0a3b0f647c2da741ddad30771f1b2b6f/types_docutils-0.22.3.20251115-py3-none-any.whl", hash = "sha256:c6e53715b65395d00a75a3a8a74e352c669bc63959e65a207dffaa22f4a2ad6e", size = 91951, upload-time = "2025-11-15T02:59:56.413Z" }, ] [[package]] name = "types-html5lib" -version = "1.1.11.20250917" +version = "1.1.11.20251117" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/68/4b/a970718e8bd9324ee8fb8eaf02ff069f6d03c20d4523bb4232892ecc3d06/types_html5lib-1.1.11.20250917.tar.gz", hash = "sha256:7b52743377f33f9b4fd7385afbd2d457b8864ee51f90ff2a795ad9e8c053373a", size = 16868, upload-time = "2025-09-17T02:47:41.18Z" } +dependencies = [ + { name = "types-webencodings", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/f3/d9a1bbba7b42b5558a3f9fe017d967f5338cf8108d35991d9b15fdea3e0d/types_html5lib-1.1.11.20251117.tar.gz", hash = "sha256:1a6a3ac5394aa12bf547fae5d5eff91dceec46b6d07c4367d9b39a37f42f201a", size = 18100, upload-time = "2025-11-17T03:08:00.78Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/8a/da91a9c64dcb5e69beb567519857411996d8ecae9f6f128bcef8260e7a8d/types_html5lib-1.1.11.20250917-py3-none-any.whl", hash = "sha256:b294fd06d60da205daeb2f615485ca4d475088d2eff1009cf427f4a80fcd5346", size = 22908, upload-time = "2025-09-17T02:47:40.39Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ab/f5606db367c1f57f7400d3cb3bead6665ee2509621439af1b29c35ef6f9e/types_html5lib-1.1.11.20251117-py3-none-any.whl", hash = "sha256:2a3fc935de788a4d2659f4535002a421e05bea5e172b649d33232e99d4272d08", size = 24302, upload-time = "2025-11-17T03:07:59.996Z" }, ] [[package]] @@ -4078,14 +5660,14 @@ wheels = [ [[package]] name = "types-pygments" -version = "2.19.0.20250809" +version = "2.19.0.20251121" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "types-docutils", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/51/1b/a6317763a8f2de01c425644273e5fbe3145d648a081f3bad590b3c34e000/types_pygments-2.19.0.20250809.tar.gz", hash = "sha256:01366fd93ef73c792e6ee16498d3abf7a184f1624b50b77f9506a47ed85974c2", size = 18454, upload-time = "2025-08-09T03:17:14.322Z" } +sdist = { url = "https://files.pythonhosted.org/packages/90/3b/cd650700ce9e26b56bd1a6aa4af397bbbc1784e22a03971cb633cdb0b601/types_pygments-2.19.0.20251121.tar.gz", hash = "sha256:eef114fde2ef6265365522045eac0f8354978a566852f69e75c531f0553822b1", size = 18590, upload-time = "2025-11-21T03:03:46.623Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/c4/d9f0923a941159664d664a0b714242fbbd745046db2d6c8de6fe1859c572/types_pygments-2.19.0.20250809-py3-none-any.whl", hash = "sha256:8e813e5fc25f741b81cadc1e181d402ebd288e34a9812862ddffee2f2b57db7c", size = 25407, upload-time = "2025-08-09T03:17:13.223Z" }, + { url = "https://files.pythonhosted.org/packages/99/8a/9244b21f1d60dcc62e261435d76b02f1853b4771663d7ec7d287e47a9ba9/types_pygments-2.19.0.20251121-py3-none-any.whl", hash = "sha256:cb3bfde34eb75b984c98fb733ce4f795213bd3378f855c32e75b49318371bb25", size = 25674, upload-time = "2025-11-21T03:03:45.72Z" }, ] [[package]] @@ -4103,20 +5685,20 @@ wheels = [ [[package]] name = "types-python-dateutil" -version = "2.9.0.20250822" +version = "2.9.0.20260124" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0c/0a/775f8551665992204c756be326f3575abba58c4a3a52eef9909ef4536428/types_python_dateutil-2.9.0.20250822.tar.gz", hash = "sha256:84c92c34bd8e68b117bff742bc00b692a1e8531262d4507b33afcc9f7716cd53", size = 16084, upload-time = "2025-08-22T03:02:00.613Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/41/4f8eb1ce08688a9e3e23709ed07089ccdeaf95b93745bfb768c6da71197d/types_python_dateutil-2.9.0.20260124.tar.gz", hash = "sha256:7d2db9f860820c30e5b8152bfe78dbdf795f7d1c6176057424e8b3fdd1f581af", size = 16596, upload-time = "2026-01-24T03:18:42.975Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/d9/a29dfa84363e88b053bf85a8b7f212a04f0d7343a4d24933baa45c06e08b/types_python_dateutil-2.9.0.20250822-py3-none-any.whl", hash = "sha256:849d52b737e10a6dc6621d2bd7940ec7c65fcb69e6aa2882acf4e56b2b508ddc", size = 17892, upload-time = "2025-08-22T03:01:59.436Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c2/aa5e3f4103cc8b1dcf92432415dde75d70021d634ecfd95b2e913cf43e17/types_python_dateutil-2.9.0.20260124-py3-none-any.whl", hash = "sha256:f802977ae08bf2260142e7ca1ab9d4403772a254409f7bbdf652229997124951", size = 18266, upload-time = "2026-01-24T03:18:42.155Z" }, ] [[package]] name = "types-pytz" -version = "2025.2.0.20250809" +version = "2025.2.0.20251108" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/07/e2/c774f754de26848f53f05defff5bb21dd9375a059d1ba5b5ea943cf8206e/types_pytz-2025.2.0.20250809.tar.gz", hash = "sha256:222e32e6a29bb28871f8834e8785e3801f2dc4441c715cd2082b271eecbe21e5", size = 10876, upload-time = "2025-08-09T03:14:17.453Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/ff/c047ddc68c803b46470a357454ef76f4acd8c1088f5cc4891cdd909bfcf6/types_pytz-2025.2.0.20251108.tar.gz", hash = "sha256:fca87917836ae843f07129567b74c1929f1870610681b4c92cb86a3df5817bdb", size = 10961, upload-time = "2025-11-08T02:55:57.001Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/d0/91c24fe54e565f2344d7a6821e6c6bb099841ef09007ea6321a0bac0f808/types_pytz-2025.2.0.20250809-py3-none-any.whl", hash = "sha256:4f55ed1b43e925cf851a756fe1707e0f5deeb1976e15bf844bcaa025e8fbd0db", size = 10095, upload-time = "2025-08-09T03:14:16.674Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c1/56ef16bf5dcd255155cc736d276efa6ae0a5c26fd685e28f0412a4013c01/types_pytz-2025.2.0.20251108-py3-none-any.whl", hash = "sha256:0f1c9792cab4eb0e46c52f8845c8f77cf1e313cb3d68bf826aa867fe4717d91c", size = 10116, upload-time = "2025-11-08T02:55:56.194Z" }, ] [[package]] @@ -4143,23 +5725,23 @@ wheels = [ [[package]] name = "types-requests" -version = "2.32.4.20250913" +version = "2.32.4.20260107" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/36/27/489922f4505975b11de2b5ad07b4fe1dca0bca9be81a703f26c5f3acfce5/types_requests-2.32.4.20250913.tar.gz", hash = "sha256:abd6d4f9ce3a9383f269775a9835a4c24e5cd6b9f647d64f88aa4613c33def5d", size = 23113, upload-time = "2025-09-13T02:40:02.309Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165, upload-time = "2026-01-07T03:20:54.091Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/20/9a227ea57c1285986c4cf78400d0a91615d25b24e257fd9e2969606bdfae/types_requests-2.32.4.20250913-py3-none-any.whl", hash = "sha256:78c9c1fffebbe0fa487a418e0fa5252017e9c60d1a2da394077f1780f655d7e1", size = 20658, upload-time = "2025-09-13T02:40:01.115Z" }, + { url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676, upload-time = "2026-01-07T03:20:52.929Z" }, ] [[package]] name = "types-setuptools" -version = "80.9.0.20250822" +version = "80.10.0.20260124" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/bd/1e5f949b7cb740c9f0feaac430e301b8f1c5f11a81e26324299ea671a237/types_setuptools-80.9.0.20250822.tar.gz", hash = "sha256:070ea7716968ec67a84c7f7768d9952ff24d28b65b6594797a464f1b3066f965", size = 41296, upload-time = "2025-08-22T03:02:08.771Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/7e/116539b9610585e34771611e33c88a4c706491fa3565500f5a63139f8731/types_setuptools-80.10.0.20260124.tar.gz", hash = "sha256:1b86d9f0368858663276a0cbe5fe5a9722caf94b5acde8aba0399a6e90680f20", size = 43299, upload-time = "2026-01-24T03:18:39.527Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/2d/475bf15c1cdc172e7a0d665b6e373ebfb1e9bf734d3f2f543d668b07a142/types_setuptools-80.9.0.20250822-py3-none-any.whl", hash = "sha256:53bf881cb9d7e46ed12c76ef76c0aaf28cfe6211d3fab12e0b83620b1a8642c3", size = 63179, upload-time = "2025-08-22T03:02:07.643Z" }, + { url = "https://files.pythonhosted.org/packages/2b/7f/016dc5cc718ec6ccaa84fb73ed409ef1c261793fd5e637cdfaa18beb40a9/types_setuptools-80.10.0.20260124-py3-none-any.whl", hash = "sha256:efed7e044f01adb9c2806c7a8e1b6aa3656b8e382379b53d5f26ee3db24d4c01", size = 64333, upload-time = "2026-01-24T03:18:38.344Z" }, ] [[package]] @@ -4174,6 +5756,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3f/13/3ff0781445d7c12730befce0fddbbc7a76e56eb0e7029446f2853238360a/types_tqdm-4.67.0.20250809-py3-none-any.whl", hash = "sha256:1a73053b31fcabf3c1f3e2a9d5ecdba0f301bde47a418cd0e0bdf774827c5c57", size = 24020, upload-time = "2025-08-09T03:17:42.453Z" }, ] +[[package]] +name = "types-webencodings" +version = "0.5.0.20251108" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/d6/75e381959a2706644f02f7527d264de3216cf6ed333f98eff95954d78e07/types_webencodings-0.5.0.20251108.tar.gz", hash = "sha256:2378e2ceccced3d41bb5e21387586e7b5305e11519fc6b0659c629f23b2e5de4", size = 7470, upload-time = "2025-11-08T02:56:00.132Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/4e/8fcf33e193ce4af03c19d0e08483cf5f0838e883f800909c6bc61cb361be/types_webencodings-0.5.0.20251108-py3-none-any.whl", hash = "sha256:e21f81ff750795faffddaffd70a3d8bfff77d006f22c27e393eb7812586249d8", size = 8715, upload-time = "2025-11-08T02:55:59.456Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -4184,12 +5775,37 @@ wheels = [ ] [[package]] -name = "tzdata" -version = "2025.2" +name = "typing-inspect" +version = "0.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +dependencies = [ + { name = "mypy-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825, upload-time = "2023-05-24T20:25:47.612Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, + { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827, upload-time = "2023-05-24T20:25:45.287Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, ] [[package]] @@ -4201,6 +5817,76 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" }, ] +[[package]] +name = "u-msgpack-python" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/9d/a40411a475e7d4838994b7f6bcc6bfca9acc5b119ce3a7503608c4428b49/u-msgpack-python-2.8.0.tar.gz", hash = "sha256:b801a83d6ed75e6df41e44518b4f2a9c221dc2da4bcd5380e3a0feda520bc61a", size = 18167, upload-time = "2023-05-18T09:28:12.187Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/5e/512aeb40fd819f4660d00f96f5c7371ee36fc8c6b605128c5ee59e0b28c6/u_msgpack_python-2.8.0-py2.py3-none-any.whl", hash = "sha256:1d853d33e78b72c4228a2025b4db28cda81214076e5b0422ed0ae1b1b2bb586a", size = 10590, upload-time = "2023-05-18T09:28:10.323Z" }, +] + +[[package]] +name = "ujson" +version = "5.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/d9/3f17e3c5773fb4941c68d9a37a47b1a79c9649d6c56aefbed87cc409d18a/ujson-5.11.0.tar.gz", hash = "sha256:e204ae6f909f099ba6b6b942131cee359ddda2b6e4ea39c12eb8b991fe2010e0", size = 7156583, upload-time = "2025-08-20T11:57:02.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/0c/8bf7a4fabfd01c7eed92d9b290930ce6d14910dec708e73538baa38885d1/ujson-5.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:446e8c11c06048611c9d29ef1237065de0af07cabdd97e6b5b527b957692ec25", size = 55248, upload-time = "2025-08-20T11:55:02.368Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2e/eeab0b8b641817031ede4f790db4c4942df44a12f44d72b3954f39c6a115/ujson-5.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:16ccb973b7ada0455201808ff11d48fe9c3f034a6ab5bd93b944443c88299f89", size = 53157, upload-time = "2025-08-20T11:55:04.012Z" }, + { url = "https://files.pythonhosted.org/packages/21/1b/a4e7a41870797633423ea79618526747353fd7be9191f3acfbdee0bf264b/ujson-5.11.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3134b783ab314d2298d58cda7e47e7a0f7f71fc6ade6ac86d5dbeaf4b9770fa6", size = 57657, upload-time = "2025-08-20T11:55:05.169Z" }, + { url = "https://files.pythonhosted.org/packages/94/ae/4e0d91b8f6db7c9b76423b3649612189506d5a06ddd3b6334b6d37f77a01/ujson-5.11.0-cp310-cp310-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:185f93ebccffebc8baf8302c869fac70dd5dd78694f3b875d03a31b03b062cdb", size = 59780, upload-time = "2025-08-20T11:55:06.325Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cc/46b124c2697ca2da7c65c4931ed3cb670646978157aa57a7a60f741c530f/ujson-5.11.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d06e87eded62ff0e5f5178c916337d2262fdbc03b31688142a3433eabb6511db", size = 57307, upload-time = "2025-08-20T11:55:07.493Z" }, + { url = "https://files.pythonhosted.org/packages/39/eb/20dd1282bc85dede2f1c62c45b4040bc4c389c80a05983515ab99771bca7/ujson-5.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:181fb5b15703a8b9370b25345d2a1fd1359f0f18776b3643d24e13ed9c036d4c", size = 1036369, upload-time = "2025-08-20T11:55:09.192Z" }, + { url = "https://files.pythonhosted.org/packages/64/a2/80072439065d493e3a4b1fbeec991724419a1b4c232e2d1147d257cac193/ujson-5.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a4df61a6df0a4a8eb5b9b1ffd673429811f50b235539dac586bb7e9e91994138", size = 1195738, upload-time = "2025-08-20T11:55:11.402Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7e/d77f9e9c039d58299c350c978e086a804d1fceae4fd4a1cc6e8d0133f838/ujson-5.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6eff24e1abd79e0ec6d7eae651dd675ddbc41f9e43e29ef81e16b421da896915", size = 1088718, upload-time = "2025-08-20T11:55:13.297Z" }, + { url = "https://files.pythonhosted.org/packages/da/ea/80346b826349d60ca4d612a47cdf3533694e49b45e9d1c07071bb867a184/ujson-5.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d7c46cb0fe5e7056b9acb748a4c35aa1b428025853032540bb7e41f46767321f", size = 55248, upload-time = "2025-08-20T11:55:19.033Z" }, + { url = "https://files.pythonhosted.org/packages/57/df/b53e747562c89515e18156513cc7c8ced2e5e3fd6c654acaa8752ffd7cd9/ujson-5.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8951bb7a505ab2a700e26f691bdfacf395bc7e3111e3416d325b513eea03a58", size = 53156, upload-time = "2025-08-20T11:55:20.174Z" }, + { url = "https://files.pythonhosted.org/packages/41/b8/ab67ec8c01b8a3721fd13e5cb9d85ab2a6066a3a5e9148d661a6870d6293/ujson-5.11.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:952c0be400229940248c0f5356514123d428cba1946af6fa2bbd7503395fef26", size = 57657, upload-time = "2025-08-20T11:55:21.296Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c7/fb84f27cd80a2c7e2d3c6012367aecade0da936790429801803fa8d4bffc/ujson-5.11.0-cp311-cp311-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:94fcae844f1e302f6f8095c5d1c45a2f0bfb928cccf9f1b99e3ace634b980a2a", size = 59779, upload-time = "2025-08-20T11:55:22.772Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/48706f7c1e917ecb97ddcfb7b1d756040b86ed38290e28579d63bd3fcc48/ujson-5.11.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e0ec1646db172beb8d3df4c32a9d78015e671d2000af548252769e33079d9a6", size = 57284, upload-time = "2025-08-20T11:55:24.01Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ce/48877c6eb4afddfd6bd1db6be34456538c07ca2d6ed233d3f6c6efc2efe8/ujson-5.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:da473b23e3a54448b008d33f742bcd6d5fb2a897e42d1fc6e7bf306ea5d18b1b", size = 1036395, upload-time = "2025-08-20T11:55:25.725Z" }, + { url = "https://files.pythonhosted.org/packages/8b/7a/2c20dc97ad70cd7c31ad0596ba8e2cf8794d77191ba4d1e0bded69865477/ujson-5.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:aa6b3d4f1c0d3f82930f4cbd7fe46d905a4a9205a7c13279789c1263faf06dba", size = 1195731, upload-time = "2025-08-20T11:55:27.915Z" }, + { url = "https://files.pythonhosted.org/packages/15/f5/ca454f2f6a2c840394b6f162fff2801450803f4ff56c7af8ce37640b8a2a/ujson-5.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4843f3ab4fe1cc596bb7e02228ef4c25d35b4bb0809d6a260852a4bfcab37ba3", size = 1088710, upload-time = "2025-08-20T11:55:29.426Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ef/a9cb1fce38f699123ff012161599fb9f2ff3f8d482b4b18c43a2dc35073f/ujson-5.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7895f0d2d53bd6aea11743bd56e3cb82d729980636cd0ed9b89418bf66591702", size = 55434, upload-time = "2025-08-20T11:55:34.987Z" }, + { url = "https://files.pythonhosted.org/packages/b1/05/dba51a00eb30bd947791b173766cbed3492269c150a7771d2750000c965f/ujson-5.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12b5e7e22a1fe01058000d1b317d3b65cc3daf61bd2ea7a2b76721fe160fa74d", size = 53190, upload-time = "2025-08-20T11:55:36.384Z" }, + { url = "https://files.pythonhosted.org/packages/03/3c/fd11a224f73fbffa299fb9644e425f38b38b30231f7923a088dd513aabb4/ujson-5.11.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0180a480a7d099082501cad1fe85252e4d4bf926b40960fb3d9e87a3a6fbbc80", size = 57600, upload-time = "2025-08-20T11:55:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/55/b9/405103cae24899df688a3431c776e00528bd4799e7d68820e7ebcf824f92/ujson-5.11.0-cp312-cp312-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:fa79fdb47701942c2132a9dd2297a1a85941d966d8c87bfd9e29b0cf423f26cc", size = 59791, upload-time = "2025-08-20T11:55:38.877Z" }, + { url = "https://files.pythonhosted.org/packages/17/7b/2dcbc2bbfdbf68f2368fb21ab0f6735e872290bb604c75f6e06b81edcb3f/ujson-5.11.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8254e858437c00f17cb72e7a644fc42dad0ebb21ea981b71df6e84b1072aaa7c", size = 57356, upload-time = "2025-08-20T11:55:40.036Z" }, + { url = "https://files.pythonhosted.org/packages/d1/71/fea2ca18986a366c750767b694430d5ded6b20b6985fddca72f74af38a4c/ujson-5.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1aa8a2ab482f09f6c10fba37112af5f957689a79ea598399c85009f2f29898b5", size = 1036313, upload-time = "2025-08-20T11:55:41.408Z" }, + { url = "https://files.pythonhosted.org/packages/a3/bb/d4220bd7532eac6288d8115db51710fa2d7d271250797b0bfba9f1e755af/ujson-5.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a638425d3c6eed0318df663df44480f4a40dc87cc7c6da44d221418312f6413b", size = 1195782, upload-time = "2025-08-20T11:55:43.357Z" }, + { url = "https://files.pythonhosted.org/packages/80/47/226e540aa38878ce1194454385701d82df538ccb5ff8db2cf1641dde849a/ujson-5.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7e3cff632c1d78023b15f7e3a81c3745cd3f94c044d1e8fa8efbd6b161997bbc", size = 1088817, upload-time = "2025-08-20T11:55:45.262Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ec/2de9dd371d52c377abc05d2b725645326c4562fc87296a8907c7bcdf2db7/ujson-5.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:109f59885041b14ee9569bf0bb3f98579c3fa0652317b355669939e5fc5ede53", size = 55435, upload-time = "2025-08-20T11:55:50.243Z" }, + { url = "https://files.pythonhosted.org/packages/5b/a4/f611f816eac3a581d8a4372f6967c3ed41eddbae4008d1d77f223f1a4e0a/ujson-5.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a31c6b8004438e8c20fc55ac1c0e07dad42941db24176fe9acf2815971f8e752", size = 53193, upload-time = "2025-08-20T11:55:51.373Z" }, + { url = "https://files.pythonhosted.org/packages/e9/c5/c161940967184de96f5cbbbcce45b562a4bf851d60f4c677704b1770136d/ujson-5.11.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78c684fb21255b9b90320ba7e199780f653e03f6c2528663768965f4126a5b50", size = 57603, upload-time = "2025-08-20T11:55:52.583Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d6/c7b2444238f5b2e2d0e3dab300b9ddc3606e4b1f0e4bed5a48157cebc792/ujson-5.11.0-cp313-cp313-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:4c9f5d6a27d035dd90a146f7761c2272cf7103de5127c9ab9c4cd39ea61e878a", size = 59794, upload-time = "2025-08-20T11:55:53.69Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a3/292551f936d3d02d9af148f53e1bc04306b00a7cf1fcbb86fa0d1c887242/ujson-5.11.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:837da4d27fed5fdc1b630bd18f519744b23a0b5ada1bbde1a36ba463f2900c03", size = 57363, upload-time = "2025-08-20T11:55:54.843Z" }, + { url = "https://files.pythonhosted.org/packages/90/a6/82cfa70448831b1a9e73f882225980b5c689bf539ec6400b31656a60ea46/ujson-5.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:787aff4a84da301b7f3bac09bc696e2e5670df829c6f8ecf39916b4e7e24e701", size = 1036311, upload-time = "2025-08-20T11:55:56.197Z" }, + { url = "https://files.pythonhosted.org/packages/84/5c/96e2266be50f21e9b27acaee8ca8f23ea0b85cb998c33d4f53147687839b/ujson-5.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6dd703c3e86dc6f7044c5ac0b3ae079ed96bf297974598116aa5fb7f655c3a60", size = 1195783, upload-time = "2025-08-20T11:55:58.081Z" }, + { url = "https://files.pythonhosted.org/packages/8d/20/78abe3d808cf3bb3e76f71fca46cd208317bf461c905d79f0d26b9df20f1/ujson-5.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3772e4fe6b0c1e025ba3c50841a0ca4786825a4894c8411bf8d3afe3a8061328", size = 1088822, upload-time = "2025-08-20T11:55:59.469Z" }, + { url = "https://files.pythonhosted.org/packages/28/08/4518146f4984d112764b1dfa6fb7bad691c44a401adadaa5e23ccd930053/ujson-5.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:65724738c73645db88f70ba1f2e6fb678f913281804d5da2fd02c8c5839af302", size = 55462, upload-time = "2025-08-20T11:56:04.873Z" }, + { url = "https://files.pythonhosted.org/packages/29/37/2107b9a62168867a692654d8766b81bd2fd1e1ba13e2ec90555861e02b0c/ujson-5.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29113c003ca33ab71b1b480bde952fbab2a0b6b03a4ee4c3d71687cdcbd1a29d", size = 53246, upload-time = "2025-08-20T11:56:06.054Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f8/25583c70f83788edbe3ca62ce6c1b79eff465d78dec5eb2b2b56b3e98b33/ujson-5.11.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c44c703842024d796b4c78542a6fcd5c3cb948b9fc2a73ee65b9c86a22ee3638", size = 57631, upload-time = "2025-08-20T11:56:07.374Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ca/19b3a632933a09d696f10dc1b0dfa1d692e65ad507d12340116ce4f67967/ujson-5.11.0-cp314-cp314-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:e750c436fb90edf85585f5c62a35b35082502383840962c6983403d1bd96a02c", size = 59877, upload-time = "2025-08-20T11:56:08.534Z" }, + { url = "https://files.pythonhosted.org/packages/55/7a/4572af5324ad4b2bfdd2321e898a527050290147b4ea337a79a0e4e87ec7/ujson-5.11.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f278b31a7c52eb0947b2db55a5133fbc46b6f0ef49972cd1a80843b72e135aba", size = 57363, upload-time = "2025-08-20T11:56:09.758Z" }, + { url = "https://files.pythonhosted.org/packages/7b/71/a2b8c19cf4e1efe53cf439cdf7198ac60ae15471d2f1040b490c1f0f831f/ujson-5.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ab2cb8351d976e788669c8281465d44d4e94413718af497b4e7342d7b2f78018", size = 1036394, upload-time = "2025-08-20T11:56:11.168Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3e/7b98668cba3bb3735929c31b999b374ebc02c19dfa98dfebaeeb5c8597ca/ujson-5.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:090b4d11b380ae25453100b722d0609d5051ffe98f80ec52853ccf8249dfd840", size = 1195837, upload-time = "2025-08-20T11:56:12.6Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ea/8870f208c20b43571a5c409ebb2fe9b9dba5f494e9e60f9314ac01ea8f78/ujson-5.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:80017e870d882d5517d28995b62e4e518a894f932f1e242cbc802a2fd64d365c", size = 1088837, upload-time = "2025-08-20T11:56:14.15Z" }, + { url = "https://files.pythonhosted.org/packages/6e/cd/e9809b064a89fe5c4184649adeb13c1b98652db3f8518980b04227358574/ujson-5.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:de6e88f62796372fba1de973c11138f197d3e0e1d80bcb2b8aae1e826096d433", size = 55759, upload-time = "2025-08-20T11:56:18.882Z" }, + { url = "https://files.pythonhosted.org/packages/1b/be/ae26a6321179ebbb3a2e2685b9007c71bcda41ad7a77bbbe164005e956fc/ujson-5.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e56ef8066f11b80d620985ae36869a3ff7e4b74c3b6129182ec5d1df0255f3", size = 53634, upload-time = "2025-08-20T11:56:20.012Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e9/fb4a220ee6939db099f4cfeeae796ecb91e7584ad4d445d4ca7f994a9135/ujson-5.11.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a325fd2c3a056cf6c8e023f74a0c478dd282a93141356ae7f16d5309f5ff823", size = 58547, upload-time = "2025-08-20T11:56:21.175Z" }, + { url = "https://files.pythonhosted.org/packages/bd/f8/fc4b952b8f5fea09ea3397a0bd0ad019e474b204cabcb947cead5d4d1ffc/ujson-5.11.0-cp314-cp314t-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:a0af6574fc1d9d53f4ff371f58c96673e6d988ed2b5bf666a6143c782fa007e9", size = 60489, upload-time = "2025-08-20T11:56:22.342Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e5/af5491dfda4f8b77e24cf3da68ee0d1552f99a13e5c622f4cef1380925c3/ujson-5.11.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10f29e71ecf4ecd93a6610bd8efa8e7b6467454a363c3d6416db65de883eb076", size = 58035, upload-time = "2025-08-20T11:56:23.92Z" }, + { url = "https://files.pythonhosted.org/packages/c4/09/0945349dd41f25cc8c38d78ace49f14c5052c5bbb7257d2f466fa7bdb533/ujson-5.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1a0a9b76a89827a592656fe12e000cf4f12da9692f51a841a4a07aa4c7ecc41c", size = 1037212, upload-time = "2025-08-20T11:56:25.274Z" }, + { url = "https://files.pythonhosted.org/packages/49/44/8e04496acb3d5a1cbee3a54828d9652f67a37523efa3d3b18a347339680a/ujson-5.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b16930f6a0753cdc7d637b33b4e8f10d5e351e1fb83872ba6375f1e87be39746", size = 1196500, upload-time = "2025-08-20T11:56:27.517Z" }, + { url = "https://files.pythonhosted.org/packages/64/ae/4bc825860d679a0f208a19af2f39206dfd804ace2403330fdc3170334a2f/ujson-5.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:04c41afc195fd477a59db3a84d5b83a871bd648ef371cf8c6f43072d89144eef", size = 1089487, upload-time = "2025-08-20T11:56:29.07Z" }, + { url = "https://files.pythonhosted.org/packages/50/17/30275aa2933430d8c0c4ead951cc4fdb922f575a349aa0b48a6f35449e97/ujson-5.11.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:abae0fb58cc820092a0e9e8ba0051ac4583958495bfa5262a12f628249e3b362", size = 51206, upload-time = "2025-08-20T11:56:48.797Z" }, + { url = "https://files.pythonhosted.org/packages/c3/15/42b3924258eac2551f8f33fa4e35da20a06a53857ccf3d4deb5e5d7c0b6c/ujson-5.11.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fac6c0649d6b7c3682a0a6e18d3de6857977378dce8d419f57a0b20e3d775b39", size = 48907, upload-time = "2025-08-20T11:56:50.136Z" }, + { url = "https://files.pythonhosted.org/packages/94/7e/0519ff7955aba581d1fe1fb1ca0e452471250455d182f686db5ac9e46119/ujson-5.11.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b42c115c7c6012506e8168315150d1e3f76e7ba0f4f95616f4ee599a1372bbc", size = 50319, upload-time = "2025-08-20T11:56:51.63Z" }, + { url = "https://files.pythonhosted.org/packages/74/cf/209d90506b7d6c5873f82c5a226d7aad1a1da153364e9ebf61eff0740c33/ujson-5.11.0-pp311-pypy311_pp73-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:86baf341d90b566d61a394869ce77188cc8668f76d7bb2c311d77a00f4bdf844", size = 56584, upload-time = "2025-08-20T11:56:52.89Z" }, + { url = "https://files.pythonhosted.org/packages/e9/97/bd939bb76943cb0e1d2b692d7e68629f51c711ef60425fa5bb6968037ecd/ujson-5.11.0-pp311-pypy311_pp73-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4598bf3965fc1a936bd84034312bcbe00ba87880ef1ee33e33c1e88f2c398b49", size = 51588, upload-time = "2025-08-20T11:56:54.054Z" }, +] + [[package]] name = "uritemplate" version = "4.2.0" @@ -4212,66 +5898,78 @@ wheels = [ [[package]] name = "urllib3" -version = "2.5.0" +version = "2.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] [[package]] name = "uv" -version = "0.9.3" +version = "0.9.27" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/dc/4a0e01bcb38c756130c8118a8561d4bf0a0bb685b70ad11e8f40a0cbfa10/uv-0.9.3.tar.gz", hash = "sha256:a290a1a8783bf04ca2d4a63d5d72191b255dfa4cc3426a9c9b5af4da49a7b5af", size = 3699151, upload-time = "2025-10-15T15:20:15.498Z" } +sdist = { url = "https://files.pythonhosted.org/packages/92/70/611bcee4385b7aa00cf7acf29bc51854c365ee09ddb2cdb14b07a36b4db8/uv-0.9.27.tar.gz", hash = "sha256:9147862902b5d40894f78339803225e39b0535c4c04537188de160eb7635e46b", size = 3830404, upload-time = "2026-01-26T23:27:43.442Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/ad/194e550062e4b3b9a74cb06401dc0afd83490af8e2ec0f414737868d0262/uv-0.9.3-py3-none-linux_armv6l.whl", hash = "sha256:7b1b79dd435ade1de97c6f0b8b90811a6ccf1bd0bdd70f4d034a93696cf0d0a3", size = 20584531, upload-time = "2025-10-15T15:19:14.26Z" }, - { url = "https://files.pythonhosted.org/packages/d0/1a/8e68d0020c29f6f329a265773c23b0c01e002794ea884b8bdbd594c7ea97/uv-0.9.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:596a982c5a061d58412824a2ebe2960b52db23f1b1658083ba9c0e7ae390308a", size = 19577639, upload-time = "2025-10-15T15:19:18.668Z" }, - { url = "https://files.pythonhosted.org/packages/16/25/6df8be6cd549200e80d19374579689fda39b18735afde841345284fb113d/uv-0.9.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:741e80c4230e1b9a5d0869aca2fb082b3832b251ef61537bc9278364b8e74df2", size = 18210073, upload-time = "2025-10-15T15:19:22.16Z" }, - { url = "https://files.pythonhosted.org/packages/07/19/bb8aa38b4441e03c742e71a31779f91b42d9db255ede66f80cdfdb672618/uv-0.9.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:406ab1a8b313b4b3cf67ad747fb8713a0c0cf3d3daf11942b5a4e49f60882339", size = 20022427, upload-time = "2025-10-15T15:19:25.453Z" }, - { url = "https://files.pythonhosted.org/packages/40/15/f190004dd855b443cfc1cc36edb1765e6cd0b6b340a50bb8015531dfff2e/uv-0.9.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:73dbd91581a82e53bb4352243d7bc491cf78ac3ebb951d95bb8b7964e5ee0659", size = 20150307, upload-time = "2025-10-15T15:19:28.99Z" }, - { url = "https://files.pythonhosted.org/packages/dd/55/553e90bc2b881f168de9cd57f9e0b0464304a12aee289e71b54c42559e1a/uv-0.9.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:970ac8428678b92eddb990dc132d75e893234bb1b809e87b90a4acd96bb054e4", size = 21152942, upload-time = "2025-10-15T15:19:32.461Z" }, - { url = "https://files.pythonhosted.org/packages/30/fb/768647a31622c2c1da7a9394eaab937e2e7ca0e8c983ca3d1918ec623620/uv-0.9.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:32694e64d6e4ea44b647866c4240659f3964b0317e98f539b73915dbcca7d973", size = 22632018, upload-time = "2025-10-15T15:19:36.091Z" }, - { url = "https://files.pythonhosted.org/packages/98/92/66d660414aed123686bf9a2a3ea167967b847b97c08cacd13d6b2b6d1267/uv-0.9.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36df7eb562b103e3263a03df1b04cee91ee52af88d005d07ee494137c7a5782a", size = 22241856, upload-time = "2025-10-15T15:19:39.662Z" }, - { url = "https://files.pythonhosted.org/packages/0d/99/af8b0cd2c958e8cb9c20e6e2d417de9476338a2b155643492a8ee2baf077/uv-0.9.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:117c5921bcfdac04b88211ee830c6c7e412eaf93a34aa3ad4bb3230bc61646aa", size = 21391699, upload-time = "2025-10-15T15:19:42.933Z" }, - { url = "https://files.pythonhosted.org/packages/82/45/488417c6c0127c00bcdfac3556ae2ea0597df8245fe5f9bcfda35ebdbe85/uv-0.9.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73ae4bbc7d555ba1738da08c64b55f21ab0ea0ff85636708cebaf460d98a440d", size = 21318117, upload-time = "2025-10-15T15:19:46.341Z" }, - { url = "https://files.pythonhosted.org/packages/1d/62/508c20f8dbdd2342cc4821ab6f41e29a9b36e2a469dfb5cbbd042e15218c/uv-0.9.3-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:2e75ce14c9375e7e99422d5383fb415e8f0eab9ebdcdfba45756749dee0c42b2", size = 20132999, upload-time = "2025-10-15T15:19:49.578Z" }, - { url = "https://files.pythonhosted.org/packages/2d/fc/ea673d1c68915ea53f1ab7e134b330a2351c543f06e9d0009b4f27cc3057/uv-0.9.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:71faefa9805ccf3f2db645ae27c9e719e47aaa8781e43dfa3760d993aadecb8c", size = 21223810, upload-time = "2025-10-15T15:19:52.711Z" }, - { url = "https://files.pythonhosted.org/packages/97/1f/af8ced7f6c8f6af887c52369088058ecae92ff21819e385531023f9ec923/uv-0.9.3-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:8844103e0b4074821fb2814abf30af59d66f33b6ca1bb2276dd37d4e5997c292", size = 20156823, upload-time = "2025-10-15T15:19:56.552Z" }, - { url = "https://files.pythonhosted.org/packages/05/2d/e1d8f74ec9d95daf57f3c53083c98a2145ee895a4f8502c61c9013c9bf5a/uv-0.9.3-py3-none-musllinux_1_1_i686.whl", hash = "sha256:214bb2fb4d87a55e2ba2bc038a8b646a24ec66980528d2ed1e6e7d0612d246e1", size = 20564971, upload-time = "2025-10-15T15:20:00.012Z" }, - { url = "https://files.pythonhosted.org/packages/bc/04/4aaf90e031f0735795407a208c9528f85b0b27b63409abe4ee3bee0d4527/uv-0.9.3-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:ccf4cd2e1907fb011764f6f4bc0e514c500e8d300288f04a4680400d5aa205ec", size = 21506573, upload-time = "2025-10-15T15:20:03.304Z" }, + { url = "https://files.pythonhosted.org/packages/53/f1/9e9afb9fadb73f7914a0fc63b6f9d182b6fe6b1e55deb7cbdc29ff31299f/uv-0.9.27-py3-none-linux_armv6l.whl", hash = "sha256:ce3f16e66a96dcdc63f6ada9f7747686930986d2df104a9dd2d09664b2d870c8", size = 22011502, upload-time = "2026-01-26T23:27:31.714Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b2/c36a87f5c745d310b7d8a53df053d6a87864aa38e3a964b0845eb6de37cc/uv-0.9.27-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a662a7df5cc781ae7baa65171b5d488d946ea93e61b7bbeda5a24d21a0cd9003", size = 21081065, upload-time = "2026-01-26T23:28:11.895Z" }, + { url = "https://files.pythonhosted.org/packages/44/1d/be2d80573c531389933059f6e5ef265ef7324c268f3ade80e500aa627f6b/uv-0.9.27-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8f00158023e77600da602c5f1fa97cd8c2eef987d6aba34c16cf04a3e5a932f4", size = 19844905, upload-time = "2026-01-26T23:27:34.486Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f7/59679af9f0446d8ffc1239e3356390c95925e0004549b64df3f189b1422b/uv-0.9.27-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:5f2393051ed2023cc7d6ff99e41184b7c7bb7da001bc727cd4bee6da96f4a966", size = 21592623, upload-time = "2026-01-26T23:27:51.132Z" }, + { url = "https://files.pythonhosted.org/packages/e2/31/0faaad82951fc6b14dfad8e187e43747a528aa50ee283385f903e86d67d1/uv-0.9.27-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:3f8cf7a50a95ae5cb0366d24edf79d15b3ba380b462af49e3404f9f7b91101c7", size = 21636917, upload-time = "2026-01-26T23:27:46.252Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8a/e181c32b7f5309fd987667d368fb944822c713e92a7eba3c73d2eddec6cd/uv-0.9.27-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c331e0445465ea6029e2dd0f5499024e552136c10069fac0ca21708f2aeb1ce6", size = 21633082, upload-time = "2026-01-26T23:28:09.417Z" }, + { url = "https://files.pythonhosted.org/packages/da/0e/1d44157bc8e5d1c382db087d9a30ab85fc7b5c2d610fb2e3d861c5a69d9b/uv-0.9.27-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a56e0c92da67060d4c86a3b92b2c69e8fb1d61933636764aba16531ddb13f6e3", size = 22843044, upload-time = "2026-01-26T23:27:29.091Z" }, + { url = "https://files.pythonhosted.org/packages/eb/76/7c1b13e4dc8237dd3721f4ec933bb2e5be400fd2812cf98dc2be645a0f7d/uv-0.9.27-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0c9b2e874f5207a50b852726f3a0740eadf30baf2c7973380d697f4e3166d347", size = 24141329, upload-time = "2026-01-26T23:27:39.705Z" }, + { url = "https://files.pythonhosted.org/packages/6f/04/551749fd863cb43290c9a3f4348ccdd88ec0445c26a00ba840d776572867/uv-0.9.27-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79841a2e4df4a32f22fbb0919c3e513226684572fba227b37467ba6404f3fafd", size = 23637517, upload-time = "2026-01-26T23:27:37.111Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c6/78b619a51a6646af4633714b161f980ab764cc892e0f79228162fba51fe8/uv-0.9.27-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33902c95b561ac67d15c339fe1eaf39e068372c7464c79c3bd0e2bf9ee914dcb", size = 22864516, upload-time = "2026-01-26T23:27:56.35Z" }, + { url = "https://files.pythonhosted.org/packages/15/19/b35928e55307beb69b60b88446df3cb8d7ff3ba0993fc2214a43266c17d1/uv-0.9.27-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79939f7e92d707fb84933509df747d1b88b00d94ebe41f3a1e30916cc33c7307", size = 22746151, upload-time = "2026-01-26T23:27:26.166Z" }, + { url = "https://files.pythonhosted.org/packages/9f/70/fbab20d40afe7ac9ec20011acec75f8bb3b9b83dfbe2cdb1405cad7a8cf2/uv-0.9.27-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:7e2d4183a0dca7596ea6385e9d5a0a87ada4f71a70aa110e2b22234370b8d8ef", size = 21661188, upload-time = "2026-01-26T23:27:53.79Z" }, + { url = "https://files.pythonhosted.org/packages/44/02/4d4cf298bd22e53d6c289404b093cf876e64ee1fb946cc32a6f965030629/uv-0.9.27-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1555ab7bc8501144e8771e54a628eb02cb95f3612d54659bb7132576260feee5", size = 22397798, upload-time = "2026-01-26T23:28:07.102Z" }, + { url = "https://files.pythonhosted.org/packages/97/29/3acef6a0eea58afbf7f7a08e4258430e3c7394a6b1e28249450f4c0ddc60/uv-0.9.27-py3-none-musllinux_1_1_i686.whl", hash = "sha256:542731a6f53072e725959a9c839b195048715d840213d9834d36f74fa4249855", size = 22111665, upload-time = "2026-01-26T23:27:58.762Z" }, + { url = "https://files.pythonhosted.org/packages/13/15/1e7b34f02e8f53c9498311f991421e794ad57fa60a2d3e41b43485e914e4/uv-0.9.27-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:4f534ad701ca3fffac4a8e1df2a36930e6a0cbf4dad52aeabc2c3c9e2cbbe65e", size = 22951420, upload-time = "2026-01-26T23:27:48.818Z" }, ] [[package]] name = "uvloop" -version = "0.21.0" +version = "0.22.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741, upload-time = "2024-10-14T23:38:35.489Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/76/44a55515e8c9505aa1420aebacf4dd82552e5e15691654894e90d0bd051a/uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f", size = 1442019, upload-time = "2024-10-14T23:37:20.068Z" }, - { url = "https://files.pythonhosted.org/packages/35/5a/62d5800358a78cc25c8a6c72ef8b10851bdb8cca22e14d9c74167b7f86da/uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d", size = 801898, upload-time = "2024-10-14T23:37:22.663Z" }, - { url = "https://files.pythonhosted.org/packages/f3/96/63695e0ebd7da6c741ccd4489b5947394435e198a1382349c17b1146bb97/uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26", size = 3827735, upload-time = "2024-10-14T23:37:25.129Z" }, - { url = "https://files.pythonhosted.org/packages/61/e0/f0f8ec84979068ffae132c58c79af1de9cceeb664076beea86d941af1a30/uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb", size = 3825126, upload-time = "2024-10-14T23:37:27.59Z" }, - { url = "https://files.pythonhosted.org/packages/bf/fe/5e94a977d058a54a19df95f12f7161ab6e323ad49f4dabc28822eb2df7ea/uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f", size = 3705789, upload-time = "2024-10-14T23:37:29.385Z" }, - { url = "https://files.pythonhosted.org/packages/26/dd/c7179618e46092a77e036650c1f056041a028a35c4d76945089fcfc38af8/uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c", size = 3800523, upload-time = "2024-10-14T23:37:32.048Z" }, - { url = "https://files.pythonhosted.org/packages/57/a7/4cf0334105c1160dd6819f3297f8700fda7fc30ab4f61fbf3e725acbc7cc/uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8", size = 1447410, upload-time = "2024-10-14T23:37:33.612Z" }, - { url = "https://files.pythonhosted.org/packages/8c/7c/1517b0bbc2dbe784b563d6ab54f2ef88c890fdad77232c98ed490aa07132/uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0", size = 805476, upload-time = "2024-10-14T23:37:36.11Z" }, - { url = "https://files.pythonhosted.org/packages/ee/ea/0bfae1aceb82a503f358d8d2fa126ca9dbdb2ba9c7866974faec1cb5875c/uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e", size = 3960855, upload-time = "2024-10-14T23:37:37.683Z" }, - { url = "https://files.pythonhosted.org/packages/8a/ca/0864176a649838b838f36d44bf31c451597ab363b60dc9e09c9630619d41/uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb", size = 3973185, upload-time = "2024-10-14T23:37:40.226Z" }, - { url = "https://files.pythonhosted.org/packages/30/bf/08ad29979a936d63787ba47a540de2132169f140d54aa25bc8c3df3e67f4/uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6", size = 3820256, upload-time = "2024-10-14T23:37:42.839Z" }, - { url = "https://files.pythonhosted.org/packages/da/e2/5cf6ef37e3daf2f06e651aae5ea108ad30df3cb269102678b61ebf1fdf42/uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d", size = 3937323, upload-time = "2024-10-14T23:37:45.337Z" }, - { url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284, upload-time = "2024-10-14T23:37:47.833Z" }, - { url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349, upload-time = "2024-10-14T23:37:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089, upload-time = "2024-10-14T23:37:51.703Z" }, - { url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770, upload-time = "2024-10-14T23:37:54.122Z" }, - { url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321, upload-time = "2024-10-14T23:37:55.766Z" }, - { url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022, upload-time = "2024-10-14T23:37:58.195Z" }, - { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123, upload-time = "2024-10-14T23:38:00.688Z" }, - { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325, upload-time = "2024-10-14T23:38:02.309Z" }, - { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806, upload-time = "2024-10-14T23:38:04.711Z" }, - { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068, upload-time = "2024-10-14T23:38:06.385Z" }, - { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428, upload-time = "2024-10-14T23:38:08.416Z" }, - { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018, upload-time = "2024-10-14T23:38:10.888Z" }, + { url = "https://files.pythonhosted.org/packages/eb/14/ecceb239b65adaaf7fde510aa8bd534075695d1e5f8dadfa32b5723d9cfb/uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c", size = 1343335, upload-time = "2025-10-16T22:16:11.43Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ae/6f6f9af7f590b319c94532b9567409ba11f4fa71af1148cab1bf48a07048/uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792", size = 742903, upload-time = "2025-10-16T22:16:12.979Z" }, + { url = "https://files.pythonhosted.org/packages/09/bd/3667151ad0702282a1f4d5d29288fce8a13c8b6858bf0978c219cd52b231/uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86", size = 3648499, upload-time = "2025-10-16T22:16:14.451Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f6/21657bb3beb5f8c57ce8be3b83f653dd7933c2fd00545ed1b092d464799a/uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd", size = 3700133, upload-time = "2025-10-16T22:16:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/09/e0/604f61d004ded805f24974c87ddd8374ef675644f476f01f1df90e4cdf72/uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2", size = 3512681, upload-time = "2025-10-16T22:16:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ce/8491fd370b0230deb5eac69c7aae35b3be527e25a911c0acdffb922dc1cd/uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec", size = 3615261, upload-time = "2025-10-16T22:16:19.596Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, ] [[package]] @@ -4285,7 +5983,7 @@ wheels = [ [[package]] name = "virtualenv" -version = "20.34.0" +version = "20.36.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -4293,9 +5991,9 @@ dependencies = [ { name = "platformdirs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1c/14/37fcdba2808a6c615681cd216fecae00413c9dab44fb2e57805ecf3eaee3/virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a", size = 6003808, upload-time = "2025-08-13T14:24:07.464Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026", size = 5983279, upload-time = "2025-08-13T14:24:05.111Z" }, + { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, ] [[package]] @@ -4328,12 +6026,101 @@ wheels = [ ] [[package]] -name = "wcwidth" -version = "0.2.14" +name = "watchfiles" +version = "1.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } +dependencies = [ + { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, + { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, + { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, + { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/64/6e/62daec357285b927e82263a81f3b4c1790215bc77c42530ce4a69d501a43/wcwidth-0.5.0.tar.gz", hash = "sha256:f89c103c949a693bf563377b2153082bf58e309919dfb7f27b04d862a0089333", size = 246585, upload-time = "2026-01-27T01:31:44.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/3e/45583b67c2ff08ad5a582d316fcb2f11d6cf0a50c7707ac09d212d25bc98/wcwidth-0.5.0-py3-none-any.whl", hash = "sha256:1efe1361b83b0ff7877b81ba57c8562c99cf812158b778988ce17ec061095695", size = 93772, upload-time = "2026-01-27T01:31:43.432Z" }, ] [[package]] @@ -4368,81 +6155,212 @@ wheels = [ [[package]] name = "wrapt" -version = "1.17.3" +version = "2.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2a/6de8a50cb435b7f42c46126cf1a54b2aab81784e74c8595c8e025e8f36d3/wrapt-2.0.1.tar.gz", hash = "sha256:9c9c635e78497cacb81e84f8b11b23e0aacac7a136e73b8e5b2109a1d9fc468f", size = 82040, upload-time = "2025-11-07T00:45:33.312Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482, upload-time = "2025-08-12T05:51:44.467Z" }, - { url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676, upload-time = "2025-08-12T05:51:32.636Z" }, - { url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957, upload-time = "2025-08-12T05:51:54.655Z" }, - { url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975, upload-time = "2025-08-12T05:52:30.109Z" }, - { url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149, upload-time = "2025-08-12T05:52:09.316Z" }, - { url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209, upload-time = "2025-08-12T05:52:10.331Z" }, - { url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551, upload-time = "2025-08-12T05:52:31.137Z" }, - { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, - { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, - { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" }, - { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" }, - { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" }, - { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" }, - { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" }, - { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, - { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, - { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, - { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, - { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, - { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, - { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, - { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, - { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, - { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, - { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, - { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, - { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, - { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, - { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, - { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, - { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, - { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, - { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, - { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, - { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, - { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, - { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, - { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, - { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, - { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, + { url = "https://files.pythonhosted.org/packages/61/0d/12d8c803ed2ce4e5e7d5b9f5f602721f9dfef82c95959f3ce97fa584bb5c/wrapt-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:64b103acdaa53b7caf409e8d45d39a8442fe6dcfec6ba3f3d141e0cc2b5b4dbd", size = 77481, upload-time = "2025-11-07T00:43:11.103Z" }, + { url = "https://files.pythonhosted.org/packages/05/3e/4364ebe221ebf2a44d9fc8695a19324692f7dd2795e64bd59090856ebf12/wrapt-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:91bcc576260a274b169c3098e9a3519fb01f2989f6d3d386ef9cbf8653de1374", size = 60692, upload-time = "2025-11-07T00:43:13.697Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ff/ae2a210022b521f86a8ddcdd6058d137c051003812b0388a5e9a03d3fe10/wrapt-2.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ab594f346517010050126fcd822697b25a7031d815bb4fbc238ccbe568216489", size = 61574, upload-time = "2025-11-07T00:43:14.967Z" }, + { url = "https://files.pythonhosted.org/packages/c6/93/5cf92edd99617095592af919cb81d4bff61c5dbbb70d3c92099425a8ec34/wrapt-2.0.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:36982b26f190f4d737f04a492a68accbfc6fa042c3f42326fdfbb6c5b7a20a31", size = 113688, upload-time = "2025-11-07T00:43:18.275Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0a/e38fc0cee1f146c9fb266d8ef96ca39fb14a9eef165383004019aa53f88a/wrapt-2.0.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23097ed8bc4c93b7bf36fa2113c6c733c976316ce0ee2c816f64ca06102034ef", size = 115698, upload-time = "2025-11-07T00:43:19.407Z" }, + { url = "https://files.pythonhosted.org/packages/b0/85/bef44ea018b3925fb0bcbe9112715f665e4d5309bd945191da814c314fd1/wrapt-2.0.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8bacfe6e001749a3b64db47bcf0341da757c95959f592823a93931a422395013", size = 112096, upload-time = "2025-11-07T00:43:16.5Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0b/733a2376e413117e497aa1a5b1b78e8f3a28c0e9537d26569f67d724c7c5/wrapt-2.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8ec3303e8a81932171f455f792f8df500fc1a09f20069e5c16bd7049ab4e8e38", size = 114878, upload-time = "2025-11-07T00:43:20.81Z" }, + { url = "https://files.pythonhosted.org/packages/da/03/d81dcb21bbf678fcda656495792b059f9d56677d119ca022169a12542bd0/wrapt-2.0.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:3f373a4ab5dbc528a94334f9fe444395b23c2f5332adab9ff4ea82f5a9e33bc1", size = 111298, upload-time = "2025-11-07T00:43:22.229Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d5/5e623040e8056e1108b787020d56b9be93dbbf083bf2324d42cde80f3a19/wrapt-2.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f49027b0b9503bf6c8cdc297ca55006b80c2f5dd36cecc72c6835ab6e10e8a25", size = 113361, upload-time = "2025-11-07T00:43:24.301Z" }, + { url = "https://files.pythonhosted.org/packages/98/60/553997acf3939079dab022e37b67b1904b5b0cc235503226898ba573b10c/wrapt-2.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e17283f533a0d24d6e5429a7d11f250a58d28b4ae5186f8f47853e3e70d2590", size = 77480, upload-time = "2025-11-07T00:43:30.573Z" }, + { url = "https://files.pythonhosted.org/packages/2d/50/e5b3d30895d77c52105c6d5cbf94d5b38e2a3dd4a53d22d246670da98f7c/wrapt-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85df8d92158cb8f3965aecc27cf821461bb5f40b450b03facc5d9f0d4d6ddec6", size = 60690, upload-time = "2025-11-07T00:43:31.594Z" }, + { url = "https://files.pythonhosted.org/packages/f0/40/660b2898703e5cbbb43db10cdefcc294274458c3ca4c68637c2b99371507/wrapt-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1be685ac7700c966b8610ccc63c3187a72e33cab53526a27b2a285a662cd4f7", size = 61578, upload-time = "2025-11-07T00:43:32.918Z" }, + { url = "https://files.pythonhosted.org/packages/5b/36/825b44c8a10556957bc0c1d84c7b29a40e05fcf1873b6c40aa9dbe0bd972/wrapt-2.0.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:df0b6d3b95932809c5b3fecc18fda0f1e07452d05e2662a0b35548985f256e28", size = 114115, upload-time = "2025-11-07T00:43:35.605Z" }, + { url = "https://files.pythonhosted.org/packages/83/73/0a5d14bb1599677304d3c613a55457d34c344e9b60eda8a737c2ead7619e/wrapt-2.0.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da7384b0e5d4cae05c97cd6f94faaf78cc8b0f791fc63af43436d98c4ab37bb", size = 116157, upload-time = "2025-11-07T00:43:37.058Z" }, + { url = "https://files.pythonhosted.org/packages/01/22/1c158fe763dbf0a119f985d945711d288994fe5514c0646ebe0eb18b016d/wrapt-2.0.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ec65a78fbd9d6f083a15d7613b2800d5663dbb6bb96003899c834beaa68b242c", size = 112535, upload-time = "2025-11-07T00:43:34.138Z" }, + { url = "https://files.pythonhosted.org/packages/5c/28/4f16861af67d6de4eae9927799b559c20ebdd4fe432e89ea7fe6fcd9d709/wrapt-2.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7de3cc939be0e1174969f943f3b44e0d79b6f9a82198133a5b7fc6cc92882f16", size = 115404, upload-time = "2025-11-07T00:43:39.214Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8b/7960122e625fad908f189b59c4aae2d50916eb4098b0fb2819c5a177414f/wrapt-2.0.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fb1a5b72cbd751813adc02ef01ada0b0d05d3dcbc32976ce189a1279d80ad4a2", size = 111802, upload-time = "2025-11-07T00:43:40.476Z" }, + { url = "https://files.pythonhosted.org/packages/3e/73/7881eee5ac31132a713ab19a22c9e5f1f7365c8b1df50abba5d45b781312/wrapt-2.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3fa272ca34332581e00bf7773e993d4f632594eb2d1b0b162a9038df0fd971dd", size = 113837, upload-time = "2025-11-07T00:43:42.921Z" }, + { url = "https://files.pythonhosted.org/packages/cb/73/8cb252858dc8254baa0ce58ce382858e3a1cf616acebc497cb13374c95c6/wrapt-2.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1fdbb34da15450f2b1d735a0e969c24bdb8d8924892380126e2a293d9902078c", size = 78129, upload-time = "2025-11-07T00:43:48.852Z" }, + { url = "https://files.pythonhosted.org/packages/19/42/44a0db2108526ee6e17a5ab72478061158f34b08b793df251d9fbb9a7eb4/wrapt-2.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d32794fe940b7000f0519904e247f902f0149edbe6316c710a8562fb6738841", size = 61205, upload-time = "2025-11-07T00:43:50.402Z" }, + { url = "https://files.pythonhosted.org/packages/4d/8a/5b4b1e44b791c22046e90d9b175f9a7581a8cc7a0debbb930f81e6ae8e25/wrapt-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:386fb54d9cd903ee0012c09291336469eb7b244f7183d40dc3e86a16a4bace62", size = 61692, upload-time = "2025-11-07T00:43:51.678Z" }, + { url = "https://files.pythonhosted.org/packages/11/53/3e794346c39f462bcf1f58ac0487ff9bdad02f9b6d5ee2dc84c72e0243b2/wrapt-2.0.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7b219cb2182f230676308cdcacd428fa837987b89e4b7c5c9025088b8a6c9faf", size = 121492, upload-time = "2025-11-07T00:43:55.017Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7e/10b7b0e8841e684c8ca76b462a9091c45d62e8f2de9c4b1390b690eadf16/wrapt-2.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:641e94e789b5f6b4822bb8d8ebbdfc10f4e4eae7756d648b717d980f657a9eb9", size = 123064, upload-time = "2025-11-07T00:43:56.323Z" }, + { url = "https://files.pythonhosted.org/packages/0e/d1/3c1e4321fc2f5ee7fd866b2d822aa89b84495f28676fd976c47327c5b6aa/wrapt-2.0.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe21b118b9f58859b5ebaa4b130dee18669df4bd111daad082b7beb8799ad16b", size = 117403, upload-time = "2025-11-07T00:43:53.258Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b0/d2f0a413cf201c8c2466de08414a15420a25aa83f53e647b7255cc2fab5d/wrapt-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:17fb85fa4abc26a5184d93b3efd2dcc14deb4b09edcdb3535a536ad34f0b4dba", size = 121500, upload-time = "2025-11-07T00:43:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/bd/45/bddb11d28ca39970a41ed48a26d210505120f925918592283369219f83cc/wrapt-2.0.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b89ef9223d665ab255ae42cc282d27d69704d94be0deffc8b9d919179a609684", size = 116299, upload-time = "2025-11-07T00:43:58.877Z" }, + { url = "https://files.pythonhosted.org/packages/81/af/34ba6dd570ef7a534e7eec0c25e2615c355602c52aba59413411c025a0cb/wrapt-2.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a453257f19c31b31ba593c30d997d6e5be39e3b5ad9148c2af5a7314061c63eb", size = 120622, upload-time = "2025-11-07T00:43:59.962Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/41af4c46b5e498c90fc87981ab2972fbd9f0bccda597adb99d3d3441b94b/wrapt-2.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:47b0f8bafe90f7736151f61482c583c86b0693d80f075a58701dd1549b0010a9", size = 78132, upload-time = "2025-11-07T00:44:04.628Z" }, + { url = "https://files.pythonhosted.org/packages/1c/92/d68895a984a5ebbbfb175512b0c0aad872354a4a2484fbd5552e9f275316/wrapt-2.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cbeb0971e13b4bd81d34169ed57a6dda017328d1a22b62fda45e1d21dd06148f", size = 61211, upload-time = "2025-11-07T00:44:05.626Z" }, + { url = "https://files.pythonhosted.org/packages/e8/26/ba83dc5ae7cf5aa2b02364a3d9cf74374b86169906a1f3ade9a2d03cf21c/wrapt-2.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb7cffe572ad0a141a7886a1d2efa5bef0bf7fe021deeea76b3ab334d2c38218", size = 61689, upload-time = "2025-11-07T00:44:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/cf/67/d7a7c276d874e5d26738c22444d466a3a64ed541f6ef35f740dbd865bab4/wrapt-2.0.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8d60527d1ecfc131426b10d93ab5d53e08a09c5fa0175f6b21b3252080c70a9", size = 121502, upload-time = "2025-11-07T00:44:09.557Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6b/806dbf6dd9579556aab22fc92908a876636e250f063f71548a8660382184/wrapt-2.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c654eafb01afac55246053d67a4b9a984a3567c3808bb7df2f8de1c1caba2e1c", size = 123110, upload-time = "2025-11-07T00:44:10.64Z" }, + { url = "https://files.pythonhosted.org/packages/e5/08/cdbb965fbe4c02c5233d185d070cabed2ecc1f1e47662854f95d77613f57/wrapt-2.0.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:98d873ed6c8b4ee2418f7afce666751854d6d03e3c0ec2a399bb039cd2ae89db", size = 117434, upload-time = "2025-11-07T00:44:08.138Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d1/6aae2ce39db4cb5216302fa2e9577ad74424dfbe315bd6669725569e048c/wrapt-2.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9e850f5b7fc67af856ff054c71690d54fa940c3ef74209ad9f935b4f66a0233", size = 121533, upload-time = "2025-11-07T00:44:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/79/35/565abf57559fbe0a9155c29879ff43ce8bd28d2ca61033a3a3dd67b70794/wrapt-2.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e505629359cb5f751e16e30cf3f91a1d3ddb4552480c205947da415d597f7ac2", size = 116324, upload-time = "2025-11-07T00:44:13.28Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e0/53ff5e76587822ee33e560ad55876d858e384158272cd9947abdd4ad42ca/wrapt-2.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2879af909312d0baf35f08edeea918ee3af7ab57c37fe47cb6a373c9f2749c7b", size = 120627, upload-time = "2025-11-07T00:44:14.431Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f4/eff2b7d711cae20d220780b9300faa05558660afb93f2ff5db61fe725b9a/wrapt-2.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a52f93d95c8d38fed0669da2ebdb0b0376e895d84596a976c15a9eb45e3eccb3", size = 82028, upload-time = "2025-11-07T00:44:18.944Z" }, + { url = "https://files.pythonhosted.org/packages/0c/67/cb945563f66fd0f61a999339460d950f4735c69f18f0a87ca586319b1778/wrapt-2.0.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e54bbf554ee29fcceee24fa41c4d091398b911da6e7f5d7bffda963c9aed2e1", size = 62949, upload-time = "2025-11-07T00:44:20.074Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ca/f63e177f0bbe1e5cf5e8d9b74a286537cd709724384ff20860f8f6065904/wrapt-2.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:908f8c6c71557f4deaa280f55d0728c3bca0960e8c3dd5ceeeafb3c19942719d", size = 63681, upload-time = "2025-11-07T00:44:21.345Z" }, + { url = "https://files.pythonhosted.org/packages/39/a1/1b88fcd21fd835dca48b556daef750952e917a2794fa20c025489e2e1f0f/wrapt-2.0.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e2f84e9af2060e3904a32cea9bb6db23ce3f91cfd90c6b426757cf7cc01c45c7", size = 152696, upload-time = "2025-11-07T00:44:24.318Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/d9185500c1960d9f5f77b9c0b890b7fc62282b53af7ad1b6bd779157f714/wrapt-2.0.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3612dc06b436968dfb9142c62e5dfa9eb5924f91120b3c8ff501ad878f90eb3", size = 158859, upload-time = "2025-11-07T00:44:25.494Z" }, + { url = "https://files.pythonhosted.org/packages/91/60/5d796ed0f481ec003220c7878a1d6894652efe089853a208ea0838c13086/wrapt-2.0.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d2d947d266d99a1477cd005b23cbd09465276e302515e122df56bb9511aca1b", size = 146068, upload-time = "2025-11-07T00:44:22.81Z" }, + { url = "https://files.pythonhosted.org/packages/04/f8/75282dd72f102ddbfba137e1e15ecba47b40acff32c08ae97edbf53f469e/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7d539241e87b650cbc4c3ac9f32c8d1ac8a54e510f6dca3f6ab60dcfd48c9b10", size = 155724, upload-time = "2025-11-07T00:44:26.634Z" }, + { url = "https://files.pythonhosted.org/packages/5a/27/fe39c51d1b344caebb4a6a9372157bdb8d25b194b3561b52c8ffc40ac7d1/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:4811e15d88ee62dbf5c77f2c3ff3932b1e3ac92323ba3912f51fc4016ce81ecf", size = 144413, upload-time = "2025-11-07T00:44:27.939Z" }, + { url = "https://files.pythonhosted.org/packages/83/2b/9f6b643fe39d4505c7bf926d7c2595b7cb4b607c8c6b500e56c6b36ac238/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c1c91405fcf1d501fa5d55df21e58ea49e6b879ae829f1039faaf7e5e509b41e", size = 150325, upload-time = "2025-11-07T00:44:29.29Z" }, + { url = "https://files.pythonhosted.org/packages/73/81/d08d83c102709258e7730d3cd25befd114c60e43ef3891d7e6877971c514/wrapt-2.0.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5e53b428f65ece6d9dad23cb87e64506392b720a0b45076c05354d27a13351a1", size = 78290, upload-time = "2025-11-07T00:44:34.691Z" }, + { url = "https://files.pythonhosted.org/packages/f6/14/393afba2abb65677f313aa680ff0981e829626fed39b6a7e3ec807487790/wrapt-2.0.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ad3ee9d0f254851c71780966eb417ef8e72117155cff04821ab9b60549694a55", size = 61255, upload-time = "2025-11-07T00:44:35.762Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/a4a1f2fba205a9462e36e708ba37e5ac95f4987a0f1f8fd23f0bf1fc3b0f/wrapt-2.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7b822c61ed04ee6ad64bc90d13368ad6eb094db54883b5dde2182f67a7f22c0", size = 61797, upload-time = "2025-11-07T00:44:37.22Z" }, + { url = "https://files.pythonhosted.org/packages/12/db/99ba5c37cf1c4fad35349174f1e38bd8d992340afc1ff27f526729b98986/wrapt-2.0.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7164a55f5e83a9a0b031d3ffab4d4e36bbec42e7025db560f225489fa929e509", size = 120470, upload-time = "2025-11-07T00:44:39.425Z" }, + { url = "https://files.pythonhosted.org/packages/30/3f/a1c8d2411eb826d695fc3395a431757331582907a0ec59afce8fe8712473/wrapt-2.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e60690ba71a57424c8d9ff28f8d006b7ad7772c22a4af432188572cd7fa004a1", size = 122851, upload-time = "2025-11-07T00:44:40.582Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8d/72c74a63f201768d6a04a8845c7976f86be6f5ff4d74996c272cefc8dafc/wrapt-2.0.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3cd1a4bd9a7a619922a8557e1318232e7269b5fb69d4ba97b04d20450a6bf970", size = 117433, upload-time = "2025-11-07T00:44:38.313Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5a/df37cf4042cb13b08256f8e27023e2f9b3d471d553376616591bb99bcb31/wrapt-2.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b4c2e3d777e38e913b8ce3a6257af72fb608f86a1df471cb1d4339755d0a807c", size = 121280, upload-time = "2025-11-07T00:44:41.69Z" }, + { url = "https://files.pythonhosted.org/packages/54/34/40d6bc89349f9931e1186ceb3e5fbd61d307fef814f09fbbac98ada6a0c8/wrapt-2.0.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3d366aa598d69416b5afedf1faa539fac40c1d80a42f6b236c88c73a3c8f2d41", size = 116343, upload-time = "2025-11-07T00:44:43.013Z" }, + { url = "https://files.pythonhosted.org/packages/70/66/81c3461adece09d20781dee17c2366fdf0cb8754738b521d221ca056d596/wrapt-2.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c235095d6d090aa903f1db61f892fffb779c1eaeb2a50e566b52001f7a0f66ed", size = 119650, upload-time = "2025-11-07T00:44:44.523Z" }, + { url = "https://files.pythonhosted.org/packages/71/49/5f5d1e867bf2064bf3933bc6cf36ade23505f3902390e175e392173d36a2/wrapt-2.0.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:9219a1d946a9b32bb23ccae66bdb61e35c62773ce7ca6509ceea70f344656b7b", size = 82031, upload-time = "2025-11-07T00:44:49.4Z" }, + { url = "https://files.pythonhosted.org/packages/2b/89/0009a218d88db66ceb83921e5685e820e2c61b59bbbb1324ba65342668bc/wrapt-2.0.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fa4184e74197af3adad3c889a1af95b53bb0466bced92ea99a0c014e48323eec", size = 62952, upload-time = "2025-11-07T00:44:50.74Z" }, + { url = "https://files.pythonhosted.org/packages/ae/18/9b968e920dd05d6e44bcc918a046d02afea0fb31b2f1c80ee4020f377cbe/wrapt-2.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c5ef2f2b8a53b7caee2f797ef166a390fef73979b15778a4a153e4b5fedce8fa", size = 63688, upload-time = "2025-11-07T00:44:52.248Z" }, + { url = "https://files.pythonhosted.org/packages/a6/7d/78bdcb75826725885d9ea26c49a03071b10c4c92da93edda612910f150e4/wrapt-2.0.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e042d653a4745be832d5aa190ff80ee4f02c34b21f4b785745eceacd0907b815", size = 152706, upload-time = "2025-11-07T00:44:54.613Z" }, + { url = "https://files.pythonhosted.org/packages/dd/77/cac1d46f47d32084a703df0d2d29d47e7eb2a7d19fa5cbca0e529ef57659/wrapt-2.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2afa23318136709c4b23d87d543b425c399887b4057936cd20386d5b1422b6fa", size = 158866, upload-time = "2025-11-07T00:44:55.79Z" }, + { url = "https://files.pythonhosted.org/packages/8a/11/b521406daa2421508903bf8d5e8b929216ec2af04839db31c0a2c525eee0/wrapt-2.0.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c72328f668cf4c503ffcf9434c2b71fdd624345ced7941bc6693e61bbe36bef", size = 146148, upload-time = "2025-11-07T00:44:53.388Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c0/340b272bed297baa7c9ce0c98ef7017d9c035a17a6a71dce3184b8382da2/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3793ac154afb0e5b45d1233cb94d354ef7a983708cc3bb12563853b1d8d53747", size = 155737, upload-time = "2025-11-07T00:44:56.971Z" }, + { url = "https://files.pythonhosted.org/packages/f3/93/bfcb1fb2bdf186e9c2883a4d1ab45ab099c79cbf8f4e70ea453811fa3ea7/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fec0d993ecba3991645b4857837277469c8cc4c554a7e24d064d1ca291cfb81f", size = 144451, upload-time = "2025-11-07T00:44:58.515Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6b/dca504fb18d971139d232652656180e3bd57120e1193d9a5899c3c0b7cdd/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:949520bccc1fa227274da7d03bf238be15389cd94e32e4297b92337df9b7a349", size = 150353, upload-time = "2025-11-07T00:44:59.753Z" }, + { url = "https://files.pythonhosted.org/packages/15/d1/b51471c11592ff9c012bd3e2f7334a6ff2f42a7aed2caffcf0bdddc9cb89/wrapt-2.0.1-py3-none-any.whl", hash = "sha256:4d2ce1bf1a48c5277d7969259232b57645aae5686dba1eaeade39442277afbca", size = 44046, upload-time = "2025-11-07T00:45:32.116Z" }, +] + +[[package]] +name = "yarl" +version = "1.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "multidict", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "propcache", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/43/a2204825342f37c337f5edb6637040fa14e365b2fcc2346960201d457579/yarl-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e", size = 140517, upload-time = "2025-10-06T14:08:42.494Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/674f3e6f02266428c56f704cd2501c22f78e8b2eeb23f153117cc86fb28a/yarl-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f", size = 93495, upload-time = "2025-10-06T14:08:46.2Z" }, + { url = "https://files.pythonhosted.org/packages/b8/12/5b274d8a0f30c07b91b2f02cba69152600b47830fcfb465c108880fcee9c/yarl-1.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf", size = 94400, upload-time = "2025-10-06T14:08:47.855Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7f/df1b6949b1fa1aa9ff6de6e2631876ad4b73c4437822026e85d8acb56bb1/yarl-1.22.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a", size = 347545, upload-time = "2025-10-06T14:08:49.683Z" }, + { url = "https://files.pythonhosted.org/packages/84/09/f92ed93bd6cd77872ab6c3462df45ca45cd058d8f1d0c9b4f54c1704429f/yarl-1.22.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c", size = 319598, upload-time = "2025-10-06T14:08:51.215Z" }, + { url = "https://files.pythonhosted.org/packages/c3/97/ac3f3feae7d522cf7ccec3d340bb0b2b61c56cb9767923df62a135092c6b/yarl-1.22.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147", size = 363893, upload-time = "2025-10-06T14:08:53.144Z" }, + { url = "https://files.pythonhosted.org/packages/06/49/f3219097403b9c84a4d079b1d7bda62dd9b86d0d6e4428c02d46ab2c77fc/yarl-1.22.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb", size = 371240, upload-time = "2025-10-06T14:08:55.036Z" }, + { url = "https://files.pythonhosted.org/packages/35/9f/06b765d45c0e44e8ecf0fe15c9eacbbde342bb5b7561c46944f107bfb6c3/yarl-1.22.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6", size = 346965, upload-time = "2025-10-06T14:08:56.722Z" }, + { url = "https://files.pythonhosted.org/packages/c5/69/599e7cea8d0fcb1694323b0db0dda317fa3162f7b90166faddecf532166f/yarl-1.22.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0", size = 342026, upload-time = "2025-10-06T14:08:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/95/6f/9dfd12c8bc90fea9eab39832ee32ea48f8e53d1256252a77b710c065c89f/yarl-1.22.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda", size = 335637, upload-time = "2025-10-06T14:09:00.506Z" }, + { url = "https://files.pythonhosted.org/packages/57/2e/34c5b4eb9b07e16e873db5b182c71e5f06f9b5af388cdaa97736d79dd9a6/yarl-1.22.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc", size = 359082, upload-time = "2025-10-06T14:09:01.936Z" }, + { url = "https://files.pythonhosted.org/packages/31/71/fa7e10fb772d273aa1f096ecb8ab8594117822f683bab7d2c5a89914c92a/yarl-1.22.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737", size = 357811, upload-time = "2025-10-06T14:09:03.445Z" }, + { url = "https://files.pythonhosted.org/packages/26/da/11374c04e8e1184a6a03cf9c8f5688d3e5cec83ed6f31ad3481b3207f709/yarl-1.22.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467", size = 351223, upload-time = "2025-10-06T14:09:05.401Z" }, + { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" }, + { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" }, + { url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406, upload-time = "2025-10-06T14:09:21.402Z" }, + { url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581, upload-time = "2025-10-06T14:09:22.98Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924, upload-time = "2025-10-06T14:09:24.655Z" }, + { url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890, upload-time = "2025-10-06T14:09:26.617Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819, upload-time = "2025-10-06T14:09:28.544Z" }, + { url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601, upload-time = "2025-10-06T14:09:30.568Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072, upload-time = "2025-10-06T14:09:32.528Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" }, + { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" }, + { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" }, + { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" }, + { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" }, + { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" }, + { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" }, + { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" }, + { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" }, + { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" }, + { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" }, + { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" }, + { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" }, + { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" }, + { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" }, + { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" }, + { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" }, + { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" }, + { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" }, + { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" }, + { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" }, + { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" }, + { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" }, + { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" }, + { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" }, + { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" }, + { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" }, + { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" }, + { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" }, + { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" }, + { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" }, + { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" }, + { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" }, + { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" }, + { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" }, + { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, ] [[package]] name = "zope-interface" -version = "8.0.1" +version = "8.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/88/3a/7fcf02178b8fad0a51e67e32765cd039ae505d054d744d76b8c2bbcba5ba/zope_interface-8.0.1.tar.gz", hash = "sha256:eba5610d042c3704a48222f7f7c6ab5b243ed26f917e2bc69379456b115e02d1", size = 253746, upload-time = "2025-09-25T05:55:51.285Z" } +sdist = { url = "https://files.pythonhosted.org/packages/86/a4/77daa5ba398996d16bb43fc721599d27d03eae68fe3c799de1963c72e228/zope_interface-8.2.tar.gz", hash = "sha256:afb20c371a601d261b4f6edb53c3c418c249db1a9717b0baafc9a9bb39ba1224", size = 254019, upload-time = "2026-01-09T07:51:07.253Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/75/e5/ffef169d17b92c6236b3b18b890c0ce73502f3cbd5b6532ff20d412d94a3/zope_interface-8.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fd7195081b8637eeed8d73e4d183b07199a1dc738fb28b3de6666b1b55662570", size = 207364, upload-time = "2025-09-25T05:58:50.262Z" }, - { url = "https://files.pythonhosted.org/packages/35/b6/87aca626c09af829d3a32011599d6e18864bc8daa0ad3a7e258f3d7f8bcf/zope_interface-8.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f7c4bc4021108847bce763673ce70d0716b08dfc2ba9889e7bad46ac2b3bb924", size = 207901, upload-time = "2025-09-25T05:58:51.74Z" }, - { url = "https://files.pythonhosted.org/packages/d8/c1/eec33cc9f847ebeb0bc6234d7d45fe3fc0a6fe8fc5b5e6be0442bd2c684d/zope_interface-8.0.1-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:758803806b962f32c87b31bb18c298b022965ba34fe532163831cc39118c24ab", size = 249358, upload-time = "2025-09-25T05:58:16.979Z" }, - { url = "https://files.pythonhosted.org/packages/58/7d/1e3476a1ef0175559bd8492dc7bb921ad0df5b73861d764b1f824ad5484a/zope_interface-8.0.1-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f8e88f35f86bbe8243cad4b2972deef0fdfca0a0723455abbebdc83bbab96b69", size = 254475, upload-time = "2025-09-25T05:58:10.032Z" }, - { url = "https://files.pythonhosted.org/packages/bc/67/ba5ea98ff23f723c5cbe7db7409f2e43c9fe2df1ced67881443c01e64478/zope_interface-8.0.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7844765695937d9b0d83211220b72e2cf6ac81a08608ad2b58f2c094af498d83", size = 254913, upload-time = "2025-09-25T06:26:22.263Z" }, - { url = "https://files.pythonhosted.org/packages/f2/2f/c10c739bcb9b072090c97c2e08533777497190daa19d190d72b4cce9c7cb/zope_interface-8.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4bd01022d2e1bce4a4a4ed9549edb25393c92e607d7daa6deff843f1f68b479d", size = 207903, upload-time = "2025-09-25T05:58:21.671Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e1/9845ac3697f108d9a1af6912170c59a23732090bbfb35955fe77e5544955/zope_interface-8.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:29be8db8b712d94f1c05e24ea230a879271d787205ba1c9a6100d1d81f06c69a", size = 208345, upload-time = "2025-09-25T05:58:24.217Z" }, - { url = "https://files.pythonhosted.org/packages/f2/49/6573bc8b841cfab18e80c8e8259f1abdbbf716140011370de30231be79ad/zope_interface-8.0.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:51ae1b856565b30455b7879fdf0a56a88763b401d3f814fa9f9542d7410dbd7e", size = 255027, upload-time = "2025-09-25T05:58:19.975Z" }, - { url = "https://files.pythonhosted.org/packages/e2/fd/908b0fd4b1ab6e412dfac9bd2b606f2893ef9ba3dd36d643f5e5b94c57b3/zope_interface-8.0.1-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d2e7596149cb1acd1d4d41b9f8fe2ffc0e9e29e2e91d026311814181d0d9efaf", size = 259800, upload-time = "2025-09-25T05:58:11.487Z" }, - { url = "https://files.pythonhosted.org/packages/dc/78/8419a2b4e88410520ed4b7f93bbd25a6d4ae66c4e2b131320f2b90f43077/zope_interface-8.0.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b2737c11c34fb9128816759864752d007ec4f987b571c934c30723ed881a7a4f", size = 260978, upload-time = "2025-09-25T06:26:24.483Z" }, - { url = "https://files.pythonhosted.org/packages/dc/a6/0f08713ddda834c428ebf97b2a7fd8dea50c0100065a8955924dbd94dae8/zope_interface-8.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:115f27c1cc95ce7a517d960ef381beedb0a7ce9489645e80b9ab3cbf8a78799c", size = 208609, upload-time = "2025-09-25T05:58:53.698Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5e/d423045f54dc81e0991ec655041e7a0eccf6b2642535839dd364b35f4d7f/zope_interface-8.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af655c573b84e3cb6a4f6fd3fbe04e4dc91c63c6b6f99019b3713ef964e589bc", size = 208797, upload-time = "2025-09-25T05:58:56.258Z" }, - { url = "https://files.pythonhosted.org/packages/c6/43/39d4bb3f7a80ebd261446792493cfa4e198badd47107224f5b6fe1997ad9/zope_interface-8.0.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:23f82ef9b2d5370750cc1bf883c3b94c33d098ce08557922a3fbc7ff3b63dfe1", size = 259242, upload-time = "2025-09-25T05:58:21.602Z" }, - { url = "https://files.pythonhosted.org/packages/da/29/49effcff64ef30731e35520a152a9dfcafec86cf114b4c2aff942e8264ba/zope_interface-8.0.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35a1565d5244997f2e629c5c68715b3d9d9036e8df23c4068b08d9316dcb2822", size = 264696, upload-time = "2025-09-25T05:58:13.351Z" }, - { url = "https://files.pythonhosted.org/packages/c7/39/b947673ec9a258eeaa20208dd2f6127d9fbb3e5071272a674ebe02063a78/zope_interface-8.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:029ea1db7e855a475bf88d9910baab4e94d007a054810e9007ac037a91c67c6f", size = 264229, upload-time = "2025-09-25T06:26:26.226Z" }, - { url = "https://files.pythonhosted.org/packages/5f/dc/3c12fca01c910c793d636ffe9c0984e0646abaf804e44552070228ed0ede/zope_interface-8.0.1-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:c7cc027fc5c61c5d69e5080c30b66382f454f43dc379c463a38e78a9c6bab71a", size = 208992, upload-time = "2025-09-25T05:58:40.712Z" }, - { url = "https://files.pythonhosted.org/packages/46/71/6127b7282a3e380ca927ab2b40778a9c97935a4a57a2656dadc312db5f30/zope_interface-8.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fcf9097ff3003b7662299f1c25145e15260ec2a27f9a9e69461a585d79ca8552", size = 209051, upload-time = "2025-09-25T05:58:42.182Z" }, - { url = "https://files.pythonhosted.org/packages/56/86/4387a9f951ee18b0e41fda77da77d59c33e59f04660578e2bad688703e64/zope_interface-8.0.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6d965347dd1fb9e9a53aa852d4ded46b41ca670d517fd54e733a6b6a4d0561c2", size = 259223, upload-time = "2025-09-25T05:58:23.191Z" }, - { url = "https://files.pythonhosted.org/packages/61/08/ce60a114466abc067c68ed41e2550c655f551468ae17b4b17ea360090146/zope_interface-8.0.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9a3b8bb77a4b89427a87d1e9eb969ab05e38e6b4a338a9de10f6df23c33ec3c2", size = 264690, upload-time = "2025-09-25T05:58:15.052Z" }, - { url = "https://files.pythonhosted.org/packages/36/9a/62a9ba3a919594605a07c34eee3068659bbd648e2fa0c4a86d876810b674/zope_interface-8.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:87e6b089002c43231fb9afec89268391bcc7a3b66e76e269ffde19a8112fb8d5", size = 264201, upload-time = "2025-09-25T06:26:27.797Z" }, + { url = "https://files.pythonhosted.org/packages/b1/fa/6d9eb3a33998a3019d7eb4fa1802d01d6602fad90e0aea443e6e0fe8e49a/zope_interface-8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:788c293f3165964ec6527b2d861072c68eef53425213f36d3893ebee89a89623", size = 207541, upload-time = "2026-01-09T08:04:55.378Z" }, + { url = "https://files.pythonhosted.org/packages/19/8c/ad23c96fdee84cb1f768f6695dac187cc26e9038e01c69713ba0f7dc46ab/zope_interface-8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9a4e785097e741a1c953b3970ce28f2823bd63c00adc5d276f2981dd66c96c15", size = 208075, upload-time = "2026-01-09T08:04:57.118Z" }, + { url = "https://files.pythonhosted.org/packages/dd/35/1bfd5fec31a307f0cf4065ee74ade63858ded3e2a71e248f1508118fcc95/zope_interface-8.2-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:16c69da19a06566664ddd4785f37cad5693a51d48df1515d264c20d005d322e2", size = 249528, upload-time = "2026-01-09T08:04:59.074Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3a/5d50b5fdb0f8226a2edff6adb7efdd3762ec95dff827dbab1761cb9a9e85/zope_interface-8.2-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c31acfa3d7cde48bec45701b0e1f4698daffc378f559bfb296837d8c834732f6", size = 254646, upload-time = "2026-01-09T08:05:00.964Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2a/ee7d675e151578eaf77828b8faac2b7ed9a69fead350bf5cf0e4afe7c73d/zope_interface-8.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0723507127f8269b8f3f22663168f717e9c9742107d1b6c9f419df561b71aa6d", size = 255083, upload-time = "2026-01-09T08:05:02.857Z" }, + { url = "https://files.pythonhosted.org/packages/98/97/9c2aa8caae79915ed64eb114e18816f178984c917aa9adf2a18345e4f2e5/zope_interface-8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c65ade7ea85516e428651048489f5e689e695c79188761de8c622594d1e13322", size = 208081, upload-time = "2026-01-09T08:05:06.623Z" }, + { url = "https://files.pythonhosted.org/packages/34/86/4e2fcb01a8f6780ac84923748e450af0805531f47c0956b83065c99ab543/zope_interface-8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1ef4b43659e1348f35f38e7d1a6bbc1682efde239761f335ffc7e31e798b65b", size = 208522, upload-time = "2026-01-09T08:05:07.986Z" }, + { url = "https://files.pythonhosted.org/packages/f6/eb/08e277da32ddcd4014922854096cf6dcb7081fad415892c2da1bedefbf02/zope_interface-8.2-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:dfc4f44e8de2ff4eba20af4f0a3ca42d3c43ab24a08e49ccd8558b7a4185b466", size = 255198, upload-time = "2026-01-09T08:05:09.532Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a1/b32484f3281a5dc83bc713ad61eca52c543735cdf204543172087a074a74/zope_interface-8.2-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8f094bfb49179ec5dc9981cb769af1275702bd64720ef94874d9e34da1390d4c", size = 259970, upload-time = "2026-01-09T08:05:11.477Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/bca0e8ae1e487d4093a8a7cfed2118aa2d4758c8cfd66e59d2af09d71f1c/zope_interface-8.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d2bb8e7364e18f083bf6744ccf30433b2a5f236c39c95df8514e3c13007098ce", size = 261153, upload-time = "2026-01-09T08:05:13.402Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a0/1e1fabbd2e9c53ef92b69df6d14f4adc94ec25583b1380336905dc37e9a0/zope_interface-8.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:624b6787fc7c3e45fa401984f6add2c736b70a7506518c3b537ffaacc4b29d4c", size = 208785, upload-time = "2026-01-09T08:05:17.348Z" }, + { url = "https://files.pythonhosted.org/packages/c3/2a/88d098a06975c722a192ef1fb7d623d1b57c6a6997cf01a7aabb45ab1970/zope_interface-8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bc9ded9e97a0ed17731d479596ed1071e53b18e6fdb2fc33af1e43f5fd2d3aaa", size = 208976, upload-time = "2026-01-09T08:05:18.792Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e8/757398549fdfd2f8c89f32c82ae4d2f0537ae2a5d2f21f4a2f711f5a059f/zope_interface-8.2-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:532367553e4420c80c0fc0cabcc2c74080d495573706f66723edee6eae53361d", size = 259411, upload-time = "2026-01-09T08:05:20.567Z" }, + { url = "https://files.pythonhosted.org/packages/91/af/502601f0395ce84dff622f63cab47488657a04d0065547df42bee3a680ff/zope_interface-8.2-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2bf9cf275468bafa3c72688aad8cfcbe3d28ee792baf0b228a1b2d93bd1d541a", size = 264859, upload-time = "2026-01-09T08:05:22.234Z" }, + { url = "https://files.pythonhosted.org/packages/89/0c/d2f765b9b4814a368a7c1b0ac23b68823c6789a732112668072fe596945d/zope_interface-8.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0009d2d3c02ea783045d7804da4fd016245e5c5de31a86cebba66dd6914d59a2", size = 264398, upload-time = "2026-01-09T08:05:23.853Z" }, + { url = "https://files.pythonhosted.org/packages/66/47/45188fb101fa060b20e6090e500682398ab415e516a0c228fbb22bc7def2/zope_interface-8.2-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:6068322004a0158c80dfd4708dfb103a899635408c67c3b10e9acec4dbacefec", size = 209170, upload-time = "2026-01-09T08:05:26.616Z" }, + { url = "https://files.pythonhosted.org/packages/09/03/f6b9336c03c2b48403c4eb73a1ec961d94dc2fb5354c583dfb5fa05fd41f/zope_interface-8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2499de92e8275d0dd68f84425b3e19e9268cd1fa8507997900fa4175f157733c", size = 209229, upload-time = "2026-01-09T08:05:28.521Z" }, + { url = "https://files.pythonhosted.org/packages/07/b1/65fe1dca708569f302ade02e6cdca309eab6752bc9f80105514f5b708651/zope_interface-8.2-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f777e68c76208503609c83ca021a6864902b646530a1a39abb9ed310d1100664", size = 259393, upload-time = "2026-01-09T08:05:29.897Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a5/97b49cfceb6ed53d3dcfb3f3ebf24d83b5553194f0337fbbb3a9fec6cf78/zope_interface-8.2-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b05a919fdb0ed6ea942e5a7800e09a8b6cdae6f98fee1bef1c9d1a3fc43aaa0", size = 264863, upload-time = "2026-01-09T08:05:31.501Z" }, + { url = "https://files.pythonhosted.org/packages/cb/02/0b7a77292810efe3a0586a505b077ebafd5114e10c6e6e659f0c8e387e1f/zope_interface-8.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ccc62b5712dd7bd64cfba3ee63089fb11e840f5914b990033beeae3b2180b6cb", size = 264369, upload-time = "2026-01-09T08:05:32.941Z" }, + { url = "https://files.pythonhosted.org/packages/1a/da/3c89de3917751446728b8898b4d53318bc2f8f6bf8196e150a063c59905e/zope_interface-8.2-cp314-cp314-macosx_10_9_x86_64.whl", hash = "sha256:46c7e4e8cbc698398a67e56ca985d19cb92365b4aafbeb6a712e8c101090f4cb", size = 209223, upload-time = "2026-01-09T08:05:36.449Z" }, + { url = "https://files.pythonhosted.org/packages/00/7f/62d00ec53f0a6e5df0c984781e6f3999ed265129c4c3413df8128d1e0207/zope_interface-8.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a87fc7517f825a97ff4a4ca4c8a950593c59e0f8e7bfe1b6f898a38d5ba9f9cf", size = 209366, upload-time = "2026-01-09T08:05:38.197Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a2/f241986315174be8e00aabecfc2153cf8029c1327cab8ed53a9d979d7e08/zope_interface-8.2-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:ccf52f7d44d669203c2096c1a0c2c15d52e36b2e7a9413df50f48392c7d4d080", size = 261037, upload-time = "2026-01-09T08:05:39.568Z" }, + { url = "https://files.pythonhosted.org/packages/02/cc/b321c51d6936ede296a1b8860cf173bee2928357fe1fff7f97234899173f/zope_interface-8.2-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aae807efc7bd26302eb2fea05cd6de7d59269ed6ae23a6de1ee47add6de99b8c", size = 264219, upload-time = "2026-01-09T08:05:41.624Z" }, + { url = "https://files.pythonhosted.org/packages/ab/fb/5f5e7b40a2f4efd873fe173624795ca47eaa22e29051270c981361b45209/zope_interface-8.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05a0e42d6d830f547e114de2e7cd15750dc6c0c78f8138e6c5035e51ddfff37c", size = 264390, upload-time = "2026-01-09T08:05:42.936Z" }, ] [[package]] @@ -4526,9 +6444,11 @@ name = "zxing-cpp" version = "2.3.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.11' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", "python_full_version < '3.11' and sys_platform == 'darwin'", - "(python_full_version >= '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'linux'", "python_full_version < '3.11' and sys_platform == 'linux'", ] sdist = { url = "https://files.pythonhosted.org/packages/d9/f2/b781bf6119abe665069777e3c0f154752cf924fe8a55fca027243abbc555/zxing_cpp-2.3.0.tar.gz", hash = "sha256:3babedb67a4c15c9de2c2b4c42d70af83a6c85780c1b2d9803ac64c6ae69f14e", size = 1172666, upload-time = "2025-01-01T21:54:05.856Z" }