diff --git a/.env.example b/.env.example index 63df700..998150c 100644 --- a/.env.example +++ b/.env.example @@ -4,10 +4,6 @@ # When the environment is "development", DEBUG is set to True. SERVALA_ENVIRONMENT='development' -# Set to "False" to disable the beta testing banner at the top of every page. -# Defaults to "True". -SERVALA_SHOW_BETA_BANNER='True' - # Set SERVALA_PREVIOUS_SECRET_KEY when rotating to a new secret key in order to not expire all sessions and to remain able to read encrypted fields! # In order to retire the previous key, run the ``reencrypt_fields`` command. Once you drop the previous secret key from # the rotation, all sessions that still rely on that key will be invalidated (i.e., users will have to log in again). diff --git a/.forgejo/workflows/build-deploy-prod.yaml b/.forgejo/workflows/build-deploy-prod.yaml index ddaeb1c..53d1e32 100644 --- a/.forgejo/workflows/build-deploy-prod.yaml +++ b/.forgejo/workflows/build-deploy-prod.yaml @@ -12,9 +12,6 @@ on: - "pyproject.toml" - "uv.lock" workflow_dispatch: - release: - types: - - published jobs: build: @@ -26,7 +23,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v5 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -72,7 +69,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v5 - name: Determine image tag id: determine-tag diff --git a/.forgejo/workflows/build-deploy-staging.yaml b/.forgejo/workflows/build-deploy-staging.yaml index 8f438cd..93c77b2 100644 --- a/.forgejo/workflows/build-deploy-staging.yaml +++ b/.forgejo/workflows/build-deploy-staging.yaml @@ -22,7 +22,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v5 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -53,7 +53,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v5 - name: Deploy to OpenShift uses: docker://quay.io/appuio/oc:v4.19 diff --git a/.forgejo/workflows/docs.yaml b/.forgejo/workflows/docs.yaml index b1e5fe5..0b6c77c 100644 --- a/.forgejo/workflows/docs.yaml +++ b/.forgejo/workflows/docs.yaml @@ -17,7 +17,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v5 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -49,7 +49,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v5 - name: Deploy to OpenShift uses: docker://quay.io/appuio/oc:v4.19 diff --git a/.forgejo/workflows/renovate.yaml b/.forgejo/workflows/renovate.yaml index a2577d9..97f1c2b 100644 --- a/.forgejo/workflows/renovate.yaml +++ b/.forgejo/workflows/renovate.yaml @@ -11,15 +11,15 @@ jobs: container: catthehacker/ubuntu:act-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v5 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v5 with: - node-version: "24" + node-version: "22" - name: Renovate - uses: https://github.com/renovatebot/github-action@v44.0.5 + uses: https://github.com/renovatebot/github-action@v43.0.14 with: token: ${{ secrets.RENOVATE_TOKEN }} env: diff --git a/.forgejo/workflows/tests.yaml b/.forgejo/workflows/tests.yaml index b2cddd0..767e526 100644 --- a/.forgejo/workflows/tests.yaml +++ b/.forgejo/workflows/tests.yaml @@ -18,15 +18,15 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v5 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v5 with: - node-version: "24" + node-version: "22" - name: Install uv - uses: https://github.com/astral-sh/setup-uv@v7 + uses: https://github.com/astral-sh/setup-uv@v6 - name: Run tests run: uv run --env-file=.env.example pytest diff --git a/.python-version b/.python-version index 6324d40..24ee5b1 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.14 +3.13 diff --git a/Dockerfile b/Dockerfile index b1af0a7..5727f03 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.14-slim +FROM python:3.13-slim EXPOSE 8000 WORKDIR /app diff --git a/README.md b/README.md index eaa1cdd..536d69f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ The Servala Self-Service Portal -Latest release: 2025.11.17-0 +Latest release: 2025.10.03-0 ## Documentation diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index d133fa2..cff0b83 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -5,7 +5,6 @@ ** xref:web-portal-admin.adoc[Admin] ** xref:web-portal-controlplanes.adoc[Control-Planes] ** xref:web-portal-billingentity.adoc[Billing Entities] -** xref:web-portal-changelog.adoc[Changelog] * xref:web-portal-planning.adoc[] ** xref:user-stories.adoc[] @@ -17,4 +16,4 @@ ** xref:api.adoc[] * Cloud Providers -** xref:exoscale-osb.adoc[] +** xref:exoscale-osb.adoc[] \ No newline at end of file diff --git a/docs/modules/ROOT/pages/web-portal-changelog.adoc b/docs/modules/ROOT/pages/web-portal-changelog.adoc deleted file mode 100644 index 0c5de9c..0000000 --- a/docs/modules/ROOT/pages/web-portal-changelog.adoc +++ /dev/null @@ -1,97 +0,0 @@ -= Portal Changelog - -== 2025.11.17-0 - -=== API -* Exoscale offboarding MVP (link:https://servala.app.codey.ch/servala/servala-portal/pulls/282[#282]) - -=== UI/UX -* Allow admins to disable the expert mode form (link:https://servala.app.codey.ch/servala/servala-portal/pulls/296[#296]) -* Support single (non-array) FQDN values (link:https://servala.app.codey.ch/servala/servala-portal/pulls/295[#295]) -* "View Availability" is now "Get It" (link:https://servala.app.codey.ch/servala/servala-portal/pulls/285[#285]) -* Add "open" button to instances with FQDN (link:https://servala.app.codey.ch/servala/servala-portal/pulls/283[#283]) -* Hide billing addresses (link:https://servala.app.codey.ch/servala/servala-portal/pulls/281[#281]) -* Custom form configuration (link:https://servala.app.codey.ch/servala/servala-portal/pulls/268[#268]) -* Skip offering selection if there is only one (link:https://servala.app.codey.ch/servala/servala-portal/pulls/273[#273]) -* Make it more clear how to register an account (link:https://servala.app.codey.ch/servala/servala-portal/pulls/270[#270]) - -=== dependencies -* Update dependency django-template-partials to >=25.3 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/297[#297]) -* Lock file maintenance (link:https://servala.app.codey.ch/servala/servala-portal/pulls/298[#298]) -* Update dependency pytest to >=9.0.1 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/284[#284]) -* Update Python to 3.14 tag (link:https://servala.app.codey.ch/servala/servala-portal/pulls/272[#272]) -* Update dependency django-fernet-encrypted-fields to >=0.3.1 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/278[#278]) -* Update https://github.com/renovatebot/github-action action to v44.0.2 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/279[#279]) -* Update dependency sentry-sdk to >=2.44.0 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/280[#280]) -* Update dependency coverage to >=7.11.3 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/274[#274]) -* Update dependency pytest to v9 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/276[#276]) -* Update dependency black to >=25.11.0 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/277[#277]) -* Update https://github.com/renovatebot/github-action action to v44 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/275[#275]) -* Update dependency django to v5.2.8 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/271[#271]) -* Update dependency django-allauth to >=65.13.0 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/265[#265]) -* Lock file maintenance (link:https://servala.app.codey.ch/servala/servala-portal/pulls/266[#266]) -* Update https://github.com/renovatebot/github-action action to v43.0.20 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/267[#267]) -* Update https://github.com/renovatebot/github-action action to v43.0.19 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/259[#259]) -* Update dependency node to v24 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/260[#260]) -* Update dependency sentry-sdk to >=2.43.0 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/261[#261]) - - -== 2025.11.13-0 - -=== UI/UX -* "View Availability" is now "Get It" (link:https://servala.app.codey.ch/servala/servala-portal/pulls/285[#285]) -* Add "open" button to instances with FQDN (link:https://servala.app.codey.ch/servala/servala-portal/pulls/283[#283]) -* Hide billing addresses (link:https://servala.app.codey.ch/servala/servala-portal/pulls/281[#281]) -* Custom form configuration (link:https://servala.app.codey.ch/servala/servala-portal/pulls/268[#268]) -* Skip offering selection if there is only one (link:https://servala.app.codey.ch/servala/servala-portal/pulls/273[#273]) -* Make it more clear how to register an account (link:https://servala.app.codey.ch/servala/servala-portal/pulls/270[#270]) -* Restrict user input to more sensible ranges (link:https://servala.app.codey.ch/servala/servala-portal/pulls/251[#251]) -* Inline user info in service offering page (link:https://servala.app.codey.ch/servala/servala-portal/pulls/250[#250]) - -=== bug -* Fix generated FQDN not being submitted (link:https://servala.app.codey.ch/servala/servala-portal/pulls/249[#249]) - -=== dependencies -* Update dependency pytest to >=9.0.1 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/284[#284]) -* Update Python to 3.14 tag (link:https://servala.app.codey.ch/servala/servala-portal/pulls/272[#272]) -* Update dependency django-fernet-encrypted-fields to >=0.3.1 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/278[#278]) -* Update https://github.com/renovatebot/github-action action to v44.0.2 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/279[#279]) -* Update dependency sentry-sdk to >=2.44.0 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/280[#280]) -* Update dependency coverage to >=7.11.3 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/274[#274]) -* Update dependency pytest to v9 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/276[#276]) -* Update dependency black to >=25.11.0 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/277[#277]) -* Update https://github.com/renovatebot/github-action action to v44 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/275[#275]) -* Update dependency django to v5.2.8 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/271[#271]) -* Update dependency django-allauth to >=65.13.0 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/265[#265]) -* Lock file maintenance (link:https://servala.app.codey.ch/servala/servala-portal/pulls/266[#266]) -* Update https://github.com/renovatebot/github-action action to v43.0.20 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/267[#267]) -* Update https://github.com/renovatebot/github-action action to v43.0.19 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/259[#259]) -* Update dependency node to v24 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/260[#260]) -* Update dependency sentry-sdk to >=2.43.0 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/261[#261]) -* Update dependency isort to v7 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/252[#252]) -* Update dependency pillow to v12 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/253[#253]) -* Lock file maintenance (link:https://servala.app.codey.ch/servala/servala-portal/pulls/255[#255]) -* Update https://github.com/astral-sh/setup-uv action to v7 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/254[#254]) -* Update dependency flake8-bugbear to v25 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/248[#248]) -* Update https://github.com/renovatebot/github-action action to v43.0.18 - autoclosed (link:https://servala.app.codey.ch/servala/servala-portal/pulls/239[#239]) -* Update actions/setup-node action to v6 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/247[#247]) - - -== 2025.10.27-0 - -=== UI/UX -* Restrict user input to more sensible ranges (link:https://servala.app.codey.ch/servala/servala-portal/pulls/251[#251]) -* Inline user info in service offering page (link:https://servala.app.codey.ch/servala/servala-portal/pulls/250[#250]) - -=== bug -* Fix generated FQDN not being submitted (link:https://servala.app.codey.ch/servala/servala-portal/pulls/249[#249]) - -=== dependencies -* Update dependency isort to v7 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/252[#252]) -* Update dependency pillow to v12 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/253[#253]) -* Lock file maintenance (link:https://servala.app.codey.ch/servala/servala-portal/pulls/255[#255]) -* Update https://github.com/astral-sh/setup-uv action to v7 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/254[#254]) -* Update dependency flake8-bugbear to v25 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/248[#248]) -* Update https://github.com/renovatebot/github-action action to v43.0.18 - autoclosed (link:https://servala.app.codey.ch/servala/servala-portal/pulls/239[#239]) -* Update actions/setup-node action to v6 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/247[#247]) - diff --git a/hack/README.md b/hack/README.md deleted file mode 100644 index 6626efc..0000000 --- a/hack/README.md +++ /dev/null @@ -1,116 +0,0 @@ -# Automation Scripts - -This directory contains automation scripts for release management and changelog generation. - -## Scripts - -### bumpver-pre-commit-hook.sh - -**Purpose**: Generates a changelog based on merged Pull Requests since the last release. - -**What it does**: -1. Queries the Forgejo API for merged pull requests since the last release tag -2. Groups pull requests by their labels (first label if multiple labels are present) -3. Formats the changes in AsciiDoc format with third-level headers for each label group -4. Appends the changelog to `docs/modules/ROOT/pages/web-portal-changelog.adoc` -5. Adds the changelog file to git staging area -6. Saves the changelog content for the post-commit hook - -**Note**: Pull requests without labels will be grouped under "Uncategorized". - -**Requirements**: -- `FORGEJO_TOKEN` environment variable must be set with a valid Forgejo API token -- `jq` command-line JSON processor must be installed -- `curl` must be installed - -### bumpver-post-commit-hook.sh - -**Purpose**: Creates a release on Forgejo after a version bump. - -**What it does**: -1. Gets the current version from `pyproject.toml` -2. Reads the changelog content generated by the pre-commit hook -3. Converts AsciiDoc format to Markdown (headers and links) -4. Creates or updates a release on Forgejo with the Markdown-formatted changelog -5. Cleans up temporary changelog files - -**Note**: The script automatically converts AsciiDoc syntax to Markdown for Forgejo releases: -- `=== Header` → `### Header` -- `link:url[text]` → `[text](url)` - -**Requirements**: -- `FORGEJO_TOKEN` environment variable must be set with a valid Forgejo API token -- `jq` command-line JSON processor must be installed -- `curl` must be installed - -## Setup - -### 1. Generate a Forgejo API Token - -1. Log in to Forgejo at https://servala.app.codey.ch -2. Go to Settings → Applications → Generate New Token -3. Give it a descriptive name (e.g., "bumpver-automation") -4. Select the required permissions: - - `repo` (Full control of repositories) -5. Copy the generated token - -### 2. Configure the token - -Export the token as an environment variable: - -```bash -export FORGEJO_TOKEN="your-token-here" -``` - -For permanent setup, add it to your shell profile (`~/.bashrc`, `~/.zshrc`, etc.): - -```bash -echo 'export FORGEJO_TOKEN="your-token-here"' >> ~/.bashrc -``` - -### 3. Update pyproject.toml - -Update the bumpver configuration in `pyproject.toml` to use these hooks: - -```toml -[tool.bumpver] -current_version = "2025.10.27-0" -version_pattern = "YYYY.0M.0D-INC0" -commit_message = "bump version {old_version} -> {new_version}" -tag_message = "{new_version}" -tag_scope = "default" -pre_commit_hook = "hack/bumpver-pre-commit-hook.sh" -post_commit_hook = "hack/bumpver-post-commit-hook.sh" -commit = true -tag = true -push = true -``` - -## Usage - -Once configured, the hooks will run automatically when you bump the version: - -```bash -# Or let bumpver determine the version based on the pattern -uvx bumpver update -``` - -The workflow is: -1. `bumpver` updates version files -2. **Pre-commit hook** runs: generates changelog, updates changelog file, stages changes -3. `bumpver` creates commit with version bump and changelog -4. `bumpver` creates git tag -5. **Post-commit hook** runs: creates Forgejo release -6. `bumpver` pushes commit and tags to remote - -## Manual execution - -You can also run the scripts manually: - -```bash -# Generate changelog (run before committing) -./hack/pre-commit-hook.sh - -# Create release (run after committing and tagging) -./hack/post-commit-hook.sh -``` diff --git a/hack/bumpver-post-commit-hook.sh b/hack/bumpver-post-commit-hook.sh deleted file mode 100755 index fdda006..0000000 --- a/hack/bumpver-post-commit-hook.sh +++ /dev/null @@ -1,149 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# Post-commit hook for bumpver to create a Forgejo release -# This script creates a release on Forgejo using the changelog generated in the pre-commit hook - -# Configuration -FORGEJO_URL="https://servala.app.codey.ch" -REPO_OWNER="servala" -REPO_NAME="servala-portal" - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Check for required environment variable -if [ -z "${FORGEJO_TOKEN:-}" ]; then - echo -e "${RED}Error: FORGEJO_TOKEN environment variable is not set${NC}" - echo "Please set FORGEJO_TOKEN to your Forgejo API token" - exit 1 -fi - -# Get the current version from pyproject.toml -CURRENT_VERSION=$(grep -E 'current_version = ".*"' pyproject.toml | head -1 | sed -E 's/current_version = "(.*)"/\1/') -echo -e "${GREEN}Creating release for version: ${CURRENT_VERSION}${NC}" - -# Get the latest tag (should match CURRENT_VERSION) -LATEST_TAG=$(git tag -l --sort=-v:refname | head -1) -if [ "$LATEST_TAG" != "$CURRENT_VERSION" ]; then - echo -e "${YELLOW}Warning: Latest tag (${LATEST_TAG}) doesn't match current version (${CURRENT_VERSION})${NC}" - echo -e "${YELLOW}Using current version: ${CURRENT_VERSION}${NC}" -fi - -# Try to read the changelog generated by the pre-commit hook -CHANGELOG_DIR=".git/changelog" -CHANGELOG_FILE="${CHANGELOG_DIR}/${CURRENT_VERSION}.txt" - -if [ -f "$CHANGELOG_FILE" ]; then - CHANGELOG_CONTENT=$(cat "$CHANGELOG_FILE") - echo -e "${GREEN}Found changelog content from pre-commit hook${NC}" -else - echo -e "${YELLOW}Warning: Changelog file not found at ${CHANGELOG_FILE}${NC}" - echo -e "${YELLOW}Generating changelog from tag information${NC}" - - # Fallback: use git log to get commits since previous tag - PREVIOUS_TAG=$(git tag -l --sort=-v:refname | head -2 | tail -1) - if [ -n "$PREVIOUS_TAG" ]; then - CHANGELOG_CONTENT=$(git log --pretty=format:"* %s" "${PREVIOUS_TAG}..${LATEST_TAG}") - else - CHANGELOG_CONTENT="* Initial release" - fi -fi - -# Convert AsciiDoc to Markdown -# 1. Convert === headers to ### (third-level headers) -# 2. Convert link:url[text] to [text](url) -CHANGELOG_MARKDOWN=$(echo "$CHANGELOG_CONTENT" | sed -E 's/^=== /### /g' | sed -E 's/link:([^[]+)\[([^]]+)\]/[\2](\1)/g') - -# Create release body in Markdown format -RELEASE_BODY="## Release ${CURRENT_VERSION} - -${CHANGELOG_MARKDOWN} -" - -# Check if release already exists -API_URL="${FORGEJO_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases/tags/${CURRENT_VERSION}" -EXISTING_RELEASE=$(curl -s -H "Authorization: token ${FORGEJO_TOKEN}" "${API_URL}") - -# Check if we got a release back (not an error) -if echo "$EXISTING_RELEASE" | jq -e '.id' > /dev/null 2>&1; then - echo -e "${YELLOW}Release ${CURRENT_VERSION} already exists${NC}" - RELEASE_ID=$(echo "$EXISTING_RELEASE" | jq -r '.id') - echo -e "${GREEN}Updating existing release (ID: ${RELEASE_ID})${NC}" - - # Update the existing release - UPDATE_URL="${FORGEJO_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases/${RELEASE_ID}" - - RESPONSE=$(curl -s -X PATCH \ - -H "Authorization: token ${FORGEJO_TOKEN}" \ - -H "Content-Type: application/json" \ - -d "$(jq -n \ - --arg tag "${CURRENT_VERSION}" \ - --arg name "Release ${CURRENT_VERSION}" \ - --arg body "${RELEASE_BODY}" \ - '{ - tag_name: $tag, - name: $name, - body: $body - }')" \ - "${UPDATE_URL}") - - if echo "$RESPONSE" | jq -e '.id' > /dev/null 2>&1; then - RELEASE_URL=$(echo "$RESPONSE" | jq -r '.html_url') - echo -e "${GREEN}Release updated successfully!${NC}" - echo -e "${GREEN}Release URL: ${RELEASE_URL}${NC}" - else - echo -e "${RED}Error updating release${NC}" - echo "$RESPONSE" | jq . - exit 1 - fi -else - echo -e "${GREEN}Creating new release${NC}" - - # Create new release - CREATE_URL="${FORGEJO_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases" - - RESPONSE=$(curl -s -X POST \ - -H "Authorization: token ${FORGEJO_TOKEN}" \ - -H "Content-Type: application/json" \ - -d "$(jq -n \ - --arg tag "${CURRENT_VERSION}" \ - --arg name "Release ${CURRENT_VERSION}" \ - --arg body "${RELEASE_BODY}" \ - '{ - tag_name: $tag, - name: $name, - body: $body, - draft: false, - prerelease: false - }')" \ - "${CREATE_URL}") - - if echo "$RESPONSE" | jq -e '.id' > /dev/null 2>&1; then - RELEASE_URL=$(echo "$RESPONSE" | jq -r '.html_url') - echo -e "${GREEN}Release created successfully!${NC}" - echo -e "${GREEN}Release URL: ${RELEASE_URL}${NC}" - else - echo -e "${RED}Error creating release${NC}" - echo "$RESPONSE" | jq . - exit 1 - fi -fi - -# Clean up the changelog file -if [ -f "$CHANGELOG_FILE" ]; then - rm -f "$CHANGELOG_FILE" -fi - -# Fetch the tag that Forgejo created when we made the release -echo -e "${GREEN}Fetching tags from remote to sync the tag created by Forgejo${NC}" -if git fetch --tags; then - echo -e "${GREEN}Tags synced successfully${NC}" -else - echo -e "${YELLOW}Warning: Failed to fetch tags from remote${NC}" -fi - -exit 0 diff --git a/hack/bumpver-pre-commit-hook.sh b/hack/bumpver-pre-commit-hook.sh deleted file mode 100755 index 89ce3c4..0000000 --- a/hack/bumpver-pre-commit-hook.sh +++ /dev/null @@ -1,134 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# Pre-commit hook for bumpver to generate changelog from Forgejo merge requests -# This script queries the Forgejo API for merged pull requests since the last release -# and appends them to the changelog file in AsciiDoc format - -# Configuration -FORGEJO_URL="https://servala.app.codey.ch" -REPO_OWNER="servala" -REPO_NAME="servala-portal" -CHANGELOG_FILE="docs/modules/ROOT/pages/web-portal-changelog.adoc" - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Check for required environment variable -if [ -z "${FORGEJO_TOKEN:-}" ]; then - echo -e "${RED}Error: FORGEJO_TOKEN environment variable is not set${NC}" - echo "Please set FORGEJO_TOKEN to your Forgejo API token" - exit 1 -fi - -# Get the new version from pyproject.toml (bumpver updates this before running the hook) -NEW_VERSION=$(grep -E 'current_version = ".*"' pyproject.toml | head -1 | sed -E 's/current_version = "(.*)"/\1/') -echo -e "${GREEN}Generating changelog for version: ${NEW_VERSION}${NC}" - -# Get the previous tag -PREVIOUS_TAG=$(git tag -l --sort=-v:refname | head -2 | tail -1) -if [ -z "$PREVIOUS_TAG" ]; then - echo -e "${YELLOW}Warning: No previous tag found, using all merge requests${NC}" - # Get date of first commit - SINCE_DATE=$(git log --reverse --format=%aI | head -1) -else - echo -e "${GREEN}Previous version: ${PREVIOUS_TAG}${NC}" - # Get the date of the previous tag - SINCE_DATE=$(git log -1 --format=%aI "${PREVIOUS_TAG}") -fi - -echo -e "${GREEN}Fetching merge requests since ${SINCE_DATE}${NC}" - -# Query Forgejo API for closed/merged pull requests -# Forgejo API returns pull requests sorted by updated time -API_URL="${FORGEJO_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/pulls?state=closed&sort=updated&limit=100" - -RESPONSE=$(curl -s -H "Authorization: token ${FORGEJO_TOKEN}" "${API_URL}") - -# Check if curl was successful -if [ $? -ne 0 ]; then - echo -e "${RED}Error: Failed to fetch pull requests from Forgejo API${NC}" - exit 1 -fi - -# Filter merged PRs since the previous tag and group by labels -# Using jq to parse JSON, filter by merge date, and group by labels -CHANGELOG_ENTRIES=$(echo "${RESPONSE}" | jq -r --arg since "${SINCE_DATE}" ' - # Filter merged PRs since the last tag - [.[] | select(.merged_at != null and .merged_at > $since)] | - sort_by(.merged_at) | reverse | - - # Group by primary label (first label if multiple, or "Uncategorized") - group_by( - if (.labels | length) > 0 then - .labels[0].name - else - "Uncategorized" - end - ) | - - # Format each group - map( - # Group header - "=== " + ( - if .[0].labels | length > 0 then - .[0].labels[0].name - else - "Uncategorized" - end - ) + "\n" + - - # List items in this group - (map("* " + .title + " (link:" + .html_url + "[#" + (.number | tostring) + "])") | join("\n")) - ) | - join("\n\n") -') - -if [ -z "$CHANGELOG_ENTRIES" ]; then - echo -e "${YELLOW}Warning: No merged pull requests found since ${PREVIOUS_TAG}${NC}" - CHANGELOG_ENTRIES="=== Uncategorized\n\n* No changes recorded" -fi - -# Create changelog section -CHANGELOG_SECTION=" -== ${NEW_VERSION} - -${CHANGELOG_ENTRIES} -" - -# Check if changelog file exists -if [ ! -f "$CHANGELOG_FILE" ]; then - echo -e "${RED}Error: Changelog file ${CHANGELOG_FILE} not found${NC}" - exit 1 -fi - -# Create temporary file with new changelog entry at the top -TMP_FILE=$(mktemp) -{ - # Keep the title - head -1 "$CHANGELOG_FILE" - # Add the new changelog section - echo "$CHANGELOG_SECTION" - # Add the rest of the file (skip the title) - tail -n +2 "$CHANGELOG_FILE" -} > "$TMP_FILE" - -# Replace the original file -mv "$TMP_FILE" "$CHANGELOG_FILE" -chmod 0644 "$CHANGELOG_FILE" - -# Add the changelog file to git -git add "$CHANGELOG_FILE" - -echo -e "${GREEN}Changelog updated successfully${NC}" -echo -e "${GREEN}Added ${CHANGELOG_FILE} to git staging area${NC}" - -# Save changelog for post-commit hook -CHANGELOG_DIR=".git/changelog" -mkdir -p "$CHANGELOG_DIR" -echo "$CHANGELOG_ENTRIES" > "${CHANGELOG_DIR}/${NEW_VERSION}.txt" - -exit 0 diff --git a/pyproject.toml b/pyproject.toml index 4548d9f..467a9c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,41 +3,41 @@ name = "servala" version = "0.0.0" description = "Servala portal server and frontend" readme = "README.md" -requires-python = ">=3.14.0" +requires-python = ">=3.13" dependencies = [ "argon2-cffi>=25.1.0", - "cryptography>=46.0.3", - "django==5.2.8", - "django-allauth>=65.13.1", - "django-auditlog>=3.3.0", - "django-fernet-encrypted-fields>=0.3.1", + "cryptography>=46.0.1", + "django==5.2.7", + "django-allauth>=65.11.2", + "django-auditlog>=3.2.1", + "django-fernet-encrypted-fields>=0.3.0", "django-jsonform>=2.23.2", "django-scopes>=2.0.0", "django-storages[s3]>=1.14.6", - "django-template-partials>=25.3", + "django-template-partials>=25.2", "jsonschema>=4.25.1", - "kubernetes>=34.1.0", - "pillow>=12.0.0", - "psycopg2-binary>=2.9.11", + "kubernetes>=33.1.0", + "pillow>=11.3.0", + "psycopg2-binary>=2.9.10", "pyjwt>=2.10.1", "requests>=2.32.5", "rules>=3.5", - "sentry-sdk[django]>=2.46.0", + "sentry-sdk[django]>=2.39.0", "urlman>=2.0.2", ] [dependency-groups] dev = [ - "black>=25.11.0", + "black>=25.9.0", "bumpver>=2025.1131", - "coverage>=7.12.0", + "coverage>=7.10.7", "djlint>=1.36.4", "flake8>=7.3.0", - "flake8-bugbear>=25.11.29", + "flake8-bugbear>=24.12.12", "flake8-pyproject>=1.2.3", - "isort>=7.0.0", - "pytest>=9.0.1", - "pytest-cov>=7.0.0", + "isort>=6.0.1", + "pytest>=8.4.2", + "pytest-cov>=6.3.0", "pytest-django>=4.11.1", "pytest-mock>=3.15.1", ] @@ -61,15 +61,15 @@ testpaths = "src/tests" pythonpath = "src" [tool.bumpver] -current_version = "2025.11.17-0" +current_version = "2025.10.03-0" version_pattern = "YYYY.0M.0D-INC0" commit_message = "bump version {old_version} -> {new_version}" tag_message = "{new_version}" tag_scope = "default" -pre_commit_hook = "hack/bumpver-pre-commit-hook.sh" -post_commit_hook = "hack/bumpver-post-commit-hook.sh" +pre_commit_hook = "" +post_commit_hook = "" commit = true -tag = false +tag = true push = true [tool.bumpver.file_patterns] diff --git a/src/servala/__about__.py b/src/servala/__about__.py index d6db270..f1e4ad8 100644 --- a/src/servala/__about__.py +++ b/src/servala/__about__.py @@ -1 +1 @@ -__version__ = "2025.11.17-0" +__version__ = "2025.10.03-0" diff --git a/src/servala/api/views.py b/src/servala/api/views.py index 015e091..48845d0 100644 --- a/src/servala/api/views.py +++ b/src/servala/api/views.py @@ -1,27 +1,20 @@ import json import logging +from contextlib import suppress from django.conf import settings from django.contrib.auth.decorators import login_not_required from django.core.mail import send_mail from django.db import transaction from django.http import JsonResponse -from django.urls import reverse from django.utils.decorators import method_decorator from django.views import View from django.views.decorators.csrf import csrf_exempt from servala.api.permissions import OSBBasicAuthPermission from servala.core.exoscale import get_exoscale_origin -from servala.core.models import ( - BillingEntity, - Organization, - OrganizationInvitation, - OrganizationRole, - User, -) -from servala.core.models.service import Service, ServiceInstance, ServiceOffering -from servala.core.odoo import create_helpdesk_ticket +from servala.core.models import BillingEntity, Organization, User +from servala.core.models.service import Service, ServiceOffering logger = logging.getLogger(__name__) @@ -30,7 +23,9 @@ logger = logging.getLogger(__name__) @method_decorator(login_not_required, name="dispatch") class OSBServiceInstanceView(OSBBasicAuthPermission, View): """ - OSB API endpoint for service instance management via Exoscale. + OSB API endpoint for service instance provisioning (onboarding). + Implements the PUT /v2/service_instances/:instance_id endpoint. + https://docs.servala.com/exoscale-osb.html#_onboarding """ def _error(self, error): @@ -107,47 +102,66 @@ class OSBServiceInstanceView(OSBBasicAuthPermission, View): ) exoscale_origin = get_exoscale_origin() - try: + with suppress(Organization.DoesNotExist): organization = Organization.objects.get( osb_guid=organization_guid, origin=exoscale_origin ) - if service in organization.limit_osb_services.all(): - return JsonResponse({"message": "Service already enabled"}, status=200) - except Organization.DoesNotExist: - try: - with transaction.atomic(): - if exoscale_origin.billing_entity: - billing_entity = exoscale_origin.billing_entity - else: - odoo_data = { - "company_name": organization_display_name, - "invoice_email": user.email, - } - billing_entity = BillingEntity.create_from_data( - name=f"{organization_display_name} (Exoscale)", - odoo_data=odoo_data, - ) - organization = Organization( - name=organization_display_name, - billing_entity=billing_entity, - origin=exoscale_origin, - osb_guid=organization_guid, - ) - organization = Organization.create_organization(organization) - invitation = OrganizationInvitation.objects.create( - organization=organization, - email=user.email.lower(), - role=OrganizationRole.OWNER, - ) - invitation.send_invitation_email(request) - except Exception: - return JsonResponse({"error": "Internal server error"}, status=500) + self._send_service_welcome_email( + request, organization, user, service, service_offering + ) + return JsonResponse({"message": "Service already enabled"}, status=200) - organization.limit_osb_services.add(service) - self._send_service_welcome_email( - request, organization, user, service, service_offering + odoo_data = { + "company_name": organization_display_name, + "invoice_email": user.email, + } + try: + with transaction.atomic(): + billing_entity = BillingEntity.create_from_data( + name=f"{organization_display_name} (Exoscale)", odoo_data=odoo_data + ) + organization = Organization( + name=organization_display_name, + billing_entity=billing_entity, + origin=exoscale_origin, + osb_guid=organization_guid, + ) + organization = Organization.create_organization(organization, user) + + self._send_invitation_email(request, organization, user) + self._send_service_welcome_email( + request, organization, user, service, service_offering + ) + + return JsonResponse( + {"message": "Successfully enabled service"}, status=201 + ) + + except Exception as e: + logger.error(f"Error creating organization for Exoscale: {str(e)}") + return JsonResponse({"error": "Internal server error"}, status=500) + + def _send_invitation_email(self, request, organization, user): + subject = f"Welcome to Servala - {organization.name}" + url = request.build_absolute_uri(organization.urls.base) + message = f"""Hello {user.first_name or user.email}, + +You have been invited to join the organization "{organization.name}" on Servala Portal. + +You can access your organization at: {url} + +Please use this email address ({user.email}) when prompted to log in. + +Best regards, +The Servala Team""" + + send_mail( + subject=subject, + message=message, + from_email=settings.EMAIL_DEFAULT_FROM, + recipient_list=[user.email], + fail_silently=False, ) - return JsonResponse({"message": "Successfully enabled service"}, status=201) def _send_service_welcome_email( self, request, organization, user, service, service_offering @@ -177,169 +191,3 @@ The Servala Team""" recipient_list=[user.email], fail_silently=False, ) - - def delete(self, request, instance_id): - """ - This implements the Exoscale offboarding flow MVP. - https://docs.servala.com/exoscale-osb.html#_offboarding - """ - service_id = request.GET.get("service_id") - plan_id = request.GET.get("plan_id") - - if not service_id: - return self._error("service_id is required but missing.") - if not plan_id: - return self._error("plan_id is required but missing.") - - try: - service = Service.objects.get(osb_service_id=service_id) - service_offering = ServiceOffering.objects.get( - osb_plan_id=plan_id, service=service - ) - except Service.DoesNotExist: - return self._error(f"Unknown service_id: {service_id}") - except ServiceOffering.DoesNotExist: - return self._error( - f"Unknown plan_id: {plan_id} for service_id: {service_id}" - ) - - self._create_action_helpdesk_ticket( - request=request, - action="Offboard", - instance_id=instance_id, - service=service, - service_offering=service_offering, - ) - - return JsonResponse({}, status=200) - - def patch(self, request, instance_id): - """ - This implements the Exoscale suspension flow MVP. - https://docs.servala.com/exoscale-osb.html#_suspension - """ - try: - data = json.loads(request.body) - except json.JSONDecodeError: - return JsonResponse({"error": "Invalid JSON in request body"}, status=400) - - service_id = data.get("service_id") - plan_id = data.get("plan_id") - - if not service_id: - return self._error("service_id is required but missing.") - if not plan_id: - return self._error("plan_id is required but missing.") - - try: - service = Service.objects.get(osb_service_id=service_id) - # Special handling: when plan_id is "suspend", don't lookup service_offering - service_offering = None - if plan_id != "suspend": - service_offering = ServiceOffering.objects.get( - osb_plan_id=plan_id, service=service - ) - except Service.DoesNotExist: # pragma: no-cover - return self._error(f"Unknown service_id: {service_id}") - except ServiceOffering.DoesNotExist: # pragma: no-cover - return self._error( - f"Unknown plan_id: {plan_id} for service_id: {service_id}" - ) - - self._create_action_helpdesk_ticket( - request=request, - action="Suspend", - instance_id=instance_id, - service=service, - service_offering=service_offering, - users=data.get("parameters", {}).get("users"), - ) - return JsonResponse({}, status=200) - - def _get_admin_url(self, model_name, pk): - admin_path = reverse(f"admin:{model_name}", args=[pk]) - return self.request.build_absolute_uri(admin_path) - - def _create_action_helpdesk_ticket( - self, request, action, instance_id, service, service_offering=None, users=None - ): - """ - Create an Odoo helpdesk ticket for offboarding or suspension actions. - This is an MVP implementation that creates a ticket for manual handling. - """ - try: - service_instance = None - organization = None - try: - # Look for instances with this name in the service offering's context - filter_kwargs = {"name": instance_id} - if service_offering: - filter_kwargs["context__service_offering"] = service_offering - - service_instance = ( - ServiceInstance.objects.filter(**filter_kwargs) - .select_related("organization") - .first() - ) - - if service_instance: - organization = service_instance.organization - except Exception: # pragma: no cover - pass - - description_parts = [f"Action: {action}", f"Service: {service.name}"] - if organization: - org_url = self._get_admin_url( - "core_organization_change", organization.pk - ) - description_parts.append( - f"Organization: {organization.name} - {org_url}" - ) - - if service_instance: - instance_url = self._get_admin_url( - "core_serviceinstance_change", service_instance.pk - ) - description_parts.append( - f"Instance: {service_instance.name} - {instance_url}" - ) - else: - description_parts.append(f"Instance: {instance_id}") - - if service_offering: - offering_url = self._get_admin_url( - "core_serviceoffering_change", service_offering.pk - ) - description_parts.append(f"Service Offering: {offering_url}") - - if users: - description_parts.append("
Users:") - for user_data in users: - email = user_data.get("email", "N/A") - full_name = user_data.get("full_name", "N/A") - role = user_data.get("role", "N/A") - - user_link = email - if email and email != "N/A": - try: - user = User.objects.get(email=email.strip().lower()) - user_link = self._get_admin_url("core_user_change", user.pk) - except User.DoesNotExist: - pass - - description_parts.append(f" - {full_name} ({user_link}) - {role}") - - description = "
".join(description_parts) - - create_helpdesk_ticket( - title=f"Exoscale OSB {action} - {service.name} - {instance_id}", - description=description, - ) - logger.info( - f"Created {action} helpdesk ticket for instance {instance_id}, service {service.name}" - ) - - except Exception as e: - logger.error( - f"Error creating Exoscale {action} helpdesk ticket for instance {instance_id}: {e}" - ) diff --git a/src/servala/core/admin.py b/src/servala/core/admin.py index 60fe147..073d444 100644 --- a/src/servala/core/admin.py +++ b/src/servala/core/admin.py @@ -1,6 +1,3 @@ -import json -from pathlib import Path - from django.contrib import admin, messages from django.utils.translation import gettext_lazy as _ from django_jsonform.widgets import JSONFormWidget @@ -12,7 +9,6 @@ from servala.core.models import ( ControlPlane, ControlPlaneCRD, Organization, - OrganizationInvitation, OrganizationMembership, OrganizationOrigin, Service, @@ -66,15 +62,10 @@ class OrganizationAdmin(admin.ModelAdmin): search_fields = ("name", "namespace") autocomplete_fields = ("billing_entity", "origin") inlines = (OrganizationMembershipInline,) - filter_horizontal = ("limit_osb_services",) def get_readonly_fields(self, request, obj=None): readonly_fields = list(super().get_readonly_fields(request, obj) or []) readonly_fields.append("namespace") # Always read-only - - if obj and obj.has_inherited_billing_entity: - readonly_fields.append("billing_entity") - return readonly_fields @@ -86,16 +77,8 @@ class BillingEntityAdmin(admin.ModelAdmin): @admin.register(OrganizationOrigin) class OrganizationOriginAdmin(admin.ModelAdmin): - list_display = ( - "name", - "billing_entity", - "default_odoo_sale_order_id", - "hide_billing_address", - ) - list_filter = ("hide_billing_address",) + list_display = ("name",) search_fields = ("name",) - autocomplete_fields = ("billing_entity",) - filter_horizontal = ("limit_cloudproviders",) @admin.register(OrganizationMembership) @@ -107,58 +90,6 @@ class OrganizationMembershipAdmin(admin.ModelAdmin): date_hierarchy = "date_joined" -@admin.register(OrganizationInvitation) -class OrganizationInvitationAdmin(admin.ModelAdmin): - list_display = ("email", "organization", "role", "is_accepted", "created_at") - list_filter = ("role", "created_at", "accepted_at", "organization") - search_fields = ("email", "organization__name") - autocomplete_fields = ("organization", "accepted_by") - readonly_fields = ( - "secret", - "accepted_by", - "accepted_at", - "created_at", - "updated_at", - ) - date_hierarchy = "created_at" - actions = ["send_invitation_emails"] - - def is_accepted(self, obj): - return obj.is_accepted - - is_accepted.boolean = True - is_accepted.short_description = _("Accepted") - - def send_invitation_emails(self, request, queryset): - pending_invitations = queryset.filter(accepted_by__isnull=True) - sent_count = 0 - failed_count = 0 - - for invitation in pending_invitations: - try: - invitation.send_invitation_email(request) - sent_count += 1 - except Exception as e: - failed_count += 1 - messages.error( - request, - _(f"Failed to send invitation to {invitation.email}: {str(e)}"), - ) - - if sent_count > 0: - messages.success( - request, - _(f"Successfully sent {sent_count} invitation email(s)."), - ) - - if failed_count > 0: - messages.warning( - request, _(f"Failed to send {failed_count} invitation email(s).") - ) - - send_invitation_emails.short_description = _("Send invitation emails") - - @admin.register(ServiceCategory) class ServiceCategoryAdmin(admin.ModelAdmin): list_display = ("name", "parent") @@ -177,6 +108,7 @@ class ServiceAdmin(admin.ModelAdmin): def get_form(self, request, obj=None, **kwargs): form = super().get_form(request, obj, **kwargs) + # JSON schema for external_links field external_links_schema = { "type": "array", "title": "External Links", @@ -209,6 +141,7 @@ class CloudProviderAdmin(admin.ModelAdmin): def get_form(self, request, obj=None, **kwargs): form = super().get_form(request, obj, **kwargs) + # JSON schema for external_links field external_links_schema = { "type": "array", "title": "External Links", @@ -241,15 +174,7 @@ class ControlPlaneAdmin(admin.ModelAdmin): fieldsets = ( ( None, - { - "fields": ( - "name", - "description", - "cloud_provider", - "required_label", - "wildcard_dns", - ) - }, + {"fields": ("name", "description", "cloud_provider", "required_label")}, ), ( _("API Credentials"), @@ -319,29 +244,8 @@ class ServiceDefinitionAdmin(admin.ModelAdmin): "description": _("API definition for the Kubernetes Custom Resource"), }, ), - ( - _("Form Configuration"), - { - "fields": ("form_config", "hide_expert_mode"), - "description": _( - "Optional custom form configuration. When provided, this will be used instead of auto-generating the form from the OpenAPI spec." - ), - }, - ), ) - def get_form(self, request, obj=None, **kwargs): - form = super().get_form(request, obj, **kwargs) - schema_path = Path(__file__).parent / "schemas" / "form_config_schema.json" - with open(schema_path) as f: - form_config_schema = json.load(f) - - if "form_config" in form.base_fields: - form.base_fields["form_config"].widget = JSONFormWidget( - schema=form_config_schema - ) - return form - def get_exclude(self, request, obj=None): # Exclude the original api_definition field as we're using our custom fields return ["api_definition"] @@ -400,23 +304,3 @@ class ServiceOfferingAdmin(admin.ModelAdmin): search_fields = ("description",) autocomplete_fields = ("service", "provider") inlines = (ControlPlaneCRDInline,) - - def get_form(self, request, obj=None, **kwargs): - form = super().get_form(request, obj, **kwargs) - external_links_schema = { - "type": "array", - "title": "External Links", - "items": { - "type": "object", - "title": "Link", - "properties": { - "url": {"type": "string", "format": "uri", "title": "URL"}, - "title": {"type": "string", "title": "Title"}, - }, - "required": ["url", "title"], - }, - } - form.base_fields["external_links"].widget = JSONFormWidget( - schema=external_links_schema - ) - return form diff --git a/src/servala/core/crd.py b/src/servala/core/crd.py new file mode 100644 index 0000000..44c809b --- /dev/null +++ b/src/servala/core/crd.py @@ -0,0 +1,419 @@ +import re + +from django import forms +from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator +from django.db import models +from django.forms.models import ModelForm, ModelFormMetaclass +from django.utils.translation import gettext_lazy as _ + +from servala.core.models import ServiceInstance + + +class CRDModel(models.Model): + """Base class for all virtual CRD models""" + + def __init__(self, **kwargs): + if spec := kwargs.pop("spec", None): + kwargs.update(unnest_data({"spec": spec})) + super().__init__(**kwargs) + + class Meta: + abstract = True + + +def duplicate_field(field_name, model): + # Get the field from the model + field = model._meta.get_field(field_name) + + # Create a new field with the same attributes + new_field = type(field).__new__(type(field)) + new_field.__dict__.update(field.__dict__) + + # Ensure the field is not linked to the original model + new_field.model = None + new_field.auto_created = False + + return new_field + + +def generate_django_model(schema, group, version, kind): + """ + Generates a virtual Django model from a Kubernetes CRD's OpenAPI v3 schema. + """ + # We always need these three fields to know our own name and our full namespace + model_fields = {"__module__": "crd_models"} + for field_name in ("name", "organization", "context"): + model_fields[field_name] = duplicate_field(field_name, ServiceInstance) + + # All other fields are generated from the schema, except for the + # resourceRef object + spec = schema["properties"].get("spec") or {} + spec["properties"].pop("resourceRef", None) + model_fields.update(build_object_fields(spec, "spec", parent_required=False)) + + # Store the original schema on the model class + model_fields["SCHEMA"] = schema + + meta_class = type("Meta", (), {"app_label": "crd_models"}) + model_fields["Meta"] = meta_class + + # create the model class + model_name = kind + model_class = type(model_name, (CRDModel,), model_fields) + return model_class + + +def build_object_fields(schema, name, verbose_name_prefix=None, parent_required=False): + required_fields = schema.get("required") or [] + properties = schema.get("properties") or {} + fields = {} + + for field_name, field_schema in properties.items(): + is_required = field_name in required_fields or parent_required + full_name = f"{name}.{field_name}" + result = get_django_field( + field_schema, + is_required, + field_name, + full_name, + verbose_name_prefix=verbose_name_prefix, + ) + if isinstance(result, dict): + fields.update(result) + else: + fields[full_name] = result + return fields + + +def deslugify(title): + if "_" in title: + title.replace("_", " ") + return title.title() + return re.sub(r"(? PostgreSQL Parameters). - """ - ACRONYMS = { - # Database systems - "SQL": "SQL", - "MYSQL": "MySQL", - "POSTGRESQL": "PostgreSQL", - "MARIADB": "MariaDB", - "MSSQL": "MSSQL", - "MONGODB": "MongoDB", - "REDIS": "Redis", - # Protocols - "HTTP": "HTTP", - "HTTPS": "HTTPS", - "FTP": "FTP", - "SFTP": "SFTP", - "SSH": "SSH", - "TLS": "TLS", - "SSL": "SSL", - # APIs - "API": "API", - "REST": "REST", - "GRPC": "gRPC", - "GRAPHQL": "GraphQL", - # Networking - "URL": "URL", - "URI": "URI", - "FQDN": "FQDN", - "DNS": "DNS", - "IP": "IP", - "TCP": "TCP", - "UDP": "UDP", - # Data formats - "JSON": "JSON", - "XML": "XML", - "YAML": "YAML", - "CSV": "CSV", - "HTML": "HTML", - "CSS": "CSS", - # Hardware - "CPU": "CPU", - "RAM": "RAM", - "GPU": "GPU", - "SSD": "SSD", - "HDD": "HDD", - # Identifiers - "ID": "ID", - "UUID": "UUID", - "GUID": "GUID", - "ARN": "ARN", - # Cloud providers - "AWS": "AWS", - "GCP": "GCP", - "AZURE": "Azure", - "IBM": "IBM", - # Kubernetes/Cloud - "DB": "DB", - "PVC": "PVC", - "PV": "PV", - "VPN": "VPN", - # Auth - "OS": "OS", - "LDAP": "LDAP", - "SAML": "SAML", - "OAUTH": "OAuth", - "JWT": "JWT", - # AWS Services - "S3": "S3", - "EC2": "EC2", - "RDS": "RDS", - "EBS": "EBS", - "IAM": "IAM", - } - - if "_" in title: - # Handle snake_case - title = title.replace("_", " ") - words = title.split() - else: - # Handle camelCase/PascalCase with smart splitting - # This regex splits on: - # - Transition from lowercase to uppercase (camelCase) - # - Transition from multiple uppercase to an uppercase followed by lowercase (SQLParameters -> SQL Parameters) - words = re.findall(r"[A-Z]+(?=[A-Z][a-z]|\b)|[A-Z][a-z]+|[a-z]+|[0-9]+", title) - - # Merge adjacent words if they form a known compound acronym (e.g., postgre + SQL = PostgreSQL) - merged_words = [] - i = 0 - while i < len(words): - if i < len(words) - 1: - # Check if current word + next word form a known acronym - combined = (words[i] + words[i + 1]).upper() - if combined in ACRONYMS: - merged_words.append(combined) - i += 2 - continue - merged_words.append(words[i]) - i += 1 - - # Capitalize each word, using proper casing for known acronyms - result = [] - for word in merged_words: - word_upper = word.upper() - if word_upper in ACRONYMS: - result.append(ACRONYMS[word_upper]) - else: - result.append(word.capitalize()) - - return " ".join(result) diff --git a/src/servala/core/forms.py b/src/servala/core/forms.py index 090abba..baa85fb 100644 --- a/src/servala/core/forms.py +++ b/src/servala/core/forms.py @@ -1,34 +1,18 @@ -import json -from pathlib import Path - -import jsonschema from django import forms from django.utils.translation import gettext_lazy as _ from django_jsonform.widgets import JSONFormWidget -from servala.core.crd.forms import DEFAULT_FIELD_CONFIGS, MANDATORY_FIELDS from servala.core.models import ControlPlane, ServiceDefinition CONTROL_PLANE_USER_INFO_SCHEMA = { - "type": "array", - "items": { - "type": "object", - "properties": { - "title": { - "type": "string", - "title": "Title", - }, - "content": { - "type": "string", - "title": "Content", - }, - "help_text": { - "type": "string", - "title": "Help Text (optional)", - }, + "type": "object", + "properties": { + "CNAME Record": { + "title": "CNAME Record", + "type": "string", }, - "required": ["title", "content"], }, + "additionalProperties": {"type": "string"}, } @@ -101,12 +85,6 @@ class ControlPlaneAdminForm(forms.ModelForm): return super().save(*args, **kwargs) -def fields_empty(fields): - if not fields: - return True - return all(not field.get("controlplane_field_mapping") for field in fields) - - class ServiceDefinitionAdminForm(forms.ModelForm): api_group = forms.CharField( required=False, @@ -135,10 +113,6 @@ class ServiceDefinitionAdminForm(forms.ModelForm): self.fields["api_version"].initial = api_def.get("version", "") self.fields["api_kind"].initial = api_def.get("kind", "") - schema_path = Path(__file__).parent / "schemas" / "form_config_schema.json" - with open(schema_path) as f: - self.form_config_schema = json.load(f) - def clean(self): cleaned_data = super().clean() @@ -166,250 +140,8 @@ class ServiceDefinitionAdminForm(forms.ModelForm): api_def["kind"] = api_kind cleaned_data["api_definition"] = api_def - form_config = cleaned_data.get("form_config") - - # Convert empty form_config to None (no custom form) - if form_config: - if not form_config.get("fieldsets") or all( - fields_empty(fieldset.get("fields")) - for fieldset in form_config.get("fieldsets") - ): - form_config = None - cleaned_data["form_config"] = None - - if form_config: - form_config = self._normalize_form_config_types(form_config) - cleaned_data["form_config"] = form_config - - try: - jsonschema.validate( - instance=form_config, schema=self.form_config_schema - ) - except jsonschema.ValidationError as e: - raise forms.ValidationError( - { - "form_config": _("Invalid form configuration: {}").format( - e.message - ) - } - ) - except jsonschema.SchemaError as e: - raise forms.ValidationError( - {"form_config": _("Schema error: {}").format(e.message)} - ) - - self._validate_field_mappings(form_config, cleaned_data) - return cleaned_data - def _normalize_form_config_types(self, form_config): - """ - Normalize form_config by converting string representations of numbers - to actual integers/floats. The JSON form widget sends all values - as strings, but the schema expects proper types. - """ - if not isinstance(form_config, dict): - return form_config - - integer_fields = ["max_length", "rows", "min_values", "max_values"] - number_fields = ["min_value", "max_value"] - - for fieldset in form_config.get("fieldsets", []): - for field in fieldset.get("fields", []): - for field_name in integer_fields: - if field_name in field and field[field_name] is not None: - value = field[field_name] - if isinstance(value, str): - try: - field[field_name] = int(value) if value else None - except (ValueError, TypeError): - pass - - for field_name in number_fields: - if field_name in field and field[field_name] is not None: - value = field[field_name] - if isinstance(value, str): - try: - field[field_name] = ( - int(value) if "." not in value else float(value) - ) - except (ValueError, TypeError): - pass - - return form_config - - def _validate_field_mappings(self, form_config, cleaned_data): - if not self.instance.pk: - return - crd = self.instance.offering_control_planes.all().first() - if not crd: - return - - schema = None - try: - schema = crd.resource_schema - except Exception: - pass - - if not schema or not (spec_schema := schema.get("properties", {}).get("spec")): - return - - valid_paths = self._extract_field_paths(spec_schema, "spec") | {"name"} - included_mappings = set() - errors = [] - for fieldset in form_config.get("fieldsets", []): - for field in fieldset.get("fields", []): - mapping = field.get("controlplane_field_mapping") - included_mappings.add(mapping) - - # Validate that fields without defaults have required properties - if mapping not in DEFAULT_FIELD_CONFIGS: - if not field.get("label"): - errors.append( - _( - "Field with mapping '{}' must have a 'label' property " - "(or use a mapping with default config)" - ).format(mapping) - ) - if not field.get("type"): - errors.append( - _( - "Field with mapping '{}' must have a 'type' property " - "(or use a mapping with default config)" - ).format(mapping) - ) - - if mapping and mapping not in valid_paths: - field_name = field.get("label", field.get("name", mapping)) - errors.append( - _( - "Field '{}' has invalid mapping '{}'. Valid paths are: {}" - ).format( - field_name, - mapping, - ", ".join(sorted(valid_paths)[:10]) - + ("..." if len(valid_paths) > 10 else ""), - ) - ) - - if field.get("type") == "choice" and field.get("choices"): - self._validate_choice_field( - field, mapping, spec_schema, "spec", errors - ) - - for mandatory_field in MANDATORY_FIELDS: - if mandatory_field not in included_mappings: - errors.append( - _( - "Required field '{}' must be included in the form configuration" - ).format(mandatory_field) - ) - - if errors: - raise forms.ValidationError({"form_config": errors}) - - def _validate_choice_field(self, field, mapping, spec_schema, prefix, errors): - if not mapping: - return - - field_name = field.get("label", mapping) - custom_choices = field.get("choices", []) - - # Single-element choices [value] are transformed to [value, value] - for i, choice in enumerate(custom_choices): - if not isinstance(choice, (list, tuple)): - errors.append( - _( - "Field '{}': Choice at index {} must be a list or tuple, " - "but got: {}" - ).format(field_name, i, repr(choice)) - ) - return - - choice_len = len(choice) - if choice_len == 1: - custom_choices[i] = [choice[0], choice[0]] - elif choice_len == 0 or choice_len > 2: - errors.append( - _( - "Field '{}': Choice at index {} must have 1 or 2 elements " - "(got {}): {}" - ).format(field_name, i, choice_len, repr(choice)) - ) - return - - field_schema = self._get_field_schema(spec_schema, mapping, prefix) - if not field_schema: - return - - control_plane_choices = field_schema.get("enum", []) - if not control_plane_choices: - return - - custom_choice_values = [choice[0] for choice in custom_choices] - - invalid_choices = [ - value - for value in custom_choice_values - if value not in control_plane_choices - ] - - if invalid_choices: - errors.append( - _( - "Field '{}' has invalid choice values: {}. " - "Valid choices from control plane are: {}" - ).format( - field_name, - ", ".join(f"'{c}'" for c in invalid_choices), - ", ".join(f"'{c}'" for c in control_plane_choices), - ) - ) - - def _get_field_schema(self, schema, field_path, prefix): - if not field_path or not schema: - return None - - if field_path.startswith(prefix + "."): - field_path = field_path[len(prefix) + 1 :] - - parts = field_path.split(".") - current_schema = schema - - for part in parts: - if not isinstance(current_schema, dict): - return None - - properties = current_schema.get("properties", {}) - if part not in properties: - return None - - current_schema = properties[part] - - return current_schema - - def _extract_field_paths(self, schema, prefix=""): - paths = set() - - if not isinstance(schema, dict): - return paths - - if "type" in schema and schema["type"] != "object": - if prefix: - paths.add(prefix) - - if schema.get("properties"): - for prop_name, prop_schema in schema["properties"].items(): - new_prefix = f"{prefix}.{prop_name}" if prefix else prop_name - paths.add(new_prefix) - paths.update(self._extract_field_paths(prop_schema, new_prefix)) - - if schema.get("type") == "array" and "items" in schema: - if prefix: - paths.add(prefix) - - return paths - def save(self, *args, **kwargs): self.instance.api_definition = self.cleaned_data["api_definition"] return super().save(*args, **kwargs) diff --git a/src/servala/core/migrations/0009_controlplane_wildcard_dns_and_more.py b/src/servala/core/migrations/0009_controlplane_wildcard_dns_and_more.py deleted file mode 100644 index 811c843..0000000 --- a/src/servala/core/migrations/0009_controlplane_wildcard_dns_and_more.py +++ /dev/null @@ -1,185 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-22 09:38 - -import django.db.models.deletion -import rules.contrib.models -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("core", "0008_organization_osb_guid_service_osb_service_id_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="controlplane", - name="wildcard_dns", - field=models.CharField( - blank=True, - help_text="Wildcard DNS domain for auto-generating FQDNs (e.g., apps.exoscale-ch-gva-2-prod2.services.servala.com)", - max_length=255, - null=True, - verbose_name="Wildcard DNS", - ), - ), - migrations.AddField( - model_name="organization", - name="limit_osb_services", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="core.service", - verbose_name="Services activated from OSB", - ), - ), - migrations.AddField( - model_name="organizationorigin", - name="billing_entity", - field=models.ForeignKey( - help_text="If set, this billing entity will be used on new organizations with this origin.", - null=True, - on_delete=django.db.models.deletion.PROTECT, - related_name="origins", - to="core.billingentity", - verbose_name="Billing entity", - ), - ), - migrations.AddField( - model_name="organizationorigin", - name="default_odoo_sale_order_id", - field=models.IntegerField( - blank=True, - help_text="If set, this sale order will be used for new organizations with this origin.", - null=True, - verbose_name="Default Odoo Sale Order ID", - ), - ), - migrations.AddField( - model_name="organizationorigin", - name="limit_cloudproviders", - field=models.ManyToManyField( - blank=True, - help_text="If set, all organizations with this origin will be limited to these cloud providers.", - related_name="+", - to="core.cloudprovider", - verbose_name="Limit to these Cloud providers", - ), - ), - migrations.AddField( - model_name="servicedefinition", - name="advanced_fields", - field=models.JSONField( - blank=True, - default=list, - help_text=( - "Array of field names that should be hidden behind an 'Advanced' toggle." - "Use dot notation (e.g., ['spec.parameters.monitoring.enabled', 'spec.parameters.backup.schedule'])" - ), - null=True, - verbose_name="Advanced fields", - ), - ), - migrations.AddField( - model_name="serviceoffering", - name="external_links", - field=models.JSONField( - blank=True, - help_text='JSON array of link objects: {"url": "…", "title": "…"}. ', - null=True, - verbose_name="External links", - ), - ), - migrations.CreateModel( - name="OrganizationInvitation", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "created_at", - models.DateTimeField(auto_now_add=True, verbose_name="Created"), - ), - ( - "updated_at", - models.DateTimeField(auto_now=True, verbose_name="Last updated"), - ), - ( - "email", - models.EmailField(max_length=254, verbose_name="Email address"), - ), - ( - "role", - models.CharField( - choices=[ - ("member", "Member"), - ("admin", "Administrator"), - ("owner", "Owner"), - ], - default="member", - max_length=20, - verbose_name="Role", - ), - ), - ( - "secret", - models.CharField( - editable=False, - max_length=64, - unique=True, - verbose_name="Secret token", - ), - ), - ( - "accepted_at", - models.DateTimeField( - blank=True, null=True, verbose_name="Accepted at" - ), - ), - ( - "accepted_by", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="accepted_invitations", - to=settings.AUTH_USER_MODEL, - verbose_name="Accepted by", - ), - ), - ( - "created_by", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="created_invitations", - to=settings.AUTH_USER_MODEL, - verbose_name="Created by", - ), - ), - ( - "organization", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="invitations", - to="core.organization", - verbose_name="Organization", - ), - ), - ], - options={ - "verbose_name": "Organization invitation", - "verbose_name_plural": "Organization invitations", - "unique_together": {("organization", "email")}, - }, - bases=(rules.contrib.models.RulesModelMixin, models.Model), - ), - ] diff --git a/src/servala/core/migrations/0010_remove_invitation_unique_constraint.py b/src/servala/core/migrations/0010_remove_invitation_unique_constraint.py deleted file mode 100644 index 78c2c45..0000000 --- a/src/servala/core/migrations/0010_remove_invitation_unique_constraint.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-22 13:19 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("core", "0009_controlplane_wildcard_dns_and_more"), - ] - - operations = [ - migrations.AlterUniqueTogether( - name="organizationinvitation", - unique_together=set(), - ), - migrations.AlterField( - model_name="servicedefinition", - name="advanced_fields", - field=models.JSONField( - blank=True, - default=list, - help_text=( - "Array of field names that should be hidden behind an 'Advanced' toggle. " - "Use dot notation (e.g., ['spec.parameters.monitoring.enabled', 'spec.parameters.backup.schedule'])" - ), - null=True, - verbose_name="Advanced fields", - ), - ), - ] diff --git a/src/servala/core/migrations/0011_alter_organizationorigin_billing_entity.py b/src/servala/core/migrations/0011_alter_organizationorigin_billing_entity.py deleted file mode 100644 index b122d68..0000000 --- a/src/servala/core/migrations/0011_alter_organizationorigin_billing_entity.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-22 13:40 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("core", "0010_remove_invitation_unique_constraint"), - ] - - operations = [ - migrations.AlterField( - model_name="organizationorigin", - name="billing_entity", - field=models.ForeignKey( - blank=True, - help_text="If set, this billing entity will be used on new organizations with this origin.", - null=True, - on_delete=django.db.models.deletion.PROTECT, - related_name="origins", - to="core.billingentity", - verbose_name="Billing entity", - ), - ), - ] diff --git a/src/servala/core/migrations/0012_convert_user_info_to_array.py b/src/servala/core/migrations/0012_convert_user_info_to_array.py deleted file mode 100644 index 892949e..0000000 --- a/src/servala/core/migrations/0012_convert_user_info_to_array.py +++ /dev/null @@ -1,65 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-24 10:04 - -from django.db import migrations - - -def convert_user_info_to_array(apps, schema_editor): - """ - Convert user_info from object format {"key": "value"} to array format - [{"title": "key", "content": "value"}]. - """ - ControlPlane = apps.get_model("core", "ControlPlane") - - for control_plane in ControlPlane.objects.all(): - if not control_plane.user_info: - continue - - # If it's already an array (migration already run or new format), skip - if isinstance(control_plane.user_info, list): - continue - - # Convert from dict to array - if isinstance(control_plane.user_info, dict): - new_user_info = [] - for key, value in control_plane.user_info.items(): - new_user_info.append({"title": key, "content": value}) - - control_plane.user_info = new_user_info - control_plane.save(update_fields=["user_info"]) - - -def reverse_user_info_to_object(apps, schema_editor): - """ - Reverse the migration by converting array format back to object format. - Note: help_text will be lost during reversal. - """ - ControlPlane = apps.get_model("core", "ControlPlane") - - for control_plane in ControlPlane.objects.all(): - if not control_plane.user_info: - continue - - # If it's already an object, skip - if isinstance(control_plane.user_info, dict): - continue - - # Convert from array to dict - if isinstance(control_plane.user_info, list): - new_user_info = {} - for item in control_plane.user_info: - if isinstance(item, dict) and "title" in item and "content" in item: - new_user_info[item["title"]] = item["content"] - - control_plane.user_info = new_user_info - control_plane.save(update_fields=["user_info"]) - - -class Migration(migrations.Migration): - - dependencies = [ - ("core", "0011_alter_organizationorigin_billing_entity"), - ] - - operations = [ - migrations.RunPython(convert_user_info_to_array, reverse_user_info_to_object), - ] diff --git a/src/servala/core/migrations/0012_remove_advanced_fields.py b/src/servala/core/migrations/0012_remove_advanced_fields.py deleted file mode 100644 index 7d0fecd..0000000 --- a/src/servala/core/migrations/0012_remove_advanced_fields.py +++ /dev/null @@ -1,32 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-31 10:40 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("core", "0012_convert_user_info_to_array"), - ] - - operations = [ - migrations.RemoveField( - model_name="servicedefinition", - name="advanced_fields", - ), - migrations.AlterField( - model_name="organization", - name="name", - field=models.CharField( - max_length=32, - validators=[ - django.core.validators.RegexValidator( - message="Organization name can only contain letters, numbers, and spaces.", - regex="^[A-Za-z0-9\\s]+$", - ) - ], - verbose_name="Name", - ), - ), - ] diff --git a/src/servala/core/migrations/0013_add_form_config.py b/src/servala/core/migrations/0013_add_form_config.py deleted file mode 100644 index 2819a6c..0000000 --- a/src/servala/core/migrations/0013_add_form_config.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-31 10:47 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("core", "0012_remove_advanced_fields"), - ] - - operations = [ - migrations.AddField( - model_name="servicedefinition", - name="form_config", - field=models.JSONField( - blank=True, - help_text=( - "Optional custom form configuration. When provided, this configuration will " - "be used to render the service form instead of auto-generating it from the OpenAPI spec. " - 'Format: {"fieldsets": [{"title": "Section", "fields": [{...}]}]}' - ), - null=True, - verbose_name="Form Configuration", - ), - ), - ] diff --git a/src/servala/core/migrations/0014_hide_billing_address.py b/src/servala/core/migrations/0014_hide_billing_address.py deleted file mode 100644 index 1e7f3bf..0000000 --- a/src/servala/core/migrations/0014_hide_billing_address.py +++ /dev/null @@ -1,44 +0,0 @@ -# Generated by Django 5.2.8 on 2025-11-12 09:31 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("core", "0013_add_form_config"), - ] - - operations = [ - migrations.AddField( - model_name="organizationorigin", - name="billing_message", - field=models.TextField( - blank=True, - help_text="Optional message to display instead of billing address (e.g., 'You will be invoiced by Exoscale').", - verbose_name="Billing Message", - ), - ), - migrations.AddField( - model_name="organizationorigin", - name="hide_billing_address", - field=models.BooleanField( - default=False, - help_text="If enabled, the billing address will not be shown in the organization details view.", - verbose_name="Hide Billing Address", - ), - ), - migrations.AlterField( - model_name="controlplane", - name="user_info", - field=models.JSONField( - blank=True, - help_text=( - 'Array of info objects: [{"title": "…", "content": "…", "help_text": "…"}]. ' - "The help_text field is optional and will be shown as a hover popover on an info icon." - ), - null=True, - verbose_name="User Information", - ), - ), - ] diff --git a/src/servala/core/migrations/0015_add_hide_expert_mode_to_service_definition.py b/src/servala/core/migrations/0015_add_hide_expert_mode_to_service_definition.py deleted file mode 100644 index cc88c9d..0000000 --- a/src/servala/core/migrations/0015_add_hide_expert_mode_to_service_definition.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 5.2.8 on 2025-11-14 15:55 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("core", "0014_hide_billing_address"), - ] - - operations = [ - migrations.AddField( - model_name="servicedefinition", - name="hide_expert_mode", - field=models.BooleanField( - default=False, - help_text=( - "When enabled, the 'Show Expert Mode' toggle will be hidden and only the custom form configuration will be available. " - "Only applies when a custom form configuration is provided." - ), - verbose_name="Disable Expert Mode", - ), - ), - ] diff --git a/src/servala/core/models/__init__.py b/src/servala/core/models/__init__.py index 4c23f18..22e8e8a 100644 --- a/src/servala/core/models/__init__.py +++ b/src/servala/core/models/__init__.py @@ -1,7 +1,6 @@ from .organization import ( BillingEntity, Organization, - OrganizationInvitation, OrganizationMembership, OrganizationOrigin, OrganizationRole, @@ -24,7 +23,6 @@ __all__ = [ "ControlPlane", "ControlPlaneCRD", "Organization", - "OrganizationInvitation", "OrganizationMembership", "OrganizationOrigin", "OrganizationRole", diff --git a/src/servala/core/models/organization.py b/src/servala/core/models/organization.py index be4f587..083bc50 100644 --- a/src/servala/core/models/organization.py +++ b/src/servala/core/models/organization.py @@ -1,14 +1,7 @@ -import secrets - import rules import urlman -from auditlog.registry import auditlog from django.conf import settings -from django.contrib.sites.shortcuts import get_current_site -from django.core.mail import send_mail -from django.core.validators import RegexValidator from django.db import models, transaction -from django.http import HttpRequest from django.utils.functional import cached_property from django.utils.safestring import mark_safe from django.utils.text import slugify @@ -21,18 +14,7 @@ from servala.core.odoo import CLIENT class Organization(ServalaModelMixin, models.Model): - name = models.CharField( - max_length=32, - verbose_name=_("Name"), - validators=[ - RegexValidator( - regex=r"^[A-Za-z0-9\s]+$", - message=_( - "Organization name can only contain letters, numbers, and spaces." - ), - ) - ], - ) + name = models.CharField(max_length=100, verbose_name=_("Name")) # The namespace is generated as "org-{id}" in accordance with RFC 1035 Label Names. # It is nullable as we need to write to the database in order to read the ID, but should # not be null in practical use. @@ -64,12 +46,6 @@ class Organization(ServalaModelMixin, models.Model): related_name="organizations", verbose_name=_("Members"), ) - limit_osb_services = models.ManyToManyField( - to="Service", - related_name="+", - verbose_name=_("Services activated from OSB"), - blank=True, - ) odoo_sale_order_id = models.IntegerField( null=True, blank=True, verbose_name=_("Odoo Sale Order ID") @@ -101,18 +77,6 @@ class Organization(ServalaModelMixin, models.Model): def get_absolute_url(self): return self.urls.base - @property - def has_inherited_billing_entity(self): - return self.origin and self.billing_entity == self.origin.billing_entity - - @property - def limit_cloudproviders(self): - if self.origin: - return self.origin.limit_cloudproviders.all() - from servala.core.models import CloudProvider - - return CloudProvider.objects.none() - def set_owner(self, user): with scopes_disabled(): OrganizationMembership.objects.filter(user=user, organization=self).delete() @@ -130,7 +94,7 @@ class Organization(ServalaModelMixin, models.Model): @classmethod @transaction.atomic - def create_organization(cls, instance, owner=None): + def create_organization(cls, instance, owner): try: instance.origin except Exception: @@ -138,23 +102,9 @@ class Organization(ServalaModelMixin, models.Model): pk=settings.SERVALA_DEFAULT_ORIGIN ) instance.save() - if owner: - instance.set_owner(owner) + instance.set_owner(owner) - if instance.origin and instance.origin.default_odoo_sale_order_id: - sale_order_id = instance.origin.default_odoo_sale_order_id - sale_order_data = CLIENT.search_read( - model="sale.order", - domain=[["id", "=", sale_order_id]], - fields=["name"], - limit=1, - ) - - instance.odoo_sale_order_id = sale_order_id - if sale_order_data: - instance.odoo_sale_order_name = sale_order_data[0]["name"] - instance.save(update_fields=["odoo_sale_order_id", "odoo_sale_order_name"]) - elif ( + if ( instance.billing_entity.odoo_company_id and instance.billing_entity.odoo_invoice_id ): @@ -181,34 +131,6 @@ class Organization(ServalaModelMixin, models.Model): return instance - def get_visible_services(self): - from servala.core.models import Service - - queryset = Service.objects.all() - if self.limit_osb_services.exists(): - queryset = self.limit_osb_services.all() - if self.limit_cloudproviders.exists(): - queryset = queryset.filter( - offerings__provider__in=self.limit_cloudproviders - ).distinct() - return queryset.prefetch_related( - "offerings", "offerings__provider" - ).select_related("category") - - def get_deactivated_services(self): - from servala.core.models import Service - - if not self.limit_osb_services.exists(): - return Service.objects.none() - - queryset = Service.objects.select_related("category") - if self.limit_cloudproviders.exists(): - queryset = queryset.filter( - offerings__provider__in=self.limit_cloudproviders - ).distinct() - queryset = queryset.exclude(id__in=self.limit_osb_services.all()) - return queryset.prefetch_related("offerings", "offerings__provider") - class Meta: verbose_name = _("Organization") verbose_name_plural = _("Organizations") @@ -391,48 +313,6 @@ class OrganizationOrigin(ServalaModelMixin, models.Model): name = models.CharField(max_length=100, verbose_name=_("Name")) description = models.TextField(blank=True, verbose_name=_("Description")) - billing_entity = models.ForeignKey( - to="BillingEntity", - on_delete=models.PROTECT, - related_name="origins", - verbose_name=_("Billing entity"), - help_text=_( - "If set, this billing entity will be used on new organizations with this origin." - ), - null=True, - blank=True, - ) - limit_cloudproviders = models.ManyToManyField( - to="CloudProvider", - related_name="+", - verbose_name=_("Limit to these Cloud providers"), - blank=True, - help_text=_( - "If set, all organizations with this origin will be limited to these cloud providers." - ), - ) - default_odoo_sale_order_id = models.IntegerField( - null=True, - blank=True, - verbose_name=_("Default Odoo Sale Order ID"), - help_text=_( - "If set, this sale order will be used for new organizations with this origin." - ), - ) - hide_billing_address = models.BooleanField( - default=False, - verbose_name=_("Hide Billing Address"), - help_text=_( - "If enabled, the billing address will not be shown in the organization details view." - ), - ) - billing_message = models.TextField( - blank=True, - verbose_name=_("Billing Message"), - help_text=_( - "Optional message to display instead of billing address (e.g., 'You will be invoiced by Exoscale')." - ), - ) class Meta: verbose_name = _("Organization origin") @@ -481,120 +361,3 @@ class OrganizationMembership(ServalaModelMixin, models.Model): def __str__(self): return f"{self.user} in {self.organization} as {self.role}" - - -class OrganizationInvitation(ServalaModelMixin, models.Model): - organization = models.ForeignKey( - to=Organization, - on_delete=models.CASCADE, - related_name="invitations", - verbose_name=_("Organization"), - ) - email = models.EmailField(verbose_name=_("Email address")) - role = models.CharField( - max_length=20, - choices=OrganizationRole.choices, - default=OrganizationRole.MEMBER, - verbose_name=_("Role"), - ) - secret = models.CharField( - max_length=64, - unique=True, - editable=False, - verbose_name=_("Secret token"), - ) - created_by = models.ForeignKey( - to="core.User", - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name="created_invitations", - verbose_name=_("Created by"), - ) - accepted_by = models.ForeignKey( - to="core.User", - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name="accepted_invitations", - verbose_name=_("Accepted by"), - ) - accepted_at = models.DateTimeField( - null=True, blank=True, verbose_name=_("Accepted at") - ) - - class urls(urlman.Urls): - accept = "/invitations/{self.secret}/accept/" - delete = "{self.organization.urls.details}invitations/{self.pk}/delete/" - - class Meta: - verbose_name = _("Organization invitation") - verbose_name_plural = _("Organization invitations") - - def __str__(self): - return f"Invitation for {self.email} to {self.organization}" - - def save(self, *args, **kwargs): - if not self.secret: - self.secret = secrets.token_urlsafe(48) - super().save(*args, **kwargs) - - @property - def is_accepted(self): - # We check both accepted_by and accepted_at to avoid a deleted user - # freeing up an invitation - return bool(self.accepted_by or self.accepted_at) - - @property - def can_be_accepted(self): - return not self.is_accepted - - def send_invitation_email(self, request=None): - subject = _("You're invited to join {organization} on Servala").format( - organization=self.organization.name - ) - - if request: - invitation_url = request.build_absolute_uri(self.urls.accept) - organization_url = request.build_absolute_uri(self.organization.urls.base) - else: - fake_request = HttpRequest() - fake_request.META["SERVER_NAME"] = get_current_site(None).domain - fake_request.META["SERVER_PORT"] = "443" - fake_request.META["wsgi.url_scheme"] = "https" - invitation_url = fake_request.build_absolute_uri(self.urls.accept) - organization_url = fake_request.build_absolute_uri( - self.organization.urls.base - ) - - message = _( - """Hello, - -You have been invited to join the organization "{organization}" on Servala Portal as a {role}. - -To accept this invitation, please click the link below: -{invitation_url} - -Once you accept, you'll be able to access the organization at: -{organization_url} - -Best regards, -The Servala Team""" - ).format( - organization=self.organization.name, - role=self.get_role_display(), - invitation_url=invitation_url, - organization_url=organization_url, - ) - - send_mail( - subject=subject, - message=message, - from_email=settings.EMAIL_DEFAULT_FROM, - recipient_list=[self.email], - fail_silently=False, - ) - - -auditlog.register(OrganizationInvitation, serialize_data=True) -auditlog.register(OrganizationMembership, serialize_data=True) diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index ab7b76f..43c9023 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -156,17 +156,7 @@ class ControlPlane(ServalaModelMixin, models.Model): blank=True, verbose_name=_("User Information"), help_text=_( - 'Array of info objects: [{"title": "…", "content": "…", "help_text": "…"}]. ' - "The help_text field is optional and will be shown as a hover popover on an info icon." - ), - ) - wildcard_dns = models.CharField( - max_length=255, - blank=True, - null=True, - verbose_name=_("Wildcard DNS"), - help_text=_( - "Wildcard DNS domain for auto-generating FQDNs (e.g., apps.exoscale-ch-gva-2-prod2.services.servala.com)" + "Key-value information displayed to users when selecting this control plane" ), ) @@ -360,24 +350,6 @@ class ServiceDefinition(ServalaModelMixin, models.Model): null=True, blank=True, ) - form_config = models.JSONField( - verbose_name=_("Form Configuration"), - help_text=_( - "Optional custom form configuration. When provided, this configuration will be used " - "to render the service form instead of auto-generating it from the OpenAPI spec. " - 'Format: {"fieldsets": [{"title": "Section", "fields": [{...}]}]}' - ), - null=True, - blank=True, - ) - hide_expert_mode = models.BooleanField( - default=False, - verbose_name=_("Disable Expert Mode"), - help_text=_( - "When enabled, the 'Show Expert Mode' toggle will be hidden and only the custom form " - "configuration will be available. Only applies when a custom form configuration is provided." - ), - ) service = models.ForeignKey( to="Service", on_delete=models.CASCADE, @@ -520,22 +492,6 @@ class ControlPlaneCRD(ServalaModelMixin, models.Model): return return generate_model_form_class(self.django_model) - @cached_property - def custom_model_form_class(self): - from servala.core.crd import generate_custom_form_class - - if not self.django_model: - return - if not ( - self.service_definition - and self.service_definition.form_config - and self.service_definition.form_config.get("fieldsets") - ): - return - return generate_custom_form_class( - self.service_definition.form_config, self.django_model - ) - class ServiceOffering(ServalaModelMixin, models.Model): """ @@ -555,12 +511,6 @@ class ServiceOffering(ServalaModelMixin, models.Model): verbose_name=_("Provider"), ) description = models.TextField(blank=True, verbose_name=_("Description")) - external_links = models.JSONField( - null=True, - blank=True, - verbose_name=_("External links"), - help_text=('JSON array of link objects: {"url": "…", "title": "…"}. '), - ) osb_plan_id = models.CharField( max_length=100, null=True, @@ -628,7 +578,7 @@ class ServiceInstance(ServalaModelMixin, models.Model): } class urls(urlman.Urls): - base = "{self.organization.urls.instances}{self.name}-{self.pk}/" + base = "{self.organization.urls.instances}{self.name}/" update = "{base}update/" delete = "{base}delete/" @@ -707,7 +657,6 @@ class ServiceInstance(ServalaModelMixin, models.Model): return mark_safe(f"") @classmethod - @transaction.atomic def create_instance(cls, name, organization, context, created_by, spec_data): # Ensure the namespace exists context.control_plane.get_or_create_namespace(organization) @@ -755,7 +704,7 @@ class ServiceInstance(ServalaModelMixin, models.Model): body=create_data, ) except Exception as e: - # Transaction will automatically roll back the instance creation + instance.delete() if isinstance(e, ApiException): try: error_body = json.loads(e.body) @@ -899,6 +848,7 @@ class ServiceInstance(ServalaModelMixin, models.Model): return return self.context.django_model( name=self.name, + organization=self.organization, context=self.context, spec=self.spec, # We pass -1 as ID in order to make it clear that a) this object exists (remotely), @@ -955,9 +905,6 @@ class ServiceInstance(ServalaModelMixin, models.Model): import base64 for key, value in secret.data.items(): - # Skip keys ending with _HOST as they're only useful for dedicated OpenShift clusters - if key.endswith("_HOST"): - continue try: credentials[key] = base64.b64decode(value).decode("utf-8") except Exception: @@ -969,21 +916,5 @@ class ServiceInstance(ServalaModelMixin, models.Model): except Exception as e: return {"error": str(e)} - @property - def fqdn_url(self): - try: - fqdn = self.spec.get("parameters", {}).get("service", {}).get("fqdn") - if not fqdn: - return None - - if isinstance(fqdn, list): - return fqdn[0] - elif isinstance(fqdn, str): - return fqdn - else: - return None - except (AttributeError, KeyError, IndexError): - return None - auditlog.register(ServiceInstance, exclude_fields=["updated_at"], serialize_data=True) diff --git a/src/servala/core/odoo.py b/src/servala/core/odoo.py index 517829a..ba91dc7 100644 --- a/src/servala/core/odoo.py +++ b/src/servala/core/odoo.py @@ -207,19 +207,3 @@ def get_invoice_addresses(user): return invoice_addresses or [] except Exception: return [] - - -def create_helpdesk_ticket(title, description, partner_id=None, sale_order_id=None): - ticket_data = { - "name": title, - "team_id": settings.ODOO["HELPDESK_TEAM_ID"], - "description": description, - } - - if partner_id: - ticket_data["partner_id"] = partner_id - - if sale_order_id: - ticket_data["sale_order_id"] = sale_order_id - - return CLIENT.execute("helpdesk.ticket", "create", [ticket_data]) diff --git a/src/servala/core/rules.py b/src/servala/core/rules.py index e1a0992..cf4dc1c 100644 --- a/src/servala/core/rules.py +++ b/src/servala/core/rules.py @@ -14,26 +14,20 @@ def has_organization_role(user, org, roles): @rules.predicate def is_organization_owner(user, obj): - from servala.core.models.organization import OrganizationRole - if hasattr(obj, "organization"): org = obj.organization else: org = obj - return has_organization_role(user, org, [OrganizationRole.OWNER]) + return has_organization_role(user, org, ["owner"]) @rules.predicate def is_organization_admin(user, obj): - from servala.core.models.organization import OrganizationRole - if hasattr(obj, "organization"): org = obj.organization else: org = obj - return has_organization_role( - user, org, [OrganizationRole.OWNER, OrganizationRole.ADMIN] - ) + return has_organization_role(user, org, ["owner", "admin"]) @rules.predicate diff --git a/src/servala/core/schemas/form_config_schema.json b/src/servala/core/schemas/form_config_schema.json deleted file mode 100644 index 3b01b3f..0000000 --- a/src/servala/core/schemas/form_config_schema.json +++ /dev/null @@ -1,107 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Service Definition Form Configuration Schema", - "description": "Schema for custom form configuration in ServiceDefinition", - "type": "object", - "required": ["fieldsets"], - "properties": { - "fieldsets": { - "type": "array", - "description": "Array of fieldset objects defining form sections", - "minItems": 1, - "items": { - "type": "object", - "required": ["fields"], - "properties": { - "title": { - "type": "string", - "description": "Optional title for the fieldset/tab" - }, - "fields": { - "type": "array", - "description": "Array of field definitions in this fieldset", - "minItems": 1, - "items": { - "type": "object", - "required": ["controlplane_field_mapping"], - "properties": { - "type": { - "type": "string", - "description": "Field type", - "enum": ["", "text", "email", "textarea", "number", "choice", "checkbox", "array"] - }, - "label": { - "type": "string", - "description": "Human-readable field label" - }, - "help_text": { - "type": "string", - "description": "Optional help text displayed below the field" - }, - "required": { - "type": "boolean", - "description": "Whether the field is required", - "default": false - }, - "controlplane_field_mapping": { - "type": "string", - "description": "Dot-notation path mapping to Kubernetes spec field (e.g., 'spec.parameters.service.fqdn')" - }, - "max_length": { - "type": ["integer", "null"], - "description": "Maximum length for text/textarea fields", - "minimum": 1 - }, - "rows": { - "type": ["integer", "null"], - "description": "Number of rows for textarea fields", - "minimum": 1 - }, - "min_value": { - "type": ["number", "null"], - "description": "Minimum value for number fields" - }, - "max_value": { - "type": ["number", "null"], - "description": "Maximum value for number fields" - }, - "choices": { - "type": "array", - "description": "Array of [value, label] pairs for choice fields", - "items": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "min_values": { - "type": ["integer", "null"], - "description": "Minimum number of values for array fields", - "minimum": 0 - }, - "max_values": { - "type": ["integer", "null"], - "description": "Maximum number of values for array fields", - "minimum": 1 - }, - "validators": { - "type": "array", - "description": "Array of validator names (for future use)", - "items": { - "type": "string", - "enum": ["email", "fqdn", "url", "ipv4", "ipv6"] - } - }, - "default_value": { - "type": "string", - "description": "Default value for the field when creating new instances" - } - } - } - } - } - } - } - } -} diff --git a/src/servala/frontend/context_processors.py b/src/servala/frontend/context_processors.py index 78dc0a9..1ff2a14 100644 --- a/src/servala/frontend/context_processors.py +++ b/src/servala/frontend/context_processors.py @@ -1,12 +1,5 @@ -from django.conf import settings - - def add_organizations(request): if not request.user.is_authenticated: return {"user_organizations": []} return {"user_organizations": request.user.organizations.all().order_by("name")} - - -def add_beta_banner(request): - return {"show_beta_banner": settings.SERVALA_SHOW_BETA_BANNER} diff --git a/src/servala/frontend/forms/organization.py b/src/servala/frontend/forms/organization.py index 45e7b11..915ad7b 100644 --- a/src/servala/frontend/forms/organization.py +++ b/src/servala/frontend/forms/organization.py @@ -1,73 +1,19 @@ from django import forms -from django.core.exceptions import ValidationError -from django.core.validators import RegexValidator from django.forms import ModelForm from django.utils.translation import gettext_lazy as _ -from servala.core.models import Organization, OrganizationInvitation, OrganizationRole +from servala.core.models import Organization from servala.core.odoo import get_invoice_addresses, get_odoo_countries from servala.frontend.forms.mixins import HtmxMixin -ORG_NAME_PATTERN = r"[\w\s\-.,&'()+]+" - class OrganizationForm(HtmxMixin, ModelForm): - name_validator = RegexValidator( - regex=f"^{ORG_NAME_PATTERN}$", - message=_( - "Organization name can only contain letters, numbers, spaces, and common punctuation (-.,&'()+)." - ), - ) - - # def __init__(self, *args, **kwargs): - # super().__init__(*args, **kwargs) - # if self.instance and self.instance.has_inherited_billing_entity: - # TODO disable billing entity editing class Meta: model = Organization fields = ("name",) - widgets = { - "name": forms.TextInput( - attrs={ - "maxlength": "100", - "pattern": ORG_NAME_PATTERN, - "title": _( - "Organization name can contain letters, numbers, spaces, and common punctuation (-.,&'()+). Emoji not allowed." - ), - } - ), - } - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields["name"].validators.append(self.name_validator) - self.fields["name"].max_length = 100 class OrganizationCreateForm(OrganizationForm): - address_validator = RegexValidator( - regex=r"^[\w\s\.,\-/()\']+$", - message=_( - "This field can only contain letters, numbers, spaces, and basic punctuation (.,-/()')." - ), - ) - city_validator = RegexValidator( - regex=r"^[\w\s\-\']+$", - message=_("City name contains invalid characters."), - ) - postal_code_validator = RegexValidator( - regex=r"^[\w\s\-]+$", - message=_( - "Postal code can only contain letters, numbers, spaces, and hyphens." - ), - ) - phone_validator = RegexValidator( - regex=r"^[0-9\s\+\-()]+$", - message=_( - "Phone number can only contain numbers, spaces, and basic punctuation (+,-,())." - ), - ) - billing_processing_choice = forms.ChoiceField( choices=[ ("existing", _("Use an existing billing address")), @@ -83,93 +29,24 @@ class OrganizationCreateForm(OrganizationForm): ) # Fields for creating a new billing address in Odoo, prefixed with 'invoice_' - invoice_street = forms.CharField( - label=_("Line 1"), - required=False, - max_length=128, - validators=[address_validator], - widget=forms.TextInput( - attrs={ - "maxlength": "128", - "title": _( - "Letters, numbers, spaces, and basic punctuation allowed. Emoji not allowed." - ), - } - ), - ) - invoice_street2 = forms.CharField( - label=_("Line 2"), - required=False, - max_length=128, - validators=[address_validator], - widget=forms.TextInput( - attrs={ - "maxlength": "128", - "title": _( - "Letters, numbers, spaces, and basic punctuation allowed. Emoji not allowed." - ), - } - ), - ) - invoice_city = forms.CharField( - label=_("City"), - required=False, - max_length=64, - validators=[city_validator], - widget=forms.TextInput( - attrs={ - "maxlength": "64", - "title": _( - "Letters, spaces, hyphens, and apostrophes allowed. Emoji not allowed." - ), - } - ), - ) - invoice_zip = forms.CharField( - label=_("Postal Code"), - required=False, - max_length=20, - validators=[postal_code_validator], - widget=forms.TextInput( - attrs={ - "maxlength": "20", - "title": _( - "Letters, numbers, spaces, and hyphens allowed. Emoji not allowed." - ), - } - ), - ) + invoice_street = forms.CharField(label=_("Line 1"), required=False, max_length=100) + invoice_street2 = forms.CharField(label=_("Line 2"), required=False, max_length=100) + invoice_city = forms.CharField(label=_("City"), required=False, max_length=100) + invoice_zip = forms.CharField(label=_("Postal Code"), required=False, max_length=20) invoice_country = forms.ChoiceField( label=_("Country"), required=False, choices=get_odoo_countries(), ) - invoice_email = forms.EmailField( - label=_("Billing Email"), - required=False, - max_length=254, - widget=forms.EmailInput(attrs={"maxlength": "254"}), - ) - invoice_phone = forms.CharField( - label=_("Phone"), - required=False, - max_length=30, - validators=[phone_validator], - widget=forms.TextInput( - attrs={ - "maxlength": "30", - "pattern": r"[0-9\s\+\-()]+", - "title": _("Only numbers, spaces, and basic punctuation allowed"), - } - ), - ) + invoice_email = forms.EmailField(label=_("Billing Email"), required=False) + invoice_phone = forms.CharField(label=_("Phone"), required=False, max_length=30) class Meta(OrganizationForm.Meta): pass def __init__(self, *args, user=None, **kwargs): super().__init__(*args, **kwargs) - self.user = user + if not self.initial.get("invoice_country"): default_country_name = "Switzerland" country_choices = self.fields["invoice_country"].choices @@ -178,6 +55,7 @@ class OrganizationCreateForm(OrganizationForm): self.initial["invoice_country"] = country_id break + self.user = user self.odoo_addresses = get_invoice_addresses(self.user) if self.odoo_addresses: @@ -230,68 +108,3 @@ class OrganizationCreateForm(OrganizationForm): "existing_odoo_address_id", _("Please select an invoice address.") ) return cleaned_data - - -class OrganizationInvitationForm(forms.ModelForm): - - def __init__(self, *args, organization=None, user_role=None, **kwargs): - super().__init__(*args, **kwargs) - self.organization = organization - self.user_role = user_role - - if user_role: - allowed_roles = self._get_allowed_roles(user_role) - self.fields["role"].choices = [ - (value, label) - for value, label in OrganizationRole.choices - if value in allowed_roles - ] - - def _get_allowed_roles(self, user_role): - role_hierarchy = { - OrganizationRole.OWNER: [ - OrganizationRole.OWNER, - OrganizationRole.ADMIN, - OrganizationRole.MEMBER, - ], - OrganizationRole.ADMIN: [ - OrganizationRole.ADMIN, - OrganizationRole.MEMBER, - ], - OrganizationRole.MEMBER: [], - } - return role_hierarchy.get(user_role, []) - - def clean_email(self): - email = self.cleaned_data["email"].lower() - - if self.organization.members.filter(email__iexact=email).exists(): - raise ValidationError( - _("A user with this email is already a member of this organization.") - ) - - if OrganizationInvitation.objects.filter( - organization=self.organization, - email__iexact=email, - accepted_by__isnull=True, - ).exists(): - raise ValidationError( - _("An invitation has already been sent to this email address.") - ) - - return email - - def save(self, commit=True): - invitation = super().save(commit=False) - invitation.organization = self.organization - if commit: - invitation.save() - return invitation - - class Meta: - model = OrganizationInvitation - fields = ("email", "role") - widgets = { - "email": forms.EmailInput(attrs={"placeholder": _("user@example.com")}), - "role": forms.RadioSelect(), - } diff --git a/src/servala/frontend/forms/service.py b/src/servala/frontend/forms/service.py index 23325f3..5dd78a7 100644 --- a/src/servala/frontend/forms/service.py +++ b/src/servala/frontend/forms/service.py @@ -21,15 +21,6 @@ class ServiceFilterForm(forms.Form): ) q = forms.CharField(label=_("Search"), required=False) - def __init__(self, *args, organization=None, **kwargs): - super().__init__(*args, **kwargs) - if organization and organization.limit_cloudproviders.exists(): - allowed_providers = organization.limit_cloudproviders - if allowed_providers.count() <= 1: - self.fields.pop("cloud_provider", None) - else: - self.fields["cloud_provider"].queryset = allowed_providers - def filter_queryset(self, queryset): if category := self.cleaned_data.get("category"): queryset = queryset.filter(category=category) diff --git a/src/servala/frontend/forms/widgets.py b/src/servala/frontend/forms/widgets.py index 99b7a59..d67030f 100644 --- a/src/servala/frontend/forms/widgets.py +++ b/src/servala/frontend/forms/widgets.py @@ -2,7 +2,6 @@ import json from django import forms from django.core.exceptions import ValidationError -from django.forms.widgets import NumberInput class DynamicArrayWidget(forms.Widget): @@ -217,21 +216,3 @@ class DynamicArrayField(forms.JSONField): raise ValidationError( f"Item {i + 1} must be one of: {', '.join(enum_values)}" ) - - -class NumberInputWithAddon(NumberInput): - """ - Widget for number input fields with a suffix add-on (e.g., "Gi", "MB"). - Renders as a Bootstrap input-group with the suffix displayed as an add-on. - """ - - template_name = "frontend/forms/number_input_with_addon.html" - - def __init__(self, addon_text="", attrs=None): - super().__init__(attrs) - self.addon_text = addon_text - - def get_context(self, name, value, attrs): - context = super().get_context(name, value, attrs) - context["widget"]["addon_text"] = self.addon_text - return context diff --git a/src/servala/frontend/templates/account/login.html b/src/servala/frontend/templates/account/login.html index e7576b0..58cd49b 100644 --- a/src/servala/frontend/templates/account/login.html +++ b/src/servala/frontend/templates/account/login.html @@ -5,7 +5,7 @@ {% translate "Sign in" %} {% endblock html_title %} {% block page_title %} - {% translate "Welcome to Servala - Sovereign App Store" %} + {% translate "Welcome to Servala" %} {% endblock page_title %} {% block card_header %}
{% translate "Ready to get started?" %}

- {% translate "Sign in to your account or create a new one to access your managed service instances and the Servala service catalog" %} + {% translate "Sign in to access your managed service instances and the Servala service catalog" %}

{% for provider in socialaccount_providers %} {% provider_login_url provider process=process scope=scope auth_params=auth_params as href %} -
+ {% csrf_token %} {{ redirect_field }}
{% endfor %} diff --git a/src/servala/frontend/templates/frontend/base.html b/src/servala/frontend/templates/frontend/base.html index 620cb6d..540a088 100644 --- a/src/servala/frontend/templates/frontend/base.html +++ b/src/servala/frontend/templates/frontend/base.html @@ -22,7 +22,6 @@
- {% include 'includes/beta_banner.html' %} {% include 'includes/header.html' %}
@@ -66,8 +65,6 @@

Crafted with in Zurich - {% load version_tags %} - - {% get_version_or_env %}

@@ -81,20 +78,13 @@ - {% block extra_js %} - {% endblock extra_js %} diff --git a/src/servala/frontend/templates/frontend/forms/dynamic_array.html b/src/servala/frontend/templates/frontend/forms/dynamic_array.html index 00c23b0..4b7e68c 100644 --- a/src/servala/frontend/templates/frontend/forms/dynamic_array.html +++ b/src/servala/frontend/templates/frontend/forms/dynamic_array.html @@ -1,9 +1,6 @@
+ id="{{ widget.attrs.id|default:'id_'|add:widget.name }}_container" + data-name="{{ widget.name }}">
{% for item in value_list %}
diff --git a/src/servala/frontend/templates/frontend/forms/field.html b/src/servala/frontend/templates/frontend/forms/field.html index b09f812..3e0a30b 100644 --- a/src/servala/frontend/templates/frontend/forms/field.html +++ b/src/servala/frontend/templates/frontend/forms/field.html @@ -14,7 +14,7 @@ {% endif %} {% if field.use_fieldset %}{% endif %} {% for text in field.errors %}
{{ text }}
{% endfor %} - {% if field.help_text and not field.is_hidden and not field.field.widget.input_type == "hidden" %} + {% if field.help_text %} {{ field.help_text|safe }} {% endif %} diff --git a/src/servala/frontend/templates/frontend/forms/number_input_with_addon.html b/src/servala/frontend/templates/frontend/forms/number_input_with_addon.html deleted file mode 100644 index 4fe3b54..0000000 --- a/src/servala/frontend/templates/frontend/forms/number_input_with_addon.html +++ /dev/null @@ -1,11 +0,0 @@ -
- - {{ widget.addon_text }} -
diff --git a/src/servala/frontend/templates/frontend/organizations/dashboard.html b/src/servala/frontend/templates/frontend/organizations/dashboard.html index 441d0be..01faa69 100644 --- a/src/servala/frontend/templates/frontend/organizations/dashboard.html +++ b/src/servala/frontend/templates/frontend/organizations/dashboard.html @@ -1,7 +1,7 @@ {% extends "frontend/base.html" %} {% load i18n static %} {% block html_title %} - {% translate "Dashboard" %} for {{ object.name }} + {{ object.name }} {% translate "Dashboard" %} {% endblock html_title %} {% block page_title %}{% endblock %} {% block content %} @@ -87,6 +87,7 @@ {% translate "Name" %} {% translate "Service" %} + {% translate "Status" %} {% translate "Created" %} {% translate "Actions" %} @@ -95,7 +96,7 @@ {% for instance in service_instances %} - {{ instance.name }} @@ -116,13 +117,13 @@
- {% if instance.has_change_permission %} - diff --git a/src/servala/frontend/templates/frontend/organizations/invitation_accept.html b/src/servala/frontend/templates/frontend/organizations/invitation_accept.html deleted file mode 100644 index fa0bfd7..0000000 --- a/src/servala/frontend/templates/frontend/organizations/invitation_accept.html +++ /dev/null @@ -1,42 +0,0 @@ -{% extends "frontend/base.html" %} -{% load i18n %} -{% block html_title %} - {% block page_title %} - {% translate "Accept Organization Invitation" %} - {% endblock page_title %} -{% endblock html_title %} -{% block content %} -
- -
-{% endblock content %} diff --git a/src/servala/frontend/templates/frontend/organizations/service_detail.html b/src/servala/frontend/templates/frontend/organizations/service_detail.html index a101ed1..55cf31e 100644 --- a/src/servala/frontend/templates/frontend/organizations/service_detail.html +++ b/src/servala/frontend/templates/frontend/organizations/service_detail.html @@ -32,7 +32,13 @@
{% translate "External Links" %}
{% for link in service.external_links %} - {% include "includes/external_link.html" %} + + {{ link.title }} + + {% endfor %}
@@ -41,7 +47,7 @@
- {% for offering in visible_offerings %} + {% for offering in service.offerings.all %}
{% endif %} - {% if control_plane.user_info %} -
-
-

{% translate "Service Provider Zone Information" %}

-
-
- {% include "includes/control_plane_user_info.html" with control_plane=instance.context.control_plane %} -
-
- {% endif %} + {% include "includes/control_plane_user_info.html" with control_plane=instance.context.control_plane %}
{% if instance.spec and spec_fieldsets %}
@@ -191,36 +173,34 @@
{% endif %} {% if instance.connection_credentials %} -
-
-
-

{% translate "Connection Credentials" %}

-
-
-
- - +
+
+

{% translate "Connection Credentials" %}

+
+
+
+
+ + + + + + + + {% for key, value in instance.connection_credentials.items %} - - + + - - - {% for key, value in instance.connection_credentials.items %} - - - - - {% endfor %} - -
{% translate "Name" %}{% translate "Value" %}
{% translate "Name" %}{% translate "Value" %}{{ key }} + {% if key == "error" %} + {{ value }} + {% else %} + {{ value }} + {% endif %} +
{{ key }} - {% if key == "error" %} - {{ value }} - {% else %} - {{ value }} - {% endif %} -
-
+ {% endfor %} + +
@@ -257,12 +237,3 @@
{% endblock content %} -{% block extra_js %} - -{% endblock extra_js %} diff --git a/src/servala/frontend/templates/frontend/organizations/service_instance_update.html b/src/servala/frontend/templates/frontend/organizations/service_instance_update.html index 17b9a51..51a9213 100644 --- a/src/servala/frontend/templates/frontend/organizations/service_instance_update.html +++ b/src/servala/frontend/templates/frontend/organizations/service_instance_update.html @@ -13,7 +13,7 @@ {% translate "Back" %} {% endblock page_title_extra %} {% partialdef service-form %} -{% if form or custom_form %} +{% if form %}
@@ -22,7 +22,7 @@ {% translate "Oops! Something went wrong with the service form generation. Please try again later." %}
{% else %} - {% include "includes/tabbed_fieldset_form.html" with form=custom_form expert_form=form %} + {% include "includes/tabbed_fieldset_form.html" with form=form %} {% endif %}
@@ -31,7 +31,7 @@ {% block content %}
- {% if not form and not custom_form %} + {% if not form %} diff --git a/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html b/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html index 927c6e3..7f3863e 100644 --- a/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html +++ b/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html @@ -7,17 +7,13 @@ {{ offering }} {% endblock page_title %} {% endblock html_title %} -{% partialdef control-plane-info inline=True %} -{% if selected_plane and selected_plane.user_info %} -
-
- {% include "includes/control_plane_user_info.html" with control_plane=selected_plane %} -
-
+{% partialdef control-plane-info %} +{% if selected_plane %} + {% include "includes/control_plane_user_info.html" with control_plane=selected_plane %} {% endif %} {% endpartialdef %} {% partialdef service-form %} -{% if service_form or custom_service_form %} +{% if service_form %}
@@ -26,7 +22,7 @@ {% translate "Oops! Something went wrong with the service form generation. Please try again later." %}
{% else %} - {% include "includes/tabbed_fieldset_form.html" with form=custom_service_form expert_form=service_form %} + {% include "includes/tabbed_fieldset_form.html" with form=service_form %} {% endif %}
@@ -34,127 +30,69 @@ {% endpartialdef %} {% block content %}
- {% if not has_control_planes %} - -
-
-
{% endblock content %} -{% block extra_js %} - {% if wildcard_dns and organization_namespace %} - - - {% endif %} - -{% endblock extra_js %} diff --git a/src/servala/frontend/templates/frontend/organizations/services.html b/src/servala/frontend/templates/frontend/organizations/services.html index 3250c52..3a48ff9 100644 --- a/src/servala/frontend/templates/frontend/organizations/services.html +++ b/src/servala/frontend/templates/frontend/organizations/services.html @@ -16,37 +16,50 @@
-
+
{% for service in services %} -
{% include "includes/service_card.html" %}
- {% empty %} -
+
-
-
-

{% translate "No services found." %}

+ +
+
+ {% if service.description %}

{{ service.description|urlize }}

{% endif %} +
+
+ +
+
+ {% empty %} +
+
+
+

{% translate "No services found." %}

+
{% endfor %}
- {% if deactivated_services %} -
-
-
{% translate "You may also be interested in one of these …" %}
-

- - {% translate "These services need to be enabled first before they become available in the Servala portal." %} -

-
-
-
- {% for service in deactivated_services %} -
{% include "includes/service_card.html" %}
- {% endfor %} -
- {% endif %} {% endblock content %} diff --git a/src/servala/frontend/templates/frontend/organizations/update.html b/src/servala/frontend/templates/frontend/organizations/update.html index bc4edf8..97d266d 100644 --- a/src/servala/frontend/templates/frontend/organizations/update.html +++ b/src/servala/frontend/templates/frontend/organizations/update.html @@ -36,97 +36,6 @@ {% endpartialdef org-name-edit %} -{% partialdef members-list %} -
- - - - - - - - - - - {% for membership in memberships %} - - - - - - - {% empty %} - - - - {% endfor %} - -
{% translate "Name" %}{% translate "Email" %}{% translate "Role" %}{% translate "Joined" %}
{{ membership.user }}{{ membership.user.email }} - - {{ membership.get_role_display }} - - {{ membership.date_joined|date:"Y-m-d" }}
{% translate "No members yet" %}
-
-{% endpartialdef members-list %} -{% partialdef pending-invitations-card %} -{% if pending_invitations %} -
-
-

- {% translate "Pending Invitations" %} -

-
-
-
-
- - - - - - - - - - - {% for invitation in pending_invitations %} - - - - - - - {% endfor %} - -
{% translate "Email" %}{% translate "Role" %}{% translate "Sent" %}{% translate "Actions" %}
{{ invitation.email }} - - {{ invitation.get_role_display }} - - {{ invitation.created_at|date:"Y-m-d H:i" }} - -
- {% csrf_token %} - - -
-
-
-
-
-
-{% endif %} -{% endpartialdef pending-invitations-card %} {% block content %}
@@ -156,136 +65,67 @@
- {% if not form.instance.origin.hide_billing_address %} - {% if form.instance.billing_entity and form.instance.billing_entity.odoo_data.invoice_address %} -
-
-

{% translate "Billing Address" %}

- {% if form.instance.has_inherited_billing_entity %} -

- {% translate "This billing address cannot be modified." %} -

- {% endif %} -
-
-
- {% with odoo_data=form.instance.billing_entity.odoo_data %} -
- - - {% if odoo_data.invoice_address %} - - - - - - - - - - {% if odoo_data.invoice_address.street2 %} - - - - - {% endif %} - - - - - - - - - - - - - - - - {% endif %} - -
- {% translate "Invoice Contact Name" %} - {{ odoo_data.invoice_address.name|default:"" }}
- {% translate "Street" %} - {{ odoo_data.invoice_address.street|default:"" }}
- {% translate "Street 2" %} - {{ odoo_data.invoice_address.street2 }}
- {% translate "City" %} - {{ odoo_data.invoice_address.city|default:"" }}
- {% translate "ZIP Code" %} - {{ odoo_data.invoice_address.zip|default:"" }}
- {% translate "Country" %} - {{ odoo_data.invoice_address.country_id.1|default:"" }}
- {% translate "Invoice Email" %} - {{ odoo_data.invoice_address.email|default:"" }}
-
- {% endwith %} -
-
-
- {% endif %} - {% elif form.instance.origin.billing_message %} + {% if form.instance.billing_entity and form.instance.billing_entity.odoo_data.invoice_address %}
-

{% translate "Billing Information" %}

+

{% translate "Billing Address" %}

-

{{ form.instance.origin.billing_message }}

-
-
-
- {% endif %} - {% if can_manage_members %} -
-
-

- {% translate "Members" %} -

-
-
-
{% partial members-list %}
-
-
-
{% partial pending-invitations-card %}
-
-
-

- {% translate "Invite New Member" %} -

-
-
-
-
-
- {% translate "Role Permissions" %} -
-
    -
  • - {% translate "Owner" %}: {% translate "Can manage all organization settings, members, services, and can appoint administrators." %} -
  • -
  • - {% translate "Administrator" %}: {% translate "Can manage members, invite users, and manage all services and instances." %} -
  • -
  • - {% translate "Member" %}: {% translate "Can view organization details, create and manage their own service instances." %} -
  • -
-
-
- {% csrf_token %} -
{{ invitation_form }}
-
-
- -
+ {% with odoo_data=form.instance.billing_entity.odoo_data %} +
+ + + {% if odoo_data.invoice_address %} + + + + + + + + + + {% if odoo_data.invoice_address.street2 %} + + + + + {% endif %} + + + + + + + + + + + + + + + + {% endif %} + +
+ {% translate "Invoice Contact Name" %} + {{ odoo_data.invoice_address.name|default:"" }}
+ {% translate "Street" %} + {{ odoo_data.invoice_address.street|default:"" }}
+ {% translate "Street 2" %} + {{ odoo_data.invoice_address.street2 }}
+ {% translate "City" %} + {{ odoo_data.invoice_address.city|default:"" }}
+ {% translate "ZIP Code" %} + {{ odoo_data.invoice_address.zip|default:"" }}
+ {% translate "Country" %} + {{ odoo_data.invoice_address.country_id.1|default:"" }}
+ {% translate "Invoice Email" %} + {{ odoo_data.invoice_address.email|default:"" }}
- + {% endwith %}
diff --git a/src/servala/frontend/templates/includes/beta_banner.html b/src/servala/frontend/templates/includes/beta_banner.html deleted file mode 100644 index 04bd91d..0000000 --- a/src/servala/frontend/templates/includes/beta_banner.html +++ /dev/null @@ -1,13 +0,0 @@ -{% if show_beta_banner %} -
-
-
- BETA - The Servala Portal is currently in beta testing. Your feedback helps us improve! - -
-
-
-{% endif %} diff --git a/src/servala/frontend/templates/includes/control_plane_user_info.html b/src/servala/frontend/templates/includes/control_plane_user_info.html index fdcc995..b9ffe99 100644 --- a/src/servala/frontend/templates/includes/control_plane_user_info.html +++ b/src/servala/frontend/templates/includes/control_plane_user_info.html @@ -1,24 +1,26 @@ {% load i18n %} +{% comment %} +Reusable snippet for displaying ControlPlane user_info +Usage: {% include "includes/control_plane_user_info.html" with control_plane=control_plane_object %} +{% endcomment %} {% if control_plane.user_info %} -
- {% for info in control_plane.user_info %} -
-
- {{ info.title }} - {% if info.help_text %} - - {% endif %} -
-
- {{ info.content }} -
+
+
+

{% translate "Service Provider Zone Information" %}

+
+
+
+ + + {% for key, value in control_plane.user_info.items %} + + + + + {% endfor %} + +
{{ key }}{{ value }}
- {% endfor %} +
{% endif %} diff --git a/src/servala/frontend/templates/includes/external_link.html b/src/servala/frontend/templates/includes/external_link.html deleted file mode 100644 index e8319bf..0000000 --- a/src/servala/frontend/templates/includes/external_link.html +++ /dev/null @@ -1,7 +0,0 @@ - - {{ link.title }} - - diff --git a/src/servala/frontend/templates/includes/header.html b/src/servala/frontend/templates/includes/header.html index 7db28b4..9176a98 100644 --- a/src/servala/frontend/templates/includes/header.html +++ b/src/servala/frontend/templates/includes/header.html @@ -130,7 +130,7 @@ {% else %} - {% translate 'Sign in' %} + {% translate 'Login' %} {% endif %} diff --git a/src/servala/frontend/templates/includes/service_card.html b/src/servala/frontend/templates/includes/service_card.html deleted file mode 100644 index dcf739f..0000000 --- a/src/servala/frontend/templates/includes/service_card.html +++ /dev/null @@ -1,31 +0,0 @@ -{% load i18n %} - diff --git a/src/servala/frontend/templates/includes/tabbed_fieldset_form.html b/src/servala/frontend/templates/includes/tabbed_fieldset_form.html index 5f289b7..c9d947a 100644 --- a/src/servala/frontend/templates/includes/tabbed_fieldset_form.html +++ b/src/servala/frontend/templates/includes/tabbed_fieldset_form.html @@ -1,149 +1,56 @@ {% load i18n %} {% load get_field %} -{% load static %}
{% csrf_token %} {% include "frontend/forms/errors.html" %} - {% if form and expert_form and not hide_expert_mode %} - - {% endif %} -
- {% if form and form.context %}{{ form.context }}{% endif %} - {% if form and form.get_fieldsets|length == 1 %} - {# Single fieldset - render without tabs #} - {% for fieldset in form.get_fieldsets %} -
- {% for field in fieldset.fields %} - {% with field=form|get_field:field %}{{ field.as_field_group }}{% endwith %} - {% endfor %} - {% for subfieldset in fieldset.fieldsets %} - {% if subfieldset.fields %} -
-

{{ subfieldset.title }}

- {% for field in subfieldset.fields %} - {% with field=form|get_field:field %}{{ field.as_field_group }}{% endwith %} - {% endfor %} -
- {% endif %} - {% endfor %} -
- {% endfor %} - {% elif form %} - {# Multiple fieldsets or auto-generated form - render with tabs #} - -
- {% for fieldset in form.get_fieldsets %} -
- {% for field in fieldset.fields %} + {% for subfieldset in fieldset.fieldsets %} + {% if subfieldset.fields %} +

{{ subfieldset.title }}

+ {% for field in subfieldset.fields %} {% with field=form|get_field:field %}{{ field.as_field_group }}{% endwith %} {% endfor %} - {% for subfieldset in fieldset.fieldsets %} - {% if subfieldset.fields %} -
-

{{ subfieldset.title }}

- {% for field in subfieldset.fields %} - {% with field=form|get_field:field %}{{ field.as_field_group }}{% endwith %} - {% endfor %} -
- {% endif %} - {% endfor %} -
- {% endfor %} -
- {% endif %} -
- {% if expert_form and not hide_expert_mode %} -
- {% if expert_form and expert_form.context %}{{ expert_form.context }}{% endif %} - -
- {% for fieldset in expert_form.get_fieldsets %} -
- {% for field in fieldset.fields %} - {% with field=expert_form|get_field:field %}{{ field.as_field_group }}{% endwith %} - {% endfor %} - {% for subfieldset in fieldset.fieldsets %} - {% if subfieldset.fields %} -
-

{{ subfieldset.title }}

- {% for field in subfieldset.fields %} - {% with field=expert_form|get_field:field %}{{ field.as_field_group }}{% endwith %} - {% endfor %} -
- {% endif %} - {% endfor %} -
- {% endfor %}
-
- {% endif %} - {% if form %} - - {% endif %} + {% endfor %} +
- {# browser form validation fails when there are fields missing/invalid that are hidden #} - +
- -{% if form and not hide_expert_mode %} - -{% endif %} diff --git a/src/servala/frontend/templatetags/get_field.py b/src/servala/frontend/templatetags/get_field.py index 2141178..3214beb 100644 --- a/src/servala/frontend/templatetags/get_field.py +++ b/src/servala/frontend/templatetags/get_field.py @@ -1,5 +1,3 @@ -from contextlib import suppress - from django import template register = template.Library() @@ -7,5 +5,4 @@ register = template.Library() @register.filter def get_field(form, field_name): - with suppress(KeyError): - return form[field_name] + return form[field_name] diff --git a/src/servala/frontend/templatetags/version_tags.py b/src/servala/frontend/templatetags/version_tags.py deleted file mode 100644 index 6019738..0000000 --- a/src/servala/frontend/templatetags/version_tags.py +++ /dev/null @@ -1,16 +0,0 @@ -import os - -from django import template - -from servala.__about__ import __version__ - -register = template.Library() - - -@register.simple_tag -def get_version_or_env(): - """Return version number in production, environment name otherwise.""" - env = os.environ.get("SERVALA_ENVIRONMENT", "development") - if env == "production": - return __version__ - return env diff --git a/src/servala/frontend/urls.py b/src/servala/frontend/urls.py index 73d0759..7790b22 100644 --- a/src/servala/frontend/urls.py +++ b/src/servala/frontend/urls.py @@ -6,11 +6,6 @@ from servala.frontend import views urlpatterns = [ path("accounts/profile/", views.ProfileView.as_view(), name="profile"), path("accounts/logout/", views.LogoutView.as_view(), name="logout"), - path( - "invitations//accept/", - views.InvitationAcceptView.as_view(), - name="invitation.accept", - ), path( "organizations/", views.OrganizationSelectionView.as_view(), @@ -30,11 +25,6 @@ urlpatterns = [ views.OrganizationUpdateView.as_view(), name="organization.details", ), - path( - "details/invitations//delete/", - views.InvitationDeleteView.as_view(), - name="invitation.delete", - ), path( "services/", views.ServiceListView.as_view(), diff --git a/src/servala/frontend/views/__init__.py b/src/servala/frontend/views/__init__.py index 33b0560..5f11a75 100644 --- a/src/servala/frontend/views/__init__.py +++ b/src/servala/frontend/views/__init__.py @@ -8,8 +8,6 @@ from .generic import ( custom_500, ) from .organization import ( - InvitationAcceptView, - InvitationDeleteView, OrganizationCreateView, OrganizationDashboardView, OrganizationUpdateView, @@ -27,8 +25,6 @@ from .support import SupportView __all__ = [ "IndexView", - "InvitationAcceptView", - "InvitationDeleteView", "LogoutView", "OrganizationCreateView", "OrganizationDashboardView", diff --git a/src/servala/frontend/views/organization.py b/src/servala/frontend/views/organization.py index c4c1336..2f35f76 100644 --- a/src/servala/frontend/views/organization.py +++ b/src/servala/frontend/views/organization.py @@ -1,31 +1,16 @@ -from django.contrib import messages -from django.shortcuts import get_object_or_404, redirect -from django.urls import reverse -from django.utils import timezone -from django.utils.decorators import method_decorator -from django.utils.functional import cached_property +from django.shortcuts import redirect from django.utils.translation import gettext_lazy as _ -from django.views.generic import CreateView, DeleteView, DetailView, TemplateView -from django_scopes import scopes_disabled +from django.views.generic import CreateView, DetailView from rules.contrib.views import AutoPermissionRequiredMixin from servala.core.models import ( BillingEntity, Organization, - OrganizationInvitation, OrganizationMembership, ServiceInstance, ) -from servala.frontend.forms.organization import ( - OrganizationCreateForm, - OrganizationForm, - OrganizationInvitationForm, -) -from servala.frontend.views.mixins import ( - HtmxUpdateView, - HtmxViewMixin, - OrganizationViewMixin, -) +from servala.frontend.forms.organization import OrganizationCreateForm, OrganizationForm +from servala.frontend.views.mixins import HtmxUpdateView, OrganizationViewMixin class OrganizationCreateView(AutoPermissionRequiredMixin, CreateView): @@ -111,225 +96,10 @@ class OrganizationDashboardView( return context -class OrganizationMembershipMixin: +class OrganizationUpdateView(OrganizationViewMixin, HtmxUpdateView): template_name = "frontend/organizations/update.html" - - @cached_property - def user_role(self): - membership = ( - OrganizationMembership.objects.filter( - user=self.request.user, organization=self.get_object() - ) - .order_by("role") - .first() - ) - return membership.role if membership else None - - @cached_property - def can_manage_members(self): - return self.request.user.has_perm( - "core.change_organization", self.request.organization - ) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - organization = self.get_object() - - if self.can_manage_members: - memberships = ( - OrganizationMembership.objects.filter(organization=organization) - .select_related("user") - .order_by("role", "user__email") - ) - pending_invitations = OrganizationInvitation.objects.filter( - organization=organization, accepted_by__isnull=True - ).order_by("-created_at") - invitation_form = OrganizationInvitationForm( - organization=organization, user_role=self.user_role - ) - context.update( - { - "memberships": memberships, - "pending_invitations": pending_invitations, - "invitation_form": invitation_form, - "can_manage_members": self.can_manage_members, - "user_role": self.user_role, - } - ) - - return context - - -class OrganizationUpdateView( - OrganizationViewMixin, OrganizationMembershipMixin, HtmxUpdateView -): form_class = OrganizationForm - fragments = ( - "org-name", - "org-name-edit", - "members-list", - "pending-invitations-card", - ) - - def post(self, request, *args, **kwargs): - if "invite_email" in request.POST: - return self.handle_invitation(request) - return super().post(request, *args, **kwargs) - - def handle_invitation(self, request): - organization = self.get_object() - if not self.can_manage_members: - messages.error(request, _("You do not have permission to invite members.")) - return redirect(self.get_success_url()) - - form = OrganizationInvitationForm( - request.POST, organization=organization, user_role=self.user_role - ) - - if form.is_valid(): - invitation = form.save(commit=False) - invitation.created_by = request.user - invitation.save() - - try: - invitation.send_invitation_email(request) - messages.success( - request, - _( - "Invitation sent to {email}. They will receive an email with the invitation link." - ).format(email=invitation.email), - ) - except Exception: - messages.warning( - request, - _( - "Invitation created for {email}, but email failed to send. Share this link manually: {url}" - ).format( - email=invitation.email, - url=request.build_absolute_uri(invitation.urls.accept), - ), - ) - else: - for error in form.errors.values(): - for error_msg in error: - messages.error(request, error_msg) - - if self.is_htmx and self._get_fragment(): - return self.get(request, *self.args, **self.kwargs) - - return redirect(self.get_success_url()) + fragments = ("org-name", "org-name-edit") def get_success_url(self): return self.request.path - - -@method_decorator(scopes_disabled(), name="dispatch") -class InvitationAcceptView(TemplateView): - template_name = "frontend/organizations/invitation_accept.html" - - def get_invitation(self): - secret = self.kwargs.get("secret") - return get_object_or_404(OrganizationInvitation, secret=secret) - - def dispatch(self, request, *args, **kwargs): - invitation = self.get_invitation() - - if invitation.is_accepted: - messages.warning( - request, - _("This invitation has already been accepted."), - ) - return redirect("frontend:organization.selection") - if not request.user.is_authenticated: - request.session["invitation_next"] = request.path - messages.info( - request, - _("Please log in or sign up to accept this invitation."), - ) - return redirect(f"{reverse('account_login')}?next={request.path}") - return super().dispatch(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["invitation"] = self.get_invitation() - return context - - def post(self, request, *args, **kwargs): - invitation = self.get_invitation() - invitation.accepted_by = request.user - invitation.accepted_at = timezone.now() - invitation.save() - - OrganizationMembership.objects.get_or_create( - user=request.user, - organization=invitation.organization, - defaults={"role": invitation.role}, - ) - - messages.success( - request, - _("You have successfully joined {organization}!").format( - organization=invitation.organization.name - ), - ) - - request.session.pop("invitation_next", None) - return redirect(invitation.organization.urls.base) - - -class InvitationDeleteView(HtmxViewMixin, OrganizationMembershipMixin, DeleteView): - model = OrganizationInvitation - http_method_names = ["get", "post"] - fragments = ("pending-invitations-card",) - - def get_queryset(self): - return OrganizationInvitation.objects.filter(accepted_by__isnull=True) - - def get_success_url(self): - return self.object.organization.urls.details - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - organization = self.request.organization - context["pending_invitations"] = OrganizationInvitation.objects.filter( - organization=organization, accepted_by__isnull=True - ).order_by("-created_at") - return context - - def _check_permission(self): - return self.request.user.has_perm( - "core.change_organization", self.request.organization - ) - - def get_object(self): - if self.request.method == "POST" and self.is_htmx: - try: - return super().get_object() - except Exception: - return - return super().get_object() - - def post(self, request, *args, **kwargs): - self.object = self.get_object() - organization = self.object.organization - - if not self._check_permission(): - if not self.is_htmx: - messages.error( - request, - _("You do not have permission to delete this invitation."), - ) - return redirect(organization.urls.details) - - email = self.object.email - self.object.delete() - if not self.is_htmx: - messages.success( - request, - _("Invitation for {email} has been deleted.").format(email=email), - ) - - if self.is_htmx and self._get_fragment(): - return self.get(request, *args, **kwargs) - - return redirect(self.get_success_url()) diff --git a/src/servala/frontend/views/service.py b/src/servala/frontend/views/service.py index c26194d..6a58a0f 100644 --- a/src/servala/frontend/views/service.py +++ b/src/servala/frontend/views/service.py @@ -1,6 +1,6 @@ from django.contrib import messages from django.core.exceptions import ValidationError -from django.http import Http404, HttpResponse +from django.http import HttpResponse from django.shortcuts import redirect from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ @@ -36,24 +36,22 @@ class ServiceListView(OrganizationViewMixin, ListView): def get_queryset(self): """Return all services.""" - services = self.request.organization.get_visible_services() - + services = ( + Service.objects.all() + .select_related("category") + .prefetch_related("offerings__provider") + ) if self.filter_form.is_valid(): services = self.filter_form.filter_queryset(services) return services.distinct() @cached_property def filter_form(self): - return ServiceFilterForm( - data=self.request.GET or None, organization=self.request.organization - ) + return ServiceFilterForm(data=self.request.GET or None) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["filter_form"] = self.filter_form - context["deactivated_services"] = ( - self.request.organization.get_deactivated_services() - ) return context @@ -64,36 +62,10 @@ class ServiceDetailView(OrganizationViewMixin, DetailView): permission_type = "view" def get_queryset(self): - return self.request.organization.get_visible_services() - - @cached_property - def visible_offerings(self): - offerings = self.object.offerings.all() - if self.request.organization.limit_cloudproviders.exists(): - offerings = offerings.filter( - provider__in=self.request.organization.limit_cloudproviders.all() - ) - return offerings - - def get(self, request, *args, **kwargs): - self.object = self.get_object() - - # If there's exactly one offering, skip provider selection and go directly to it - if self.visible_offerings.count() == 1: - offering = self.visible_offerings.first() - return redirect( - "frontend:organization.offering", - organization=self.request.organization.slug, - slug=self.object.slug, - pk=offering.pk, - ) - - return super().get(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["visible_offerings"] = self.visible_offerings.select_related("provider") - return context + return Service.objects.select_related("category").prefetch_related( + "offerings", + "offerings__provider", + ) class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView): @@ -104,14 +76,7 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView fragments = ("service-form", "control-plane-info") def has_permission(self): - if not self.has_organization_permission(): - return False - if self.request.organization.limit_cloudproviders.exists(): - return ( - self.get_object().provider - in self.request.organization.limit_cloudproviders.all() - ) - return True + return self.has_organization_permission() def get_queryset(self): return ServiceOffering.objects.all().select_related( @@ -142,9 +107,7 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView def context_object(self): if self.request.method == "POST": return ControlPlaneCRD.objects.filter( - pk=self.request.POST.get( - "expert-context", self.request.POST.get("custom-context") - ), + pk=self.request.POST.get("context"), # Make sure we don’t use a malicious ID control_plane__in=self.planes, ).first() @@ -152,52 +115,15 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView control_plane=self.selected_plane, service_offering=self.object ).first() - def get_instance_form_kwargs(self, ignore_data=False): - return { - "initial": { + def get_instance_form(self): + if not self.context_object or not self.context_object.model_form_class: + return None + return self.context_object.model_form_class( + data=self.request.POST if self.request.method == "POST" else None, + initial={ "organization": self.request.organization, "context": self.context_object, }, - "prefix": "expert", - "data": ( - self.request.POST - if (self.request.method == "POST" and not ignore_data) - else None - ), - } - - def get_instance_form(self, ignore_data=False): - if ( - not self.context_object - or not self.context_object.model_form_class - or self.hide_expert_mode - ): - return - - return self.context_object.model_form_class( - **self.get_instance_form_kwargs(ignore_data=ignore_data) - ) - - def get_custom_instance_form(self, ignore_data=False): - if not self.context_object or not self.context_object.custom_model_form_class: - return - kwargs = self.get_instance_form_kwargs(ignore_data=ignore_data) - kwargs["prefix"] = "custom" - return self.context_object.custom_model_form_class(**kwargs) - - @property - def is_custom_form(self): - # Note: "custom form" = user-friendly, subset of fields - # vs "expert form" = auto-generated (all technical fields) - return self.request.POST.get("active_form", "expert") == "custom" - - @cached_property - def hide_expert_mode(self): - return ( - self.context_object - and self.context_object.service_definition - and self.context_object.service_definition.form_config - and self.context_object.service_definition.hide_expert_mode ) def get_context_data(self, **kwargs): @@ -205,23 +131,7 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView context["select_form"] = self.select_form context["has_control_planes"] = self.planes.exists() context["selected_plane"] = self.selected_plane - context["hide_expert_mode"] = self.hide_expert_mode - if self.request.method == "POST": - if self.is_custom_form: - context["service_form"] = self.get_instance_form(ignore_data=True) - context["custom_service_form"] = self.get_custom_instance_form() - else: - context["service_form"] = self.get_instance_form() - context["custom_service_form"] = self.get_custom_instance_form( - ignore_data=True - ) - else: - context["service_form"] = self.get_instance_form() - context["custom_service_form"] = self.get_custom_instance_form() - - if self.selected_plane and self.selected_plane.wildcard_dns: - context["wildcard_dns"] = self.selected_plane.wildcard_dns - context["organization_namespace"] = self.request.organization.namespace + context["service_form"] = self.get_instance_form() return context def post(self, request, *args, **kwargs): @@ -232,10 +142,7 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView context["form_error"] = True return self.render_to_response(context) - if self.is_custom_form: - form = self.get_custom_instance_form() - else: - form = self.get_instance_form() + form = self.get_instance_form() if not form: # Should not happen if context_object is valid, but as a safeguard messages.error( self.request, @@ -263,13 +170,15 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView ) form.add_error(None, error_message) + # If the form is not valid or if the service creation failed, we render it again + context["service_form"] = form return self.render_to_response(context) class ServiceInstanceMixin: model = ServiceInstance context_object_name = "instance" - pk_url_kwarg = "slug" + slug_field = "name" def dispatch(self, *args, **kwargs): self._has_warned = False @@ -286,25 +195,7 @@ class ServiceInstanceMixin: ) def get_object(self, **kwargs): - queryset = kwargs.get("queryset") or self.get_queryset() - - # Get the slug from URL (format: "my-instance-123") - slug = self.kwargs.get(self.pk_url_kwarg) - if slug is None: - raise Http404("No slug provided in URL") - - # Extract pk from the slug (everything after the last dash) - try: - pk_str = slug.rsplit("-", 1)[-1] - pk = int(pk_str) - except (ValueError, IndexError): - raise Http404(f"Invalid slug format: {slug}") - - try: - instance = queryset.get(pk=pk) - except ServiceInstance.DoesNotExist: - raise Http404("Service instance not found") - + instance = super().get_object(**kwargs) if not instance.kubernetes_object and not self._has_warned: messages.warning( self.request, @@ -451,88 +342,11 @@ class ServiceInstanceUpdateView( def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs["instance"] = self.object.spec_object - kwargs["prefix"] = "expert" return kwargs - def get_form(self, *args, ignore_data=False, **kwargs): - if self.hide_expert_mode: - return - if not ignore_data: - return super().get_form(*args, **kwargs) - cls = self.get_form_class() - kwargs = self.get_form_kwargs() - if ignore_data: - kwargs.pop("data", None) - return cls(**kwargs) - - def get_custom_form(self, ignore_data=False): - cls = self.object.context.custom_model_form_class - if not cls: - return - kwargs = self.get_form_kwargs() - kwargs["prefix"] = "custom" - if ignore_data: - kwargs.pop("data", None) - return cls(**kwargs) - - @property - def is_custom_form(self): - # Note: "custom form" = user-friendly, subset of fields - # vs "expert form" = auto-generated (all technical fields) - return self.request.POST.get("active_form", "expert") == "custom" - - def post(self, request, *args, **kwargs): - self.object = self.get_object() - - if self.is_custom_form: - form = self.get_custom_form() - else: - form = self.get_form() - - if form.is_valid(): - return self.form_valid(form) - return self.form_invalid(form) - - @cached_property - def hide_expert_mode(self): - return ( - self.object - and self.object.context - and self.object.context.service_definition - and self.object.context.service_definition.form_config - and self.object.context.service_definition.hide_expert_mode - ) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["hide_expert_mode"] = self.hide_expert_mode - if self.request.method == "POST": - if self.is_custom_form: - context["custom_form"] = self.get_custom_form() - context["form"] = self.get_form(ignore_data=True) - else: - context["custom_form"] = self.get_custom_form(ignore_data=True) - else: - context["custom_form"] = self.get_custom_form() - return context - - def _deep_merge(self, base, update): - for key, value in update.items(): - if key in base and isinstance(base[key], dict) and isinstance(value, dict): - self._deep_merge(base[key], value) - else: - base[key] = value - return base - def form_valid(self, form): try: - form_data = form.get_nested_data() - spec_data = form_data.get("spec") - - if self.is_custom_form: - current_spec = dict(self.object.spec) if self.object.spec else {} - spec_data = self._deep_merge(current_spec, spec_data) - + spec_data = form.get_nested_data().get("spec") self.object.update_spec(spec_data=spec_data, updated_by=self.request.user) messages.success( self.request, diff --git a/src/servala/frontend/views/support.py b/src/servala/frontend/views/support.py index 2cd4cf3..6f4c4aa 100644 --- a/src/servala/frontend/views/support.py +++ b/src/servala/frontend/views/support.py @@ -1,10 +1,11 @@ +from django.conf import settings from django.contrib import messages from django.shortcuts import redirect from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from django.views.generic import FormView -from servala.core.odoo import create_helpdesk_ticket +from servala.core.odoo import CLIENT from servala.frontend.forms.support import SupportForm from servala.frontend.views.mixins import OrganizationViewMixin @@ -23,16 +24,21 @@ class SupportView(OrganizationViewMixin, FormView): if not partner_id: raise Exception("Could not get or create Odoo contact for user") + ticket_data = { + "name": f"Servala Support - Organization {organization.name}", + "team_id": settings.ODOO["HELPDESK_TEAM_ID"], + "partner_id": partner_id, + "description": message, + } + # All orgs should have a sale order ID, but legacy ones might not have it. # Also, we want to be very sure that support requests work, especially for # organizations where something in the creation process may have gone wrong, # so if the ID does not exist, we omit it entirely. - create_helpdesk_ticket( - title=f"Servala Support - Organization {organization.name}", - description=message, - partner_id=partner_id, - sale_order_id=organization.odoo_sale_order_id or None, - ) + if organization.odoo_sale_order_id: + ticket_data["sale_order_id"] = organization.odoo_sale_order_id + + CLIENT.execute("helpdesk.ticket", "create", [ticket_data]) messages.success( self.request, _( diff --git a/src/servala/settings.py b/src/servala/settings.py index f57ac0d..eef44f2 100644 --- a/src/servala/settings.py +++ b/src/servala/settings.py @@ -20,7 +20,6 @@ from servala.__about__ import __version__ as version SERVALA_ENVIRONMENT = os.environ.get("SERVALA_ENVIRONMENT", "development") DEBUG = SERVALA_ENVIRONMENT == "development" -SERVALA_SHOW_BETA_BANNER = os.environ.get("SERVALA_SHOW_BETA_BANNER", "True") == "True" SECRET_KEY = os.environ.get("SERVALA_SECRET_KEY") if previous_secret_key := os.environ.get("SERVALA_PREVIOUS_SECRET_KEY"): @@ -220,7 +219,6 @@ TEMPLATES = [ "django.contrib.messages.context_processors.messages", "django.template.context_processors.static", "servala.frontend.context_processors.add_organizations", - "servala.frontend.context_processors.add_beta_banner", ], "loaders": template_loaders, }, diff --git a/src/servala/settings_test.py b/src/servala/settings_test.py index fec1bfb..477ecb2 100644 --- a/src/servala/settings_test.py +++ b/src/servala/settings_test.py @@ -8,7 +8,6 @@ overrides/adds settings specific to testing. from servala.settings import * # noqa: F403, F401 SECRET_KEY = "test-secret-key-for-testing-only-do-not-use-in-production" -SALT_KEY = SECRET_KEY PASSWORD_HASHERS = [ "django.contrib.auth.hashers.MD5PasswordHasher", ] diff --git a/src/servala/static/css/servala.css b/src/servala/static/css/servala.css index cc69a4f..9a59b8f 100644 --- a/src/servala/static/css/servala.css +++ b/src/servala/static/css/servala.css @@ -237,127 +237,45 @@ a.btn-keycloak { flex-grow: 1; } -/* Expert CRD Form mandatory field styling */ -.expert-crd-form .form-group.mandatory .form-label { +/* CRD Form mandatory field styling */ +.crd-form .form-group.mandatory .form-label { font-weight: bold; position: relative; } -.expert-crd-form .form-group.mandatory .form-label::after { +.crd-form .form-group.mandatory .form-label::after { content: " *"; color: #dc3545; font-weight: bold; } -.expert-crd-form .form-group.mandatory { +.crd-form .form-group.mandatory { border-left: 3px solid #dc3545; padding-left: 10px; background-color: rgba(220, 53, 69, 0.05); border-radius: 3px; } -.expert-crd-form .nav-tabs .nav-link .mandatory-indicator { +.crd-form .nav-tabs .nav-link .mandatory-indicator { color: #dc3545; font-weight: bold; font-size: 1.1em; margin-left: 4px; } -html[data-bs-theme="dark"] .expert-crd-form .form-group.mandatory { +html[data-bs-theme="dark"] .crd-form .form-group.mandatory { background-color: rgba(220, 53, 69, 0.1); border-left-color: #ff6b6b; } -html[data-bs-theme="dark"] .expert-crd-form .form-group.mandatory .form-label::after { +html[data-bs-theme="dark"] .crd-form .form-group.mandatory .form-label::after { color: #ff6b6b; } -html[data-bs-theme="dark"] .expert-crd-form .nav-tabs .nav-link .mandatory-indicator { +html[data-bs-theme="dark"] .crd-form .nav-tabs .nav-link .mandatory-indicator { color: #ff6b6b; } .crd-form .nav-tabs .nav-link.has-mandatory { position: relative; } - -.service-deactivated .card { - opacity: 50%; - cursor: not-allowed; - img { - opacity: 75% - } - h4, small, p { - color: var(--bs-secondary-color) !important; - } - a.btn-outline-secondary { - color: var(--bs-btn-disabled-color) !important; - background-color: var(--bs-btn-disabled-bg) !important; - border-color: var(--bs-btn-disabled-border-color) !important; - opacity: var(--bs-btn-disabled-opacity); - } - a.btn-secondary { - color: white !important; - } - a.btn { - pointer-events: none; - } -} -.ml-auto { - margin-left: auto !important -} - -.beta-banner { - background: linear-gradient(135deg, var(--bs-primary) 0%, var(--brand-mid) 100%); - color: white; - padding: 0.75rem 0; - text-align: center; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} -.beta-banner-content { - display: flex; - align-items: center; - justify-content: center; - flex-wrap: wrap; - gap: 0.75rem; -} -.beta-banner-badge { - background-color: white; - color: var(--bs-primary); - padding: 0.25rem 0.75rem; - border-radius: 1rem; - font-weight: bold; - font-size: 0.875rem; - letter-spacing: 0.5px; -} -.beta-banner-text { - font-size: 0.95rem; -} -.beta-banner-button { - background-color: white; - color: var(--bs-primary); - border: none; - font-weight: 600; - padding: 0.375rem 1rem; - transition: all 0.2s ease; -} -.beta-banner-button:hover { - background-color: var(--brand-light); - color: var(--bs-primary); - transform: translateY(-1px); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); -} - -html[data-bs-theme="dark"] .beta-banner { - background: linear-gradient(135deg, var(--bs-primary) 0%, #7a4fc4 100%); -} -html[data-bs-theme="dark"] .beta-banner-badge { - background-color: rgba(255, 255, 255, 0.95); -} -html[data-bs-theme="dark"] .beta-banner-button { - background-color: rgba(255, 255, 255, 0.95); - color: var(--bs-primary); -} -html[data-bs-theme="dark"] .beta-banner-button:hover { - background-color: white; - color: var(--bs-primary); -} diff --git a/src/servala/static/js/bootstrap-tabs.js b/src/servala/static/js/bootstrap-tabs.js deleted file mode 100644 index d382475..0000000 --- a/src/servala/static/js/bootstrap-tabs.js +++ /dev/null @@ -1,30 +0,0 @@ -// Bootstrap 5 automatically initializes tabs with data-bs-toggle="tab" -// but we need to ensure they work after HTMX swaps -(function() { - 'use strict'; - - const initBootstrapTabs = () => { - const customTabList = document.querySelectorAll('#myTab button[data-bs-toggle="tab"]'); - customTabList.forEach(function(tabButton) { - new bootstrap.Tab(tabButton); - }); - - const expertTabList = document.querySelectorAll('#expertTab button[data-bs-toggle="tab"]'); - expertTabList.forEach(function(tabButton) { - new bootstrap.Tab(tabButton); - }); - } - - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initBootstrapTabs); - } else { - initBootstrapTabs(); - } - - document.addEventListener('htmx:afterSwap', function(event) { - if (event.detail.target.id === 'service-form' || - event.detail.target.classList.contains('crd-form')) { - initBootstrapTabs(); - } - }); -})(); diff --git a/src/servala/static/js/dynamic-array.js b/src/servala/static/js/dynamic-array.js index 2d0098b..c198ddf 100644 --- a/src/servala/static/js/dynamic-array.js +++ b/src/servala/static/js/dynamic-array.js @@ -7,10 +7,6 @@ const initDynamicArrayWidget = () => { const containers = document.querySelectorAll('.dynamic-array-widget') containers.forEach(container => { - if (container.dataset.initialized === 'true') { - return - } - const itemsContainer = container.querySelector('.array-items') const addButton = container.querySelector('.add-array-item') const hiddenInput = container.querySelector('input[type="hidden"]') @@ -26,7 +22,6 @@ const initDynamicArrayWidget = () => { // Ensure hidden input is synced with visible inputs on initialization updateHiddenInput(container) - container.dataset.initialized = 'true' }) } @@ -129,8 +124,6 @@ const updateRemoveButtonVisibility = (container) => { }) } -window.updateHiddenInput = updateHiddenInput - document.addEventListener('DOMContentLoaded', initDynamicArrayWidget) document.addEventListener('htmx:afterSwap', initDynamicArrayWidget) document.addEventListener('htmx:afterSettle', initDynamicArrayWidget) diff --git a/src/servala/static/js/expert-mode.js b/src/servala/static/js/expert-mode.js deleted file mode 100644 index 2bfc2fa..0000000 --- a/src/servala/static/js/expert-mode.js +++ /dev/null @@ -1,49 +0,0 @@ -(function() { - 'use strict'; - - let isExpertMode = false; - - function initExpertMode() { - const toggleButton = document.getElementById('expert-mode-toggle'); - if (!toggleButton) return; - - const customFormContainer = document.getElementById('custom-form-container'); - const expertFormContainer = document.getElementById('expert-form-container'); - - if (!customFormContainer || !expertFormContainer) { - console.warn('Expert mode containers not found'); - return; - } - - toggleButton.addEventListener('click', function(e) { - e.preventDefault(); - isExpertMode = !isExpertMode; - - const activeFormInput = document.getElementById('active-form-input'); - - if (isExpertMode) { - customFormContainer.style.display = 'none'; - expertFormContainer.style.display = 'block'; - toggleButton.textContent = 'Show Simplified Form'; - if (activeFormInput) activeFormInput.value = 'expert'; - } else { - customFormContainer.style.display = 'block'; - expertFormContainer.style.display = 'none'; - toggleButton.textContent = 'Show Expert Mode'; - if (activeFormInput) activeFormInput.value = 'custom'; - } - }); - } - - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initExpertMode); - } else { - initExpertMode(); - } - - document.addEventListener('htmx:afterSwap', function(event) { - if (event.detail.target.id === 'service-form' || event.detail.target.classList.contains('crd-form')) { - initExpertMode(); - } - }); -})(); diff --git a/src/servala/static/js/fqdn.js b/src/servala/static/js/fqdn.js deleted file mode 100644 index 0996bda..0000000 --- a/src/servala/static/js/fqdn.js +++ /dev/null @@ -1,62 +0,0 @@ - -const initializeFqdnGeneration = (prefix) => { - const nameField = document.querySelector(`input#id_${prefix}-name`); - if (!nameField) return - - // Try to find array input first (DynamicArrayWidget), then fallback to regular text input - const fqdnFieldContainer = document.getElementById(`${prefix}-spec.parameters.service.fqdn_container`) - let fqdnField = null; - let isArrayField = true; - - if (fqdnFieldContainer) { - let fqdnField = fqdnFieldContainer.querySelector('input.array-item-input'); - } else { - fqdnField = document.getElementById(`id_${prefix}-spec.parameters.service.fqdn`); - isArrayField = false; - } - - if (!fqdnField) return - - if (nameField && fqdnField) { - const generateFqdn = (instanceName) => { - if (!instanceName) return ''; - return `${instanceName}-${fqdnConfig.namespace}.${fqdnConfig.wildcardDns}`; - } - - nameField.addEventListener('input', function() { - if (!fqdnField.dataset.manuallyEdited) { - fqdnField.value = generateFqdn(this.value); - if (isArrayField) { - // Update hidden input for array fields - const container = fqdnField.closest('.dynamic-array-widget'); - if (container && window.updateHiddenInput) { - window.updateHiddenInput(container); - } - } - } - }); - - fqdnField.addEventListener('input', function() { - this.dataset.manuallyEdited = 'true'; - }); - - if (nameField.value && !fqdnField.value) { - fqdnField.value = generateFqdn(nameField.value); - if (isArrayField) { - // Update hidden input for array fields - const container = fqdnField.closest('.dynamic-array-widget'); - if (container && window.updateHiddenInput) { - window.updateHiddenInput(container); - } - } - } - } -} - -document.addEventListener('DOMContentLoaded', () => {initializeFqdnGeneration("custom"), initializeFqdnGeneration("expert")}); -document.body.addEventListener('htmx:afterSwap', function(event) { - if (event.detail.target.id === 'service-form') { - initializeFqdnGeneration("custom"); - initializeFqdnGeneration("expert"); - } -}); diff --git a/src/tests/conftest.py b/src/tests/conftest.py index 09db220..32499ca 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -3,7 +3,6 @@ import base64 import pytest from servala.core.models import ( - BillingEntity, Organization, OrganizationMembership, OrganizationOrigin, @@ -22,11 +21,6 @@ def origin(): return OrganizationOrigin.objects.create(name="TESTORIGIN") -@pytest.fixture -def billing_entity(): - return BillingEntity.objects.create(name="Test Entity") - - @pytest.fixture def organization(origin): return Organization.objects.create(name="Test Org", origin=origin) diff --git a/src/tests/test_api_exoscale.py b/src/tests/test_api_exoscale.py index 19f8b93..a14bd44 100644 --- a/src/tests/test_api_exoscale.py +++ b/src/tests/test_api_exoscale.py @@ -5,12 +5,6 @@ from django.core import mail from django_scopes import scopes_disabled from servala.core.models import Organization, OrganizationOrigin, User -from servala.core.models.service import ( - ControlPlane, - ControlPlaneCRD, - ServiceDefinition, - ServiceInstance, -) @pytest.fixture @@ -79,8 +73,12 @@ def test_successful_onboarding_new_organization( assert org.origin == exoscale_origin assert org.namespace.startswith("org-") + user = User.objects.get(email="test@example.com") + assert user.first_name == "Test" + assert user.last_name == "User" with scopes_disabled(): - assert org.invitations.all().filter(email="test@example.com").exists() + membership = org.memberships.get(user=user) + assert membership.role == "owner" billing_entity = org.billing_entity assert billing_entity.name == "Test Organization Display (Exoscale)" @@ -89,14 +87,10 @@ def test_successful_onboarding_new_organization( assert org.odoo_sale_order_id == 789 assert org.odoo_sale_order_name == "SO001" - assert org.limit_osb_services.all().count() == 1 assert len(mail.outbox) == 2 invitation_email = mail.outbox[0] - assert ( - invitation_email.subject - == "You're invited to join Test Organization Display on Servala" - ) + assert invitation_email.subject == "Welcome to Servala - Test Organization Display" assert "test@example.com" in invitation_email.to welcome_email = mail.outbox[1] @@ -104,36 +98,6 @@ def test_successful_onboarding_new_organization( assert "redis/offering/" in welcome_email.body -@pytest.mark.django_db -def test_new_organization_inherits_origin( - osb_client, - test_service, - test_service_offering, - valid_osb_payload, - exoscale_origin, - instance_id, - billing_entity, -): - valid_osb_payload["service_id"] = test_service.osb_service_id - valid_osb_payload["plan_id"] = test_service_offering.osb_plan_id - exoscale_origin.billing_entity = billing_entity - exoscale_origin.save() - - response = osb_client.put( - f"/api/osb/v2/service_instances/{instance_id}", - data=json.dumps(valid_osb_payload), - content_type="application/json", - ) - - assert response.status_code == 201 - response_data = json.loads(response.content) - assert response_data["message"] == "Successfully enabled service" - - org = Organization.objects.get(osb_guid="test-org-guid-123") - assert org.name == "Test Organization Display" - assert org.billing_entity == exoscale_origin.billing_entity - - @pytest.mark.django_db def test_duplicate_organization_returns_existing( osb_client, @@ -143,12 +107,11 @@ def test_duplicate_organization_returns_existing( exoscale_origin, instance_id, ): - org = Organization.objects.create( + Organization.objects.create( name="Existing Org", osb_guid="test-org-guid-123", origin=exoscale_origin, ) - org.limit_osb_services.add(test_service) valid_osb_payload["service_id"] = test_service.osb_service_id valid_osb_payload["plan_id"] = test_service_offering.osb_plan_id @@ -163,7 +126,7 @@ def test_duplicate_organization_returns_existing( response_data = json.loads(response.content) assert response_data["message"] == "Service already enabled" assert Organization.objects.filter(osb_guid="test-org-guid-123").count() == 1 - assert len(mail.outbox) == 0 # No email necessary + assert len(mail.outbox) == 1 # Only one email was sent @pytest.mark.django_db @@ -457,290 +420,3 @@ def test_organization_creation_with_context_only( assert response.status_code == 201 org = Organization.objects.get(osb_guid="fallback-org-guid") assert org is not None - - -@pytest.mark.django_db -def test_delete_offboarding_success( - mock_odoo_success, - osb_client, - test_service, - test_service_offering, - instance_id, -): - response = osb_client.delete( - f"/api/osb/v2/service_instances/{instance_id}" - f"?service_id={test_service.osb_service_id}&plan_id={test_service_offering.osb_plan_id}" - ) - - assert response.status_code == 200 - assert response.content == b"{}" - - -@pytest.mark.django_db -def test_delete_missing_service_id(osb_client, test_service_offering, instance_id): - response = osb_client.delete( - f"/api/osb/v2/service_instances/{instance_id}?plan_id={test_service_offering.osb_plan_id}" - ) - - assert response.status_code == 400 - response_data = json.loads(response.content) - assert "service_id is required but missing" in response_data["error"] - - -@pytest.mark.django_db -def test_delete_missing_plan_id(osb_client, test_service, instance_id): - response = osb_client.delete( - f"/api/osb/v2/service_instances/{instance_id}?service_id={test_service.osb_service_id}" - ) - - assert response.status_code == 400 - response_data = json.loads(response.content) - assert "plan_id is required but missing" in response_data["error"] - - -@pytest.mark.django_db -def test_delete_invalid_service_id(osb_client, instance_id): - response = osb_client.delete( - f"/api/osb/v2/service_instances/{instance_id}?service_id=invalid&plan_id=invalid" - ) - - assert response.status_code == 400 - response_data = json.loads(response.content) - assert "Unknown service_id: invalid" in response_data["error"] - - -@pytest.mark.django_db -def test_delete_invalid_plan_id(osb_client, test_service, instance_id): - response = osb_client.delete( - f"/api/osb/v2/service_instances/{instance_id}" - f"?service_id={test_service.osb_service_id}&plan_id=invalid" - ) - - assert response.status_code == 400 - response_data = json.loads(response.content) - assert ( - f"Unknown plan_id: invalid for service_id: {test_service.osb_service_id}" - in response_data["error"] - ) - - -@pytest.mark.django_db -def test_patch_suspension_success( - mock_odoo_success, - osb_client, - test_service, - test_service_offering, - instance_id, -): - payload = { - "service_id": test_service.osb_service_id, - "plan_id": test_service_offering.osb_plan_id, - "parameters": { - "users": [ - { - "email": "user@example.com", - "full_name": "Test User", - "role": "owner", - } - ] - }, - } - - response = osb_client.patch( - f"/api/osb/v2/service_instances/{instance_id}", - data=json.dumps(payload), - content_type="application/json", - ) - - assert response.status_code == 200 - assert response.content == b"{}" - - -@pytest.mark.django_db -def test_patch_missing_service_id(osb_client, test_service_offering, instance_id): - payload = { - "plan_id": test_service_offering.osb_plan_id, - "parameters": {"users": []}, - } - - response = osb_client.patch( - f"/api/osb/v2/service_instances/{instance_id}", - data=json.dumps(payload), - content_type="application/json", - ) - - assert response.status_code == 400 - response_data = json.loads(response.content) - assert "service_id is required but missing" in response_data["error"] - - -@pytest.mark.django_db -def test_patch_missing_plan_id(osb_client, test_service, instance_id): - payload = { - "service_id": test_service.osb_service_id, - "parameters": {"users": []}, - } - - response = osb_client.patch( - f"/api/osb/v2/service_instances/{instance_id}", - data=json.dumps(payload), - content_type="application/json", - ) - - assert response.status_code == 400 - response_data = json.loads(response.content) - assert "plan_id is required but missing" in response_data["error"] - - -@pytest.mark.django_db -def test_patch_invalid_json(osb_client, instance_id): - response = osb_client.patch( - f"/api/osb/v2/service_instances/{instance_id}", - data="invalid json{", - content_type="application/json", - ) - - assert response.status_code == 400 - response_data = json.loads(response.content) - assert "Invalid JSON in request body" in response_data["error"] - - -@pytest.mark.django_db -def test_delete_creates_ticket_with_admin_links( - mocker, - mock_odoo_success, - osb_client, - test_service, - test_service_offering, - instance_id, -): - # Mock the create_helpdesk_ticket function - mock_create_ticket = mocker.patch("servala.api.views.create_helpdesk_ticket") - - response = osb_client.delete( - f"/api/osb/v2/service_instances/{instance_id}" - f"?service_id={test_service.osb_service_id}&plan_id={test_service_offering.osb_plan_id}" - ) - - assert response.status_code == 200 - - # Verify the ticket was created with admin URL - mock_create_ticket.assert_called_once() - call_kwargs = mock_create_ticket.call_args[1] - - # Check that the description contains an admin URL - assert "admin/core/serviceoffering" in call_kwargs["description"] - assert f"/{test_service_offering.pk}/" in call_kwargs["description"] - assert ( - call_kwargs["title"] - == f"Exoscale OSB Offboard - {test_service.name} - {instance_id}" - ) - - -@pytest.mark.django_db -def test_patch_creates_ticket_with_user_admin_links( - mocker, - mock_odoo_success, - osb_client, - test_service, - test_service_offering, - instance_id, - org_owner, -): - # Mock the create_helpdesk_ticket function - mock_create_ticket = mocker.patch("servala.api.views.create_helpdesk_ticket") - - payload = { - "service_id": test_service.osb_service_id, - "plan_id": test_service_offering.osb_plan_id, - "parameters": { - "users": [ - { - "email": org_owner.email, - "full_name": "Test User", - "role": "owner", - } - ] - }, - } - - response = osb_client.patch( - f"/api/osb/v2/service_instances/{instance_id}", - data=json.dumps(payload), - content_type="application/json", - ) - - assert response.status_code == 200 - - # Verify the ticket was created with admin URLs - mock_create_ticket.assert_called_once() - call_kwargs = mock_create_ticket.call_args[1] - - # Check that the description contains admin URLs - assert "admin/core/serviceoffering" in call_kwargs["description"] - assert "admin/core/user" in call_kwargs["description"] - assert f"/{org_owner.pk}/" in call_kwargs["description"] - assert ( - call_kwargs["title"] - == f"Exoscale OSB Suspend - {test_service.name} - {instance_id}" - ) - - -@pytest.mark.django_db -def test_ticket_includes_organization_and_instance_when_found( - mocker, - mock_odoo_success, - osb_client, - test_service, - test_service_offering, - organization, -): - # Mock the create_helpdesk_ticket function - mock_create_ticket = mocker.patch("servala.api.views.create_helpdesk_ticket") - - service_definition = ServiceDefinition.objects.create( - name="Test Definition", - service=test_service, - api_definition={"group": "test.example.com", "version": "v1", "kind": "Test"}, - ) - control_plane = ControlPlane.objects.create( - name="Test Control Plane", - cloud_provider=test_service_offering.provider, - api_credentials={ - "certificate-authority-data": "test", - "server": "https://test", - "token": "test", - }, - ) - crd = ControlPlaneCRD.objects.create( - service_offering=test_service_offering, - control_plane=control_plane, - service_definition=service_definition, - ) - instance_name = "test-instance-123" - service_instance = ServiceInstance.objects.create( - name=instance_name, - organization=organization, - context=crd, - ) - - response = osb_client.delete( - f"/api/osb/v2/service_instances/{instance_name}" - f"?service_id={test_service.osb_service_id}&plan_id={test_service_offering.osb_plan_id}" - ) - - assert response.status_code == 200 - - # Verify the ticket was created with all admin URLs - mock_create_ticket.assert_called_once() - call_kwargs = mock_create_ticket.call_args[1] - - # Check organization is included - assert f"Organization: {organization.name}" in call_kwargs["description"] - assert "admin/core/organization" in call_kwargs["description"] - assert f"/{organization.pk}/" in call_kwargs["description"] - - # Check instance is included - assert f"Instance: {service_instance.name}" in call_kwargs["description"] - assert "admin/core/serviceinstance" in call_kwargs["description"] - assert f"/{service_instance.pk}/" in call_kwargs["description"] diff --git a/src/tests/test_form_config.py b/src/tests/test_form_config.py deleted file mode 100644 index c93f3fb..0000000 --- a/src/tests/test_form_config.py +++ /dev/null @@ -1,1095 +0,0 @@ -from unittest.mock import Mock - -import jsonschema -from django.core.validators import MaxValueValidator, MinValueValidator -from django.db import models - -from servala.core.crd import generate_custom_form_class -from servala.core.crd.forms import DEFAULT_FIELD_CONFIGS, MANDATORY_FIELDS -from servala.core.forms import ServiceDefinitionAdminForm -from servala.core.models import ControlPlaneCRD - - -def test_custom_model_form_class_returns_class_when_form_config_exists(): - - crd = Mock(spec=ControlPlaneCRD) - service_def = Mock() - service_def.form_config = { - "fieldsets": [ - { - "title": "General", - "fields": [ - { - "type": "text", - "label": "Name", - "controlplane_field_mapping": "name", - "required": True, - } - ], - } - ] - } - crd.service_definition = service_def - - class TestModel(models.Model): - name = models.CharField(max_length=100) - - class Meta: - app_label = "test" - - crd.django_model = TestModel - result = generate_custom_form_class( - crd.service_definition.form_config, crd.django_model - ) - - assert result is not None - assert hasattr(result, "form_config") - - -def test_form_config_schema_validates_minimal_config(): - form = ServiceDefinitionAdminForm() - schema = form.form_config_schema - - minimal_config = { - "fieldsets": [ - { - "fields": [ - { - "type": "text", - "label": "Service Name", - "controlplane_field_mapping": "spec.serviceName", - } - ] - } - ] - } - - jsonschema.validate(instance=minimal_config, schema=schema) - - -def test_form_config_schema_validates_config_with_null_integers(): - form = ServiceDefinitionAdminForm() - schema = form.form_config_schema - - config_with_nulls = { - "fieldsets": [ - { - "fields": [ - { - "type": "text", - "label": "Service Name", - "controlplane_field_mapping": "spec.serviceName", - "max_length": None, - "required": False, - }, - { - "type": "textarea", - "label": "Description", - "controlplane_field_mapping": "spec.description", - "rows": None, - "max_length": None, - }, - { - "type": "number", - "label": "Port", - "controlplane_field_mapping": "spec.port", - "min_value": None, - "max_value": None, - }, - { - "type": "array", - "label": "Tags", - "controlplane_field_mapping": "spec.tags", - "min_values": None, - "max_values": None, - }, - ] - } - ] - } - - jsonschema.validate(instance=config_with_nulls, schema=schema) - - -def test_form_config_schema_validates_full_config(): - form = ServiceDefinitionAdminForm() - schema = form.form_config_schema - - full_config = { - "fieldsets": [ - { - "title": "Service Configuration", - "fields": [ - { - "type": "text", - "label": "Service Name", - "controlplane_field_mapping": "spec.serviceName", - "help_text": "Enter a unique service name", - "required": True, - "max_length": 100, - }, - { - "type": "email", - "label": "Admin Email", - "controlplane_field_mapping": "spec.adminEmail", - "help_text": "Contact email for service administrator", - "required": True, - "max_length": 255, - }, - { - "type": "textarea", - "label": "Description", - "controlplane_field_mapping": "spec.description", - "help_text": "Describe the service purpose", - "required": False, - "rows": 5, - "max_length": 500, - }, - { - "type": "number", - "label": "Port", - "controlplane_field_mapping": "spec.port", - "help_text": "Service port number", - "required": True, - "min_value": 1, - "max_value": 65535, - }, - { - "type": "choice", - "label": "Environment", - "controlplane_field_mapping": "spec.environment", - "help_text": "Deployment environment", - "required": True, - "choices": [ - ["dev", "Development"], - ["staging", "Staging"], - ["prod", "Production"], - ], - }, - { - "type": "checkbox", - "label": "Enable Monitoring", - "controlplane_field_mapping": "spec.monitoring.enabled", - "help_text": "Enable service monitoring", - "required": False, - }, - { - "type": "array", - "label": "Tags", - "controlplane_field_mapping": "spec.tags", - "help_text": "Service tags for organization", - "required": False, - "min_values": 0, - "max_values": 10, - }, - ], - } - ] - } - jsonschema.validate(instance=full_config, schema=schema) - - -def test_choice_field_uses_custom_choices_from_form_config(): - """Test that choice fields use custom choices when provided in form_config""" - - class TestModel(models.Model): - name = models.CharField(max_length=100) - environment = models.CharField( - max_length=20, - choices=[ - ("dev", "Development"), - ("staging", "Staging"), - ("prod", "Production"), - ("test", "Testing"), - ], - ) - - class Meta: - app_label = "test" - - form_config = { - "fieldsets": [ - { - "title": "General", - "fields": [ - { - "type": "text", - "label": "Name", - "controlplane_field_mapping": "name", - "required": True, - }, - { - "type": "choice", - "label": "Environment", - "controlplane_field_mapping": "environment", - "required": True, - "choices": [["dev", "Development"], ["prod", "Production"]], - }, - ], - } - ] - } - - form_class = generate_custom_form_class(form_config, TestModel) - form = form_class() - - environment_field = form.fields["environment"] - assert list(environment_field.choices) == [ - ("dev", "Development"), - ("prod", "Production"), - ] - - assert hasattr(environment_field, "_controlplane_choices") - assert len(environment_field._controlplane_choices) == 5 # 4 choices + empty choice - - -def test_choice_field_uses_control_plane_choices_when_no_custom_choices(): - - class TestModel(models.Model): - name = models.CharField(max_length=100) - environment = models.CharField( - max_length=20, - choices=[ - ("dev", "Development"), - ("staging", "Staging"), - ("prod", "Production"), - ], - ) - - class Meta: - app_label = "test" - - form_config = { - "fieldsets": [ - { - "title": "General", - "fields": [ - { - "type": "text", - "label": "Name", - "controlplane_field_mapping": "name", - "required": True, - }, - { - "type": "choice", - "label": "Environment", - "controlplane_field_mapping": "environment", - "required": True, - }, - ], - } - ] - } - - form_class = generate_custom_form_class(form_config, TestModel) - form = form_class() - - environment_field = form.fields["environment"] - choices_list = list(environment_field.choices) - assert len(choices_list) == 4 # 3 choices + empty choice - assert ("dev", "Development") in choices_list - - -def test_choice_field_validates_against_control_plane_choices(): - class TestModel(models.Model): - name = models.CharField(max_length=100) - environment = models.CharField( - max_length=20, - choices=[ - ("dev", "Development"), - ("staging", "Staging"), - ("prod", "Production"), - ], - ) - - class Meta: - app_label = "test" - - form_config = { - "fieldsets": [ - { - "title": "General", - "fields": [ - { - "type": "text", - "label": "Name", - "controlplane_field_mapping": "name", - "required": True, - }, - { - "type": "choice", - "label": "Environment", - "controlplane_field_mapping": "environment", - "required": True, - "choices": [["dev", "Development"], ["prod", "Production"]], - }, - ], - } - ] - } - - form_class = generate_custom_form_class(form_config, TestModel) - - form = form_class(data={"name": "test-service", "environment": "dev"}) - form.fields["context"].required = False # Skip context validation - assert form.is_valid(), f"Form should be valid but has errors: {form.errors}" - - form = form_class(data={"name": "test-service", "environment": "prod"}) - form.fields["context"].required = False # Skip context validation - assert form.is_valid(), f"Form should be valid but has errors: {form.errors}" - - form = form_class(data={"name": "test-service", "environment": "invalid"}) - form.fields["context"].required = False # Skip context validation - assert not form.is_valid() - assert "environment" in form.errors - - -def test_admin_form_validates_choice_values_against_schema(): - - form = ServiceDefinitionAdminForm() - mock_crd = Mock() - mock_crd.resource_schema = { - "properties": { - "spec": { - "properties": { - "environment": { - "type": "string", - "enum": ["dev", "staging", "prod"], - } - } - } - } - } - - valid_form_config = { - "fieldsets": [ - { - "fields": [ - { - "type": "text", - "label": "Name", - "controlplane_field_mapping": "name", - }, - { - "type": "choice", - "label": "Environment", - "controlplane_field_mapping": "spec.environment", - "choices": [["dev", "Development"], ["prod", "Production"]], - }, - ] - } - ] - } - - spec_schema = mock_crd.resource_schema["properties"]["spec"] - errors = [] - - for field in valid_form_config["fieldsets"][0]["fields"]: - if field.get("type") == "choice": - form._validate_choice_field( - field, field["controlplane_field_mapping"], spec_schema, "spec", errors - ) - - assert len(errors) == 0, f"Expected no errors but got: {errors}" - - invalid_form_config = { - "fieldsets": [ - { - "fields": [ - { - "type": "text", - "label": "Name", - "controlplane_field_mapping": "name", - }, - { - "type": "choice", - "label": "Environment", - "controlplane_field_mapping": "spec.environment", - "choices": [ - ["dev", "Development"], - ["invalid", "Invalid Environment"], - ], - }, - ] - } - ] - } - - errors = [] - - for field in invalid_form_config["fieldsets"][0]["fields"]: - if field.get("type") == "choice": - form._validate_choice_field( - field, field["controlplane_field_mapping"], spec_schema, "spec", errors - ) - - assert len(errors) > 0, "Expected validation errors but got none" - error_message = str(errors[0]) - assert "invalid" in error_message.lower() - assert "Environment" in error_message - - -def test_number_field_min_max_sets_widget_attributes(): - class TestModel(models.Model): - name = models.CharField(max_length=100) - port = models.IntegerField() - replica_count = models.IntegerField() - - class Meta: - app_label = "test" - - form_config = { - "fieldsets": [ - { - "title": "General", - "fields": [ - { - "type": "text", - "label": "Name", - "controlplane_field_mapping": "name", - "required": True, - }, - { - "type": "number", - "label": "Port", - "controlplane_field_mapping": "port", - "required": True, - "min_value": 1, - "max_value": 65535, - }, - { - "type": "number", - "label": "Replicas", - "controlplane_field_mapping": "replica_count", - "required": True, - "min_value": 1, - "max_value": 10, - }, - ], - } - ] - } - - form_class = generate_custom_form_class(form_config, TestModel) - form = form_class() - - port_field = form.fields["port"] - assert port_field.widget.attrs.get("min") == 1 - assert port_field.widget.attrs.get("max") == 65535 - - replica_field = form.fields["replica_count"] - assert replica_field.widget.attrs.get("min") == 1 - assert replica_field.widget.attrs.get("max") == 10 - - port_validators = port_field.validators - assert any( - isinstance(v, MinValueValidator) and v.limit_value == 1 for v in port_validators - ) - assert any( - isinstance(v, MaxValueValidator) and v.limit_value == 65535 - for v in port_validators - ) - - -def test_default_value_for_all_field_types(): - - class TestModel(models.Model): - name = models.CharField(max_length=100) - description = models.TextField() - port = models.IntegerField() - environment = models.CharField( - max_length=20, - choices=[ - ("dev", "Development"), - ("staging", "Staging"), - ("prod", "Production"), - ], - ) - monitoring_enabled = models.BooleanField() - tags = models.JSONField() - - class Meta: - app_label = "test" - - form_config = { - "fieldsets": [ - { - "fields": [ - { - "type": "text", - "label": "Name", - "controlplane_field_mapping": "name", - "default_value": "default-name", - }, - { - "type": "textarea", - "label": "Description", - "controlplane_field_mapping": "description", - "default_value": "Default description text", - }, - { - "type": "number", - "label": "Port", - "controlplane_field_mapping": "port", - "default_value": "8080", - }, - { - "type": "choice", - "label": "Environment", - "controlplane_field_mapping": "environment", - "default_value": "dev", - }, - { - "type": "checkbox", - "label": "Enable Monitoring", - "controlplane_field_mapping": "monitoring_enabled", - "default_value": "true", - }, - { - "type": "array", - "label": "Tags", - "controlplane_field_mapping": "tags", - "default_value": "tag1,tag2,tag3", - }, - ], - } - ] - } - - form_class = generate_custom_form_class(form_config, TestModel) - form = form_class() - - assert form.fields["name"].initial == "default-name" - assert form.fields["description"].initial == "Default description text" - assert form.fields["port"].initial == "8080" - assert form.fields["environment"].initial == "dev" - assert form.fields["monitoring_enabled"].initial == "true" - assert form.fields["tags"].initial == "tag1,tag2,tag3" - - -def test_default_value_not_override_existing_instance(): - - class TestModel(models.Model): - name = models.CharField(max_length=100) - port = models.IntegerField() - - class Meta: - app_label = "test" - - form_config = { - "fieldsets": [ - { - "fields": [ - { - "type": "text", - "label": "Name", - "controlplane_field_mapping": "name", - "default_value": "default-name", - }, - { - "type": "number", - "label": "Port", - "controlplane_field_mapping": "port", - "default_value": "8080", - }, - ], - } - ] - } - - instance = TestModel(name="existing-name", port=3000) - form_class = generate_custom_form_class(form_config, TestModel) - form = form_class(instance=instance) - - assert form.initial["name"] == "existing-name" - assert form.initial["port"] == 3000 - - -def test_form_config_coerces_string_numbers_to_integers(): - form = ServiceDefinitionAdminForm() - schema = form.form_config_schema - - config_with_string_numbers = { - "fieldsets": [ - { - "fields": [ - { - "type": "text", - "label": "Service Name", - "controlplane_field_mapping": "spec.serviceName", - "max_length": "64", # String instead of integer - "required": True, - }, - { - "type": "textarea", - "label": "Description", - "controlplane_field_mapping": "spec.description", - "rows": "5", # String instead of integer - "max_length": "500", # String instead of integer - }, - { - "type": "number", - "label": "Port", - "controlplane_field_mapping": "spec.port", - "min_value": "1", # String instead of integer - "max_value": "65535", # String instead of integer - }, - { - "type": "array", - "label": "Tags", - "controlplane_field_mapping": "spec.tags", - "min_values": "0", # String instead of integer - "max_values": "10", # String instead of integer - }, - ] - } - ] - } - - normalized_config = form._normalize_form_config_types(config_with_string_numbers) - fields = normalized_config["fieldsets"][0]["fields"] - - assert fields[0]["max_length"] == 64 - assert isinstance(fields[0]["max_length"], int) - - assert fields[1]["rows"] == 5 - assert isinstance(fields[1]["rows"], int) - assert fields[1]["max_length"] == 500 - assert isinstance(fields[1]["max_length"], int) - - assert fields[2]["min_value"] == 1 - assert isinstance(fields[2]["min_value"], int) - assert fields[2]["max_value"] == 65535 - assert isinstance(fields[2]["max_value"], int) - - assert fields[3]["min_values"] == 0 - assert isinstance(fields[3]["min_values"], int) - assert fields[3]["max_values"] == 10 - assert isinstance(fields[3]["max_values"], int) - - jsonschema.validate(instance=normalized_config, schema=schema) - - -def test_form_config_handles_float_numbers(): - form = ServiceDefinitionAdminForm() - - config_with_floats = { - "fieldsets": [ - { - "fields": [ - { - "type": "number", - "label": "Price", - "controlplane_field_mapping": "spec.price", - "min_value": "0.01", # String float - "max_value": "999.99", # String float - }, - ] - } - ] - } - - normalized_config = form._normalize_form_config_types(config_with_floats) - field = normalized_config["fieldsets"][0]["fields"][0] - - assert field["min_value"] == 0.01 - assert isinstance(field["min_value"], float) - assert field["max_value"] == 999.99 - assert isinstance(field["max_value"], float) - - -def test_form_config_handles_empty_string_as_none(): - form = ServiceDefinitionAdminForm() - - config_with_empty_strings = { - "fieldsets": [ - { - "fields": [ - { - "type": "text", - "label": "Name", - "controlplane_field_mapping": "name", - "max_length": "", # Empty string - }, - ] - } - ] - } - - normalized_config = form._normalize_form_config_types(config_with_empty_strings) - field = normalized_config["fieldsets"][0]["fields"][0] - assert field["max_length"] is None - - -def test_single_element_choices_are_normalized(): - form = ServiceDefinitionAdminForm() - mock_crd = Mock() - mock_crd.resource_schema = { - "properties": { - "spec": { - "properties": { - "version": { - "type": "string", - "enum": ["6.2", "7.0", "7.2"], - } - } - } - } - } - - config_with_single_choices = { - "fieldsets": [ - { - "fields": [ - { - "type": "text", - "label": "Name", - "controlplane_field_mapping": "name", - }, - { - "type": "choice", - "label": "Version", - "controlplane_field_mapping": "spec.version", - "choices": [["6.2"]], # Single element - should be transformed - }, - ] - } - ] - } - - spec_schema = mock_crd.resource_schema["properties"]["spec"] - errors = [] - - for field in config_with_single_choices["fieldsets"][0]["fields"]: - if field.get("type") == "choice": - form._validate_choice_field( - field, field["controlplane_field_mapping"], spec_schema, "spec", errors - ) - - assert len(errors) == 0, f"Expected no errors but got: {errors}" - version_field = config_with_single_choices["fieldsets"][0]["fields"][1] - assert version_field["choices"] == [["6.2", "6.2"]] - - -def test_two_element_choices_work_correctly(): - form = ServiceDefinitionAdminForm() - mock_crd = Mock() - mock_crd.resource_schema = { - "properties": { - "spec": { - "properties": { - "version": { - "type": "string", - "enum": ["6.2", "7.0"], - } - } - } - } - } - - config_with_proper_choices = { - "fieldsets": [ - { - "fields": [ - { - "type": "choice", - "label": "Version", - "controlplane_field_mapping": "spec.version", - "choices": [["6.2", "Version 6.2"], ["7.0", "Version 7.0"]], - }, - ] - } - ] - } - - spec_schema = mock_crd.resource_schema["properties"]["spec"] - errors = [] - - for field in config_with_proper_choices["fieldsets"][0]["fields"]: - if field.get("type") == "choice": - form._validate_choice_field( - field, field["controlplane_field_mapping"], spec_schema, "spec", errors - ) - - assert len(errors) == 0, f"Expected no errors but got: {errors}" - version_field = config_with_proper_choices["fieldsets"][0]["fields"][0] - assert version_field["choices"] == [["6.2", "Version 6.2"], ["7.0", "Version 7.0"]] - - -def test_empty_choices_fail_validation(): - form = ServiceDefinitionAdminForm() - config_with_empty_choice = { - "fieldsets": [ - { - "fields": [ - { - "type": "choice", - "label": "Version", - "controlplane_field_mapping": "spec.version", - "choices": [[]], # Empty choice - invalid - }, - ] - } - ] - } - - errors = [] - - for field in config_with_empty_choice["fieldsets"][0]["fields"]: - if field.get("type") == "choice": - form._validate_choice_field( - field, field["controlplane_field_mapping"], {}, "spec", errors - ) - - assert len(errors) > 0 - assert "must have 1 or 2 elements" in str(errors[0]) - - -def test_three_plus_element_choices_fail_validation(): - form = ServiceDefinitionAdminForm() - config_with_long_choice = { - "fieldsets": [ - { - "fields": [ - { - "type": "choice", - "label": "Version", - "controlplane_field_mapping": "spec.version", - "choices": [ - ["6.2", "Version 6.2", "Extra"] - ], # 3 elements - invalid - }, - ] - } - ] - } - - errors = [] - - for field in config_with_long_choice["fieldsets"][0]["fields"]: - if field.get("type") == "choice": - form._validate_choice_field( - field, field["controlplane_field_mapping"], {}, "spec", errors - ) - - assert len(errors) > 0 - assert "must have 1 or 2 elements" in str(errors[0]) - - -def test_field_with_default_config_only_needs_mapping(): - - class TestModel(models.Model): - name = models.CharField(max_length=100) - - class Meta: - app_label = "test" - - minimal_config = { - "fieldsets": [ - { - "fields": [ - { - "controlplane_field_mapping": "name", - }, - ] - } - ] - } - - form_class = generate_custom_form_class(minimal_config, TestModel) - form = form_class() - - name_field = form.fields["name"] - assert name_field.label == DEFAULT_FIELD_CONFIGS["name"]["label"] - assert name_field.help_text == DEFAULT_FIELD_CONFIGS["name"]["help_text"] - assert name_field.required == DEFAULT_FIELD_CONFIGS["name"]["required"] - - -def test_field_with_default_config_can_override_defaults(): - - class TestModel(models.Model): - name = models.CharField(max_length=100) - - class Meta: - app_label = "test" - - override_config = { - "fieldsets": [ - { - "fields": [ - { - "controlplane_field_mapping": "name", - "label": "Custom Name Label", - "required": False, - }, - ] - } - ] - } - - form_class = generate_custom_form_class(override_config, TestModel) - form = form_class() - - name_field = form.fields["name"] - assert name_field.label == "Custom Name Label" - assert name_field.required is False - assert name_field.help_text == DEFAULT_FIELD_CONFIGS["name"]["help_text"] - - -def test_admin_form_validates_mandatory_fields_present(): - - mock_crd = Mock() - mock_crd.resource_schema = { - "properties": { - "spec": { - "properties": { - "environment": { - "type": "string", - "enum": ["dev", "prod"], - } - } - } - } - } - - config_without_name = { - "fieldsets": [ - { - "fields": [ - { - "type": "choice", - "label": "Environment", - "controlplane_field_mapping": "spec.environment", - "choices": [["dev", "Development"]], - }, - ] - } - ] - } - - errors = [] - included_mappings = set() - for fieldset in config_without_name.get("fieldsets", []): - for field in fieldset.get("fields", []): - mapping = field.get("controlplane_field_mapping") - included_mappings.add(mapping) - - for mandatory_field in MANDATORY_FIELDS: - if mandatory_field not in included_mappings: - errors.append(f"Required field '{mandatory_field}' must be included") - - assert len(errors) > 0 - assert "name" in str(errors[0]).lower() - - -def test_admin_form_validates_fields_without_defaults_need_label_and_type(): - config_with_incomplete_field = { - "fieldsets": [ - { - "fields": [ - {"controlplane_field_mapping": "name"}, # Has defaults - OK - { - "controlplane_field_mapping": "spec.unknown", # No defaults - # Missing label and type - }, - ] - } - ] - } - - errors = [] - - for fieldset in config_with_incomplete_field.get("fieldsets", []): - for field in fieldset.get("fields", []): - mapping = field.get("controlplane_field_mapping") - - if mapping not in DEFAULT_FIELD_CONFIGS: - if not field.get("label"): - errors.append( - f"Field with mapping '{mapping}' must have a 'label' property" - ) - if not field.get("type"): - errors.append( - f"Field with mapping '{mapping}' must have a 'type' property" - ) - - assert len(errors) == 2 - assert any("label" in str(e) for e in errors) - assert any("type" in str(e) for e in errors) - - -def test_empty_values_dont_override_default_configs(): - - class TestModel(models.Model): - name = models.CharField(max_length=100) - - class Meta: - app_label = "test" - - admin_form_config = { - "fieldsets": [ - { - "fields": [ - { - "controlplane_field_mapping": "name", - "type": "", - "label": "", - "help_text": None, - "max_length": None, - "required": False, - }, - ] - } - ] - } - - form_class = generate_custom_form_class(admin_form_config, TestModel) - form = form_class() - - name_field = form.fields["name"] - - assert name_field.label == DEFAULT_FIELD_CONFIGS["name"]["label"] - assert name_field.help_text == DEFAULT_FIELD_CONFIGS["name"]["help_text"] - assert name_field.max_length == DEFAULT_FIELD_CONFIGS["name"]["max_length"] - - assert name_field.required is False # Was overridden by explicit False - - -def test_number_field_with_addon_text_roundtrip(): - class TestModel(models.Model): - name = models.CharField(max_length=100) - disk_size = models.IntegerField() - - class Meta: - app_label = "test" - - form_config = { - "fieldsets": [ - { - "fields": [ - { - "type": "text", - "label": "Name", - "controlplane_field_mapping": "name", - "required": True, - }, - { - "type": "number", - "label": "Disk Size", - "controlplane_field_mapping": "disk_size", - "addon_text": "Gi", - }, - ], - } - ] - } - - form_class = generate_custom_form_class(form_config, TestModel) - form = form_class(initial={"name": "test-instance", "disk_size": "25Gi"}) - - assert form.initial["disk_size"] == 25 - form = form_class(data={"name": "test-instance", "disk_size": "25"}) - form.fields["context"].required = False - assert form.is_valid(), f"Form should be valid but has errors: {form.errors}" - nested_data = form.get_nested_data() - assert nested_data["disk_size"] == "25Gi" diff --git a/src/tests/test_views.py b/src/tests/test_views.py index 0a39444..5ec429e 100644 --- a/src/tests/test_views.py +++ b/src/tests/test_views.py @@ -1,7 +1,5 @@ import pytest -from servala.core.models.service import CloudProvider, ServiceOffering - @pytest.mark.parametrize( "url,redirect", @@ -47,103 +45,3 @@ def test_organization_linked_in_sidebar( assert response.status_code == 200 assert organization.name in response.content.decode() assert other_organization.name not in response.content.decode() - - -@pytest.mark.django_db -def test_service_detail_redirects_with_single_offering( - client, org_owner, organization, test_service, test_service_offering -): - client.force_login(org_owner) - url = f"/org/{organization.slug}/services/{test_service.slug}/" - response = client.get(url) - - assert response.status_code == 302 - expected_url = f"/org/{organization.slug}/services/{test_service.slug}/offering/{test_service_offering.pk}/" - assert response.url == expected_url - - -@pytest.mark.django_db -def test_service_detail_shows_multiple_offerings( - client, org_owner, organization, test_service, test_service_offering -): - second_provider = CloudProvider.objects.create( - name="AWS", description="Amazon Web Services" - ) - second_offering = ServiceOffering.objects.create( - service=test_service, - provider=second_provider, - description="Redis on AWS", - osb_plan_id="test-plan-456", - ) - - client.force_login(org_owner) - url = f"/org/{organization.slug}/services/{test_service.slug}/" - response = client.get(url) - - assert response.status_code == 200 - content = response.content.decode() - - assert test_service_offering.provider.name in content - assert second_offering.provider.name in content - assert "Create Instance" in content - - -@pytest.mark.django_db -def test_service_detail_respects_cloud_provider_restrictions( - client, org_owner, organization, test_service, test_service_offering -): - second_provider = CloudProvider.objects.create( - name="AWS", description="Amazon Web Services" - ) - ServiceOffering.objects.create( - service=test_service, - provider=second_provider, - description="Redis on AWS", - osb_plan_id="test-plan-456", - ) - organization.origin.limit_cloudproviders.add(test_service_offering.provider) - - client.force_login(org_owner) - url = f"/org/{organization.slug}/services/{test_service.slug}/" - response = client.get(url) - - assert response.status_code == 302 - expected_url = f"/org/{organization.slug}/services/{test_service.slug}/offering/{test_service_offering.pk}/" - assert response.url == expected_url - - -@pytest.mark.django_db -def test_service_detail_no_redirect_with_restricted_multiple_offerings( - client, org_owner, organization, test_service, test_service_offering -): - second_provider = CloudProvider.objects.create( - name="AWS", description="Amazon Web Services" - ) - second_offering = ServiceOffering.objects.create( - service=test_service, - provider=second_provider, - description="Redis on AWS", - osb_plan_id="test-plan-456", - ) - third_provider = CloudProvider.objects.create( - name="Azure", description="Microsoft Azure" - ) - third_offering = ServiceOffering.objects.create( - service=test_service, - provider=third_provider, - description="Redis on Azure", - osb_plan_id="test-plan-789", - ) - organization.origin.limit_cloudproviders.add( - test_service_offering.provider, second_provider - ) - - client.force_login(org_owner) - url = f"/org/{organization.slug}/services/{test_service.slug}/" - response = client.get(url) - - assert response.status_code == 200 - content = response.content.decode() - assert test_service_offering.provider.name in content - assert second_offering.provider.name in content - assert third_offering.provider.name not in content diff --git a/uv.lock b/uv.lock index 16b69a3..81fa302 100644 --- a/uv.lock +++ b/uv.lock @@ -1,25 +1,57 @@ version = 1 revision = 3 -requires-python = ">=3.14.0" +requires-python = ">=3.13" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version < '3.14'", +] [[package]] name = "argon2-cffi" version = "25.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "argon2-cffi-bindings" }, + { name = "argon2-cffi-bindings", version = "21.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "argon2-cffi-bindings", version = "25.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, ] +[[package]] +name = "argon2-cffi-bindings" +version = "21.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", +] +dependencies = [ + { name = "cffi", marker = "python_full_version >= '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/e9/184b8ccce6683b0aa2fbb7ba5683ea4b9c5763f1356347f1312c32e3c66e/argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3", size = 1779911, upload-time = "2021-12-01T08:52:55.68Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/13/838ce2620025e9666aa8f686431f67a29052241692a3dd1ae9d3692a89d3/argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367", size = 29658, upload-time = "2021-12-01T09:09:17.016Z" }, + { url = "https://files.pythonhosted.org/packages/b3/02/f7f7bb6b6af6031edb11037639c697b912e1dea2db94d436e681aea2f495/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d", size = 80583, upload-time = "2021-12-01T09:09:19.546Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f7/378254e6dd7ae6f31fe40c8649eea7d4832a42243acaf0f1fff9083b2bed/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae", size = 86168, upload-time = "2021-12-01T09:09:21.445Z" }, + { url = "https://files.pythonhosted.org/packages/74/f6/4a34a37a98311ed73bb80efe422fed95f2ac25a4cacc5ae1d7ae6a144505/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c", size = 82709, upload-time = "2021-12-01T09:09:18.182Z" }, + { url = "https://files.pythonhosted.org/packages/74/2b/73d767bfdaab25484f7e7901379d5f8793cccbb86c6e0cbc4c1b96f63896/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86", size = 83613, upload-time = "2021-12-01T09:09:22.741Z" }, + { url = "https://files.pythonhosted.org/packages/4f/fd/37f86deef67ff57c76f137a67181949c2d408077e2e3dd70c6c42912c9bf/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f", size = 84583, upload-time = "2021-12-01T09:09:24.177Z" }, + { url = "https://files.pythonhosted.org/packages/6f/52/5a60085a3dae8fded8327a4f564223029f5f54b0cb0455a31131b5363a01/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e", size = 88475, upload-time = "2021-12-01T09:09:26.673Z" }, + { url = "https://files.pythonhosted.org/packages/8b/95/143cd64feb24a15fa4b189a3e1e7efbaeeb00f39a51e99b26fc62fbacabd/argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082", size = 27698, upload-time = "2021-12-01T09:09:27.87Z" }, + { url = "https://files.pythonhosted.org/packages/37/2c/e34e47c7dee97ba6f01a6203e0383e15b60fb85d78ac9a15cd066f6fe28b/argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f", size = 30817, upload-time = "2021-12-01T09:09:30.267Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e4/bf8034d25edaa495da3c8a3405627d2e35758e44ff6eaa7948092646fdcc/argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93", size = 53104, upload-time = "2021-12-01T09:09:31.335Z" }, +] + [[package]] name = "argon2-cffi-bindings" version = "25.1.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.14'", +] dependencies = [ - { name = "cffi" }, + { name = "cffi", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } wheels = [ @@ -47,25 +79,25 @@ wheels = [ [[package]] name = "asgiref" -version = "3.11.0" +version = "3.9.1" source = { registry = "https://pypi.org/simple" } -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" } +sdist = { url = "https://files.pythonhosted.org/packages/90/61/0aa957eec22ff70b830b22ff91f825e70e1ef732c06666a805730f28b36b/asgiref-3.9.1.tar.gz", hash = "sha256:a5ab6582236218e5ef1648f242fd9f10626cfd4de8dc377db215d5d5098e3142", size = 36870, upload-time = "2025-07-08T09:07:43.344Z" } wheels = [ - { 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" }, + { url = "https://files.pythonhosted.org/packages/7c/3c/0464dcada90d5da0e71018c04a140ad6349558afb30b3051b4264cc5b965/asgiref-3.9.1-py3-none-any.whl", hash = "sha256:f3bba7092a48005b5f5bacd747d36ee4a5a61f4a269a6df590b43144355ebd2c", size = 23790, upload-time = "2025-07-08T09:07:41.548Z" }, ] [[package]] name = "attrs" -version = "25.4.0" +version = "25.3.0" source = { registry = "https://pypi.org/simple" } -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" } +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" } wheels = [ - { 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" }, + { 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" }, ] [[package]] name = "black" -version = "25.11.0" +version = "25.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -75,41 +107,41 @@ dependencies = [ { name = "platformdirs" }, { name = "pytokens" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8c/ad/33adf4708633d047950ff2dfdea2e215d84ac50ef95aff14a614e4b6e9b2/black-25.11.0.tar.gz", hash = "sha256:9a323ac32f5dc75ce7470501b887250be5005a01602e931a15e45593f70f6e08", size = 655669, upload-time = "2025-11-10T01:53:50.558Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/43/20b5c90612d7bdb2bdbcceeb53d588acca3bb8f0e4c5d5c751a2c8fdd55a/black-25.9.0.tar.gz", hash = "sha256:0474bca9a0dd1b51791fcc507a4e02078a1c63f6d4e4ae5544b9848c7adfb619", size = 648393, upload-time = "2025-09-19T00:27:37.758Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/67/c0/cc865ce594d09e4cd4dfca5e11994ebb51604328489f3ca3ae7bb38a7db5/black-25.11.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:35690a383f22dd3e468c85dc4b915217f87667ad9cce781d7b42678ce63c4170", size = 1771358, upload-time = "2025-11-10T02:03:33.331Z" }, - { url = "https://files.pythonhosted.org/packages/37/77/4297114d9e2fd2fc8ab0ab87192643cd49409eb059e2940391e7d2340e57/black-25.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dae49ef7369c6caa1a1833fd5efb7c3024bb7e4499bf64833f65ad27791b1545", size = 1612902, upload-time = "2025-11-10T01:59:33.382Z" }, - { url = "https://files.pythonhosted.org/packages/de/63/d45ef97ada84111e330b2b2d45e1dd163e90bd116f00ac55927fb6bf8adb/black-25.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bd4a22a0b37401c8e492e994bce79e614f91b14d9ea911f44f36e262195fdda", size = 1680571, upload-time = "2025-11-10T01:57:04.239Z" }, - { url = "https://files.pythonhosted.org/packages/ff/4b/5604710d61cdff613584028b4cb4607e56e148801ed9b38ee7970799dab6/black-25.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:aa211411e94fdf86519996b7f5f05e71ba34835d8f0c0f03c00a26271da02664", size = 1382599, upload-time = "2025-11-10T01:57:57.427Z" }, - { url = "https://files.pythonhosted.org/packages/00/5d/aed32636ed30a6e7f9efd6ad14e2a0b0d687ae7c8c7ec4e4a557174b895c/black-25.11.0-py3-none-any.whl", hash = "sha256:e3f562da087791e96cefcd9dda058380a442ab322a02e222add53736451f604b", size = 204918, upload-time = "2025-11-10T01:53:48.917Z" }, + { url = "https://files.pythonhosted.org/packages/48/99/3acfea65f5e79f45472c45f87ec13037b506522719cd9d4ac86484ff51ac/black-25.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0172a012f725b792c358d57fe7b6b6e8e67375dd157f64fa7a3097b3ed3e2175", size = 1742165, upload-time = "2025-09-19T00:34:10.402Z" }, + { url = "https://files.pythonhosted.org/packages/3a/18/799285282c8236a79f25d590f0222dbd6850e14b060dfaa3e720241fd772/black-25.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3bec74ee60f8dfef564b573a96b8930f7b6a538e846123d5ad77ba14a8d7a64f", size = 1581259, upload-time = "2025-09-19T00:32:49.685Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ce/883ec4b6303acdeca93ee06b7622f1fa383c6b3765294824165d49b1a86b/black-25.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b756fc75871cb1bcac5499552d771822fd9db5a2bb8db2a7247936ca48f39831", size = 1655583, upload-time = "2025-09-19T00:30:44.505Z" }, + { url = "https://files.pythonhosted.org/packages/21/17/5c253aa80a0639ccc427a5c7144534b661505ae2b5a10b77ebe13fa25334/black-25.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:846d58e3ce7879ec1ffe816bb9df6d006cd9590515ed5d17db14e17666b2b357", size = 1343428, upload-time = "2025-09-19T00:32:13.839Z" }, + { url = "https://files.pythonhosted.org/packages/1b/46/863c90dcd3f9d41b109b7f19032ae0db021f0b2a81482ba0a1e28c84de86/black-25.9.0-py3-none-any.whl", hash = "sha256:474b34c1342cdc157d307b56c4c65bce916480c4a8f6551fdc6bf9b486a7c4ae", size = 203363, upload-time = "2025-09-19T00:27:35.724Z" }, ] [[package]] name = "boto3" -version = "1.42.0" +version = "1.40.19" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f0/9b/eef5346ce3148bf4856318fe629e0fd7f6dd73ffd55ea08e316c967f8af0/boto3-1.42.0.tar.gz", hash = "sha256:9c67729a6112b7dced521ea70b0369fba138e89852b029a7876041cd1460c084", size = 112854, upload-time = "2025-12-01T02:31:09.157Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/d6/f67e90c53f499a12353e6f19104fc55f9fc9ec514207dbe08e1e1de9a45b/boto3-1.40.19.tar.gz", hash = "sha256:772f259fdef6efa752c5744e140c0371593a20a0c728cce91d67b8b58d1090e7", size = 111524, upload-time = "2025-08-27T19:19:38.453Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/2c/6c6ee5667426aee6629106b9e51668449fb34ec077655da82bf4b15d8890/boto3-1.42.0-py3-none-any.whl", hash = "sha256:af32b7f61dd6293cad728ec205bcb3611ab1bf7b7dbccfd0f2bd7b9c9af96039", size = 140617, upload-time = "2025-12-01T02:31:07.238Z" }, + { url = "https://files.pythonhosted.org/packages/d3/15/3886b46973d10814f0aa89e94e03182f233fd478b2be317e706d9f0e85bd/boto3-1.40.19-py3-none-any.whl", hash = "sha256:9cdf01576fae6cb12b71fd6b793f34876feafa962cdaf3a9489253580355fc60", size = 139324, upload-time = "2025-08-27T19:19:36.693Z" }, ] [[package]] name = "botocore" -version = "1.41.6" +version = "1.40.19" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/03/04/8e8ca38631eeb499a1099dcc2a081faaea399f9d46080720540ff54ec609/botocore-1.41.6.tar.gz", hash = "sha256:08fe47e9b306f4436f5eaf6a02cb6d55c7745d13d2d093ce5d917d3ef3d3df75", size = 14770281, upload-time = "2025-12-01T02:30:54.286Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/8c/8e319b9936fea23a3be19fac2921dd91cc60a99c0cf771d7d676e3329bb3/botocore-1.40.19.tar.gz", hash = "sha256:becc101b3047ec4cffa6c86bab747b8312db20529ee0132fe77007092a9c9f85", size = 14320063, upload-time = "2025-08-27T19:19:27.94Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/d4/587a71c599997b0f7aa842ea71604348f5a7d239cfff338292904f236983/botocore-1.41.6-py3-none-any.whl", hash = "sha256:963cc946e885acb941c96e7d343cb6507b479812ca22566ceb3e9410d0588de0", size = 14442076, upload-time = "2025-12-01T02:30:50.724Z" }, + { url = "https://files.pythonhosted.org/packages/20/11/a633975158d79bb361ebaa802c393f5408b4cf95226ba3abe573f29741fb/botocore-1.40.19-py3-none-any.whl", hash = "sha256:6a7c2ceaf8ed3321cf4bc15420dad4e778263d3b480c86f7fd9da982e1deaa64", size = 13985499, upload-time = "2025-08-27T19:19:22.249Z" }, ] [[package]] @@ -129,20 +161,20 @@ wheels = [ [[package]] name = "cachetools" -version = "6.2.2" +version = "5.5.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fb/44/ca1675be2a83aeee1886ab745b28cda92093066590233cc501890eb8417a/cachetools-6.2.2.tar.gz", hash = "sha256:8e6d266b25e539df852251cfd6f990b4bc3a141db73b939058d809ebd2590fc6", size = 31571, upload-time = "2025-11-13T17:42:51.465Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380, upload-time = "2025-02-20T21:01:19.524Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/46/eb6eca305c77a4489affe1c5d8f4cae82f285d9addd8de4ec084a7184221/cachetools-6.2.2-py3-none-any.whl", hash = "sha256:6c09c98183bf58560c97b2abfcedcbaf6a896a490f534b031b661d3723b45ace", size = 11503, upload-time = "2025-11-13T17:42:50.232Z" }, + { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" }, ] [[package]] name = "certifi" -version = "2025.11.12" +version = "2025.8.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, + { 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" }, ] [[package]] @@ -154,6 +186,18 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, @@ -180,39 +224,45 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.4" +version = "3.4.3" source = { registry = "https://pypi.org/simple" } -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" } +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" } wheels = [ - { 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/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, - { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, - { 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" }, + { 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/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, + { 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/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, + { 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" }, ] [[package]] name = "click" -version = "8.3.1" +version = "8.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -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" } +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" } wheels = [ - { 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" }, + { 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" }, ] [[package]] @@ -226,93 +276,119 @@ wheels = [ [[package]] name = "coverage" -version = "7.12.0" +version = "7.10.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/89/26/4a96807b193b011588099c3b5c89fbb05294e5b90e71018e065465f34eb6/coverage-7.12.0.tar.gz", hash = "sha256:fc11e0a4e372cb5f282f16ef90d4a585034050ccda536451901abfb19a57f40c", size = 819341, upload-time = "2025-11-18T13:34:20.766Z" } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/2e/fc12db0883478d6e12bbd62d481210f0c8daf036102aa11434a0c5755825/coverage-7.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a1c59b7dc169809a88b21a936eccf71c3895a78f5592051b1af8f4d59c2b4f92", size = 217777, upload-time = "2025-11-18T13:33:32.86Z" }, - { url = "https://files.pythonhosted.org/packages/1f/c1/ce3e525d223350c6ec16b9be8a057623f54226ef7f4c2fee361ebb6a02b8/coverage-7.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8787b0f982e020adb732b9f051f3e49dd5054cebbc3f3432061278512a2b1360", size = 218100, upload-time = "2025-11-18T13:33:34.532Z" }, - { url = "https://files.pythonhosted.org/packages/15/87/113757441504aee3808cb422990ed7c8bcc2d53a6779c66c5adef0942939/coverage-7.12.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ea5a9f7dc8877455b13dd1effd3202e0bca72f6f3ab09f9036b1bcf728f69ac", size = 249151, upload-time = "2025-11-18T13:33:36.135Z" }, - { url = "https://files.pythonhosted.org/packages/d9/1d/9529d9bd44049b6b05bb319c03a3a7e4b0a8a802d28fa348ad407e10706d/coverage-7.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fdba9f15849534594f60b47c9a30bc70409b54947319a7c4fd0e8e3d8d2f355d", size = 251667, upload-time = "2025-11-18T13:33:37.996Z" }, - { url = "https://files.pythonhosted.org/packages/11/bb/567e751c41e9c03dc29d3ce74b8c89a1e3396313e34f255a2a2e8b9ebb56/coverage-7.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a00594770eb715854fb1c57e0dea08cce6720cfbc531accdb9850d7c7770396c", size = 253003, upload-time = "2025-11-18T13:33:39.553Z" }, - { url = "https://files.pythonhosted.org/packages/e4/b3/c2cce2d8526a02fb9e9ca14a263ca6fc074449b33a6afa4892838c903528/coverage-7.12.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5560c7e0d82b42eb1951e4f68f071f8017c824ebfd5a6ebe42c60ac16c6c2434", size = 249185, upload-time = "2025-11-18T13:33:42.086Z" }, - { url = "https://files.pythonhosted.org/packages/0e/a7/967f93bb66e82c9113c66a8d0b65ecf72fc865adfba5a145f50c7af7e58d/coverage-7.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2e26b481c9159c2773a37947a9718cfdc58893029cdfb177531793e375cfc", size = 251025, upload-time = "2025-11-18T13:33:43.634Z" }, - { url = "https://files.pythonhosted.org/packages/b9/b2/f2f6f56337bc1af465d5b2dc1ee7ee2141b8b9272f3bf6213fcbc309a836/coverage-7.12.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6e1a8c066dabcde56d5d9fed6a66bc19a2883a3fe051f0c397a41fc42aedd4cc", size = 248979, upload-time = "2025-11-18T13:33:46.04Z" }, - { url = "https://files.pythonhosted.org/packages/f4/7a/bf4209f45a4aec09d10a01a57313a46c0e0e8f4c55ff2965467d41a92036/coverage-7.12.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f7ba9da4726e446d8dd8aae5a6cd872511184a5d861de80a86ef970b5dacce3e", size = 248800, upload-time = "2025-11-18T13:33:47.546Z" }, - { url = "https://files.pythonhosted.org/packages/b8/b7/1e01b8696fb0521810f60c5bbebf699100d6754183e6cc0679bf2ed76531/coverage-7.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e0f483ab4f749039894abaf80c2f9e7ed77bbf3c737517fb88c8e8e305896a17", size = 250460, upload-time = "2025-11-18T13:33:49.537Z" }, - { url = "https://files.pythonhosted.org/packages/71/ae/84324fb9cb46c024760e706353d9b771a81b398d117d8c1fe010391c186f/coverage-7.12.0-cp314-cp314-win32.whl", hash = "sha256:76336c19a9ef4a94b2f8dc79f8ac2da3f193f625bb5d6f51a328cd19bfc19933", size = 220533, upload-time = "2025-11-18T13:33:51.16Z" }, - { url = "https://files.pythonhosted.org/packages/e2/71/1033629deb8460a8f97f83e6ac4ca3b93952e2b6f826056684df8275e015/coverage-7.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7c1059b600aec6ef090721f8f633f60ed70afaffe8ecab85b59df748f24b31fe", size = 221348, upload-time = "2025-11-18T13:33:52.776Z" }, - { url = "https://files.pythonhosted.org/packages/0a/5f/ac8107a902f623b0c251abdb749be282dc2ab61854a8a4fcf49e276fce2f/coverage-7.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:172cf3a34bfef42611963e2b661302a8931f44df31629e5b1050567d6b90287d", size = 219922, upload-time = "2025-11-18T13:33:54.316Z" }, - { url = "https://files.pythonhosted.org/packages/79/6e/f27af2d4da367f16077d21ef6fe796c874408219fa6dd3f3efe7751bd910/coverage-7.12.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:aa7d48520a32cb21c7a9b31f81799e8eaec7239db36c3b670be0fa2403828d1d", size = 218511, upload-time = "2025-11-18T13:33:56.343Z" }, - { url = "https://files.pythonhosted.org/packages/67/dd/65fd874aa460c30da78f9d259400d8e6a4ef457d61ab052fd248f0050558/coverage-7.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:90d58ac63bc85e0fb919f14d09d6caa63f35a5512a2205284b7816cafd21bb03", size = 218771, upload-time = "2025-11-18T13:33:57.966Z" }, - { url = "https://files.pythonhosted.org/packages/55/e0/7c6b71d327d8068cb79c05f8f45bf1b6145f7a0de23bbebe63578fe5240a/coverage-7.12.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca8ecfa283764fdda3eae1bdb6afe58bf78c2c3ec2b2edcb05a671f0bba7b3f9", size = 260151, upload-time = "2025-11-18T13:33:59.597Z" }, - { url = "https://files.pythonhosted.org/packages/49/ce/4697457d58285b7200de6b46d606ea71066c6e674571a946a6ea908fb588/coverage-7.12.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:874fe69a0785d96bd066059cd4368022cebbec1a8958f224f0016979183916e6", size = 262257, upload-time = "2025-11-18T13:34:01.166Z" }, - { url = "https://files.pythonhosted.org/packages/2f/33/acbc6e447aee4ceba88c15528dbe04a35fb4d67b59d393d2e0d6f1e242c1/coverage-7.12.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b3c889c0b8b283a24d721a9eabc8ccafcfc3aebf167e4cd0d0e23bf8ec4e339", size = 264671, upload-time = "2025-11-18T13:34:02.795Z" }, - { url = "https://files.pythonhosted.org/packages/87/ec/e2822a795c1ed44d569980097be839c5e734d4c0c1119ef8e0a073496a30/coverage-7.12.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8bb5b894b3ec09dcd6d3743229dc7f2c42ef7787dc40596ae04c0edda487371e", size = 259231, upload-time = "2025-11-18T13:34:04.397Z" }, - { url = "https://files.pythonhosted.org/packages/72/c5/a7ec5395bb4a49c9b7ad97e63f0c92f6bf4a9e006b1393555a02dae75f16/coverage-7.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:79a44421cd5fba96aa57b5e3b5a4d3274c449d4c622e8f76882d76635501fd13", size = 262137, upload-time = "2025-11-18T13:34:06.068Z" }, - { url = "https://files.pythonhosted.org/packages/67/0c/02c08858b764129f4ecb8e316684272972e60777ae986f3865b10940bdd6/coverage-7.12.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:33baadc0efd5c7294f436a632566ccc1f72c867f82833eb59820ee37dc811c6f", size = 259745, upload-time = "2025-11-18T13:34:08.04Z" }, - { url = "https://files.pythonhosted.org/packages/5a/04/4fd32b7084505f3829a8fe45c1a74a7a728cb251aaadbe3bec04abcef06d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c406a71f544800ef7e9e0000af706b88465f3573ae8b8de37e5f96c59f689ad1", size = 258570, upload-time = "2025-11-18T13:34:09.676Z" }, - { url = "https://files.pythonhosted.org/packages/48/35/2365e37c90df4f5342c4fa202223744119fe31264ee2924f09f074ea9b6d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e71bba6a40883b00c6d571599b4627f50c360b3d0d02bfc658168936be74027b", size = 260899, upload-time = "2025-11-18T13:34:11.259Z" }, - { url = "https://files.pythonhosted.org/packages/05/56/26ab0464ca733fa325e8e71455c58c1c374ce30f7c04cebb88eabb037b18/coverage-7.12.0-cp314-cp314t-win32.whl", hash = "sha256:9157a5e233c40ce6613dead4c131a006adfda70e557b6856b97aceed01b0e27a", size = 221313, upload-time = "2025-11-18T13:34:12.863Z" }, - { url = "https://files.pythonhosted.org/packages/da/1c/017a3e1113ed34d998b27d2c6dba08a9e7cb97d362f0ec988fcd873dcf81/coverage-7.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e84da3a0fd233aeec797b981c51af1cabac74f9bd67be42458365b30d11b5291", size = 222423, upload-time = "2025-11-18T13:34:15.14Z" }, - { url = "https://files.pythonhosted.org/packages/4c/36/bcc504fdd5169301b52568802bb1b9cdde2e27a01d39fbb3b4b508ab7c2c/coverage-7.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:01d24af36fedda51c2b1aca56e4330a3710f83b02a5ff3743a6b015ffa7c9384", size = 220459, upload-time = "2025-11-18T13:34:17.222Z" }, - { url = "https://files.pythonhosted.org/packages/ce/a3/43b749004e3c09452e39bb56347a008f0a0668aad37324a99b5c8ca91d9e/coverage-7.12.0-py3-none-any.whl", hash = "sha256:159d50c0b12e060b15ed3d39f87ed43d4f7f7ad40b8a534f4dd331adbb51104a", size = 209503, upload-time = "2025-11-18T13:34:18.892Z" }, + { 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/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, + { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, + { 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/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, + { 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/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, + { 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/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, + { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, + { 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" }, ] [[package]] name = "cryptography" -version = "46.0.3" +version = "46.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -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" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/62/e3664e6ffd7743e1694b244dde70b43a394f6f7fbcacf7014a8ff5197c73/cryptography-46.0.1.tar.gz", hash = "sha256:ed570874e88f213437f5cf758f9ef26cbfc3f336d889b1e592ee11283bb8d1c7", size = 749198, upload-time = "2025-09-17T00:10:35.797Z" } wheels = [ - { 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/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, - { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, - { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, - { 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/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, - { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, - { 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/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, - { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, - { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, + { url = "https://files.pythonhosted.org/packages/4c/8c/44ee01267ec01e26e43ebfdae3f120ec2312aa72fa4c0507ebe41a26739f/cryptography-46.0.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:1cd6d50c1a8b79af1a6f703709d8973845f677c8e97b1268f5ff323d38ce8475", size = 7285044, upload-time = "2025-09-17T00:08:36.807Z" }, + { url = "https://files.pythonhosted.org/packages/22/59/9ae689a25047e0601adfcb159ec4f83c0b4149fdb5c3030cc94cd218141d/cryptography-46.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0ff483716be32690c14636e54a1f6e2e1b7bf8e22ca50b989f88fa1b2d287080", size = 4308182, upload-time = "2025-09-17T00:08:39.388Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/ca6cc9df7118f2fcd142c76b1da0f14340d77518c05b1ebfbbabca6b9e7d/cryptography-46.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9873bf7c1f2a6330bdfe8621e7ce64b725784f9f0c3a6a55c3047af5849f920e", size = 4572393, upload-time = "2025-09-17T00:08:41.663Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a3/0f5296f63815d8e985922b05c31f77ce44787b3127a67c0b7f70f115c45f/cryptography-46.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0dfb7c88d4462a0cfdd0d87a3c245a7bc3feb59de101f6ff88194f740f72eda6", size = 4308400, upload-time = "2025-09-17T00:08:43.559Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8c/74fcda3e4e01be1d32775d5b4dd841acaac3c1b8fa4d0774c7ac8d52463d/cryptography-46.0.1-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e22801b61613ebdebf7deb18b507919e107547a1d39a3b57f5f855032dd7cfb8", size = 4015786, upload-time = "2025-09-17T00:08:45.758Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b8/85d23287baeef273b0834481a3dd55bbed3a53587e3b8d9f0898235b8f91/cryptography-46.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:757af4f6341ce7a1e47c326ca2a81f41d236070217e5fbbad61bbfe299d55d28", size = 4982606, upload-time = "2025-09-17T00:08:47.602Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d3/de61ad5b52433b389afca0bc70f02a7a1f074651221f599ce368da0fe437/cryptography-46.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f7a24ea78de345cfa7f6a8d3bde8b242c7fac27f2bd78fa23474ca38dfaeeab9", size = 4604234, upload-time = "2025-09-17T00:08:49.879Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1f/dbd4d6570d84748439237a7478d124ee0134bf166ad129267b7ed8ea6d22/cryptography-46.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e8776dac9e660c22241b6587fae51a67b4b0147daa4d176b172c3ff768ad736", size = 4307669, upload-time = "2025-09-17T00:08:52.321Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fd/ca0a14ce7f0bfe92fa727aacaf2217eb25eb7e4ed513b14d8e03b26e63ed/cryptography-46.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9f40642a140c0c8649987027867242b801486865277cbabc8c6059ddef16dc8b", size = 4947579, upload-time = "2025-09-17T00:08:54.697Z" }, + { url = "https://files.pythonhosted.org/packages/89/6b/09c30543bb93401f6f88fce556b3bdbb21e55ae14912c04b7bf355f5f96c/cryptography-46.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:449ef2b321bec7d97ef2c944173275ebdab78f3abdd005400cc409e27cd159ab", size = 4603669, upload-time = "2025-09-17T00:08:57.16Z" }, + { url = "https://files.pythonhosted.org/packages/23/9a/38cb01cb09ce0adceda9fc627c9cf98eb890fc8d50cacbe79b011df20f8a/cryptography-46.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2dd339ba3345b908fa3141ddba4025568fa6fd398eabce3ef72a29ac2d73ad75", size = 4435828, upload-time = "2025-09-17T00:08:59.606Z" }, + { url = "https://files.pythonhosted.org/packages/0f/53/435b5c36a78d06ae0bef96d666209b0ecd8f8181bfe4dda46536705df59e/cryptography-46.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7411c910fb2a412053cf33cfad0153ee20d27e256c6c3f14d7d7d1d9fec59fd5", size = 4709553, upload-time = "2025-09-17T00:09:01.832Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c4/0da6e55595d9b9cd3b6eb5dc22f3a07ded7f116a3ea72629cab595abb804/cryptography-46.0.1-cp311-abi3-win32.whl", hash = "sha256:cbb8e769d4cac884bb28e3ff620ef1001b75588a5c83c9c9f1fdc9afbe7f29b0", size = 3058327, upload-time = "2025-09-17T00:09:03.726Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/cd29a35e0d6e78a0ee61793564c8cff0929c38391cb0de27627bdc7525aa/cryptography-46.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:92e8cfe8bd7dd86eac0a677499894862cd5cc2fd74de917daa881d00871ac8e7", size = 3523893, upload-time = "2025-09-17T00:09:06.272Z" }, + { url = "https://files.pythonhosted.org/packages/f2/dd/eea390f3e78432bc3d2f53952375f8b37cb4d37783e626faa6a51e751719/cryptography-46.0.1-cp311-abi3-win_arm64.whl", hash = "sha256:db5597a4c7353b2e5fb05a8e6cb74b56a4658a2b7bf3cb6b1821ae7e7fd6eaa0", size = 2932145, upload-time = "2025-09-17T00:09:08.568Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fb/c73588561afcd5e24b089952bd210b14676c0c5bf1213376350ae111945c/cryptography-46.0.1-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:4c49eda9a23019e11d32a0eb51a27b3e7ddedde91e099c0ac6373e3aacc0d2ee", size = 7193928, upload-time = "2025-09-17T00:09:10.595Z" }, + { url = "https://files.pythonhosted.org/packages/26/34/0ff0bb2d2c79f25a2a63109f3b76b9108a906dd2a2eb5c1d460b9938adbb/cryptography-46.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9babb7818fdd71394e576cf26c5452df77a355eac1a27ddfa24096665a27f8fd", size = 4293515, upload-time = "2025-09-17T00:09:12.861Z" }, + { url = "https://files.pythonhosted.org/packages/df/b7/d4f848aee24ecd1be01db6c42c4a270069a4f02a105d9c57e143daf6cf0f/cryptography-46.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9f2c4cc63be3ef43c0221861177cee5d14b505cd4d4599a89e2cd273c4d3542a", size = 4545619, upload-time = "2025-09-17T00:09:15.397Z" }, + { url = "https://files.pythonhosted.org/packages/44/a5/42fedefc754fd1901e2d95a69815ea4ec8a9eed31f4c4361fcab80288661/cryptography-46.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:41c281a74df173876da1dc9a9b6953d387f06e3d3ed9284e3baae3ab3f40883a", size = 4299160, upload-time = "2025-09-17T00:09:17.155Z" }, + { url = "https://files.pythonhosted.org/packages/86/a1/cd21174f56e769c831fbbd6399a1b7519b0ff6280acec1b826d7b072640c/cryptography-46.0.1-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0a17377fa52563d730248ba1f68185461fff36e8bc75d8787a7dd2e20a802b7a", size = 3994491, upload-time = "2025-09-17T00:09:18.971Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2f/a8cbfa1c029987ddc746fd966711d4fa71efc891d37fbe9f030fe5ab4eec/cryptography-46.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:0d1922d9280e08cde90b518a10cd66831f632960a8d08cb3418922d83fce6f12", size = 4960157, upload-time = "2025-09-17T00:09:20.923Z" }, + { url = "https://files.pythonhosted.org/packages/67/ae/63a84e6789e0d5a2502edf06b552bcb0fa9ff16147265d5c44a211942abe/cryptography-46.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:af84e8e99f1a82cea149e253014ea9dc89f75b82c87bb6c7242203186f465129", size = 4577263, upload-time = "2025-09-17T00:09:23.356Z" }, + { url = "https://files.pythonhosted.org/packages/ef/8f/1b9fa8e92bd9cbcb3b7e1e593a5232f2c1e6f9bd72b919c1a6b37d315f92/cryptography-46.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ef648d2c690703501714588b2ba640facd50fd16548133b11b2859e8655a69da", size = 4298703, upload-time = "2025-09-17T00:09:25.566Z" }, + { url = "https://files.pythonhosted.org/packages/c3/af/bb95db070e73fea3fae31d8a69ac1463d89d1c084220f549b00dd01094a8/cryptography-46.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:e94eb5fa32a8a9f9bf991f424f002913e3dd7c699ef552db9b14ba6a76a6313b", size = 4926363, upload-time = "2025-09-17T00:09:27.451Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3b/d8fb17ffeb3a83157a1cc0aa5c60691d062aceecba09c2e5e77ebfc1870c/cryptography-46.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:534b96c0831855e29fc3b069b085fd185aa5353033631a585d5cd4dd5d40d657", size = 4576958, upload-time = "2025-09-17T00:09:29.924Z" }, + { url = "https://files.pythonhosted.org/packages/d9/46/86bc3a05c10c8aa88c8ae7e953a8b4e407c57823ed201dbcba55c4d655f4/cryptography-46.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9b55038b5c6c47559aa33626d8ecd092f354e23de3c6975e4bb205df128a2a0", size = 4422507, upload-time = "2025-09-17T00:09:32.222Z" }, + { url = "https://files.pythonhosted.org/packages/a8/4e/387e5a21dfd2b4198e74968a541cfd6128f66f8ec94ed971776e15091ac3/cryptography-46.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ec13b7105117dbc9afd023300fb9954d72ca855c274fe563e72428ece10191c0", size = 4683964, upload-time = "2025-09-17T00:09:34.118Z" }, + { url = "https://files.pythonhosted.org/packages/25/a3/f9f5907b166adb8f26762071474b38bbfcf89858a5282f032899075a38a1/cryptography-46.0.1-cp314-cp314t-win32.whl", hash = "sha256:504e464944f2c003a0785b81668fe23c06f3b037e9cb9f68a7c672246319f277", size = 3029705, upload-time = "2025-09-17T00:09:36.381Z" }, + { url = "https://files.pythonhosted.org/packages/12/66/4d3a4f1850db2e71c2b1628d14b70b5e4c1684a1bd462f7fffb93c041c38/cryptography-46.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c52fded6383f7e20eaf70a60aeddd796b3677c3ad2922c801be330db62778e05", size = 3502175, upload-time = "2025-09-17T00:09:38.261Z" }, + { url = "https://files.pythonhosted.org/packages/52/c7/9f10ad91435ef7d0d99a0b93c4360bea3df18050ff5b9038c489c31ac2f5/cryptography-46.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:9495d78f52c804b5ec8878b5b8c7873aa8e63db9cd9ee387ff2db3fffe4df784", size = 2912354, upload-time = "2025-09-17T00:09:40.078Z" }, + { url = "https://files.pythonhosted.org/packages/98/e5/fbd632385542a3311915976f88e0dfcf09e62a3fc0aff86fb6762162a24d/cryptography-46.0.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:d84c40bdb8674c29fa192373498b6cb1e84f882889d21a471b45d1f868d8d44b", size = 7255677, upload-time = "2025-09-17T00:09:42.407Z" }, + { url = "https://files.pythonhosted.org/packages/56/3e/13ce6eab9ad6eba1b15a7bd476f005a4c1b3f299f4c2f32b22408b0edccf/cryptography-46.0.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9ed64e5083fa806709e74fc5ea067dfef9090e5b7a2320a49be3c9df3583a2d8", size = 4301110, upload-time = "2025-09-17T00:09:45.614Z" }, + { url = "https://files.pythonhosted.org/packages/a2/67/65dc233c1ddd688073cf7b136b06ff4b84bf517ba5529607c9d79720fc67/cryptography-46.0.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:341fb7a26bc9d6093c1b124b9f13acc283d2d51da440b98b55ab3f79f2522ead", size = 4562369, upload-time = "2025-09-17T00:09:47.601Z" }, + { url = "https://files.pythonhosted.org/packages/17/db/d64ae4c6f4e98c3dac5bf35dd4d103f4c7c345703e43560113e5e8e31b2b/cryptography-46.0.1-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6ef1488967e729948d424d09c94753d0167ce59afba8d0f6c07a22b629c557b2", size = 4302126, upload-time = "2025-09-17T00:09:49.335Z" }, + { url = "https://files.pythonhosted.org/packages/3d/19/5f1eea17d4805ebdc2e685b7b02800c4f63f3dd46cfa8d4c18373fea46c8/cryptography-46.0.1-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7823bc7cdf0b747ecfb096d004cc41573c2f5c7e3a29861603a2871b43d3ef32", size = 4009431, upload-time = "2025-09-17T00:09:51.239Z" }, + { url = "https://files.pythonhosted.org/packages/81/b5/229ba6088fe7abccbfe4c5edb96c7a5ad547fac5fdd0d40aa6ea540b2985/cryptography-46.0.1-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:f736ab8036796f5a119ff8211deda416f8c15ce03776db704a7a4e17381cb2ef", size = 4980739, upload-time = "2025-09-17T00:09:54.181Z" }, + { url = "https://files.pythonhosted.org/packages/3a/9c/50aa38907b201e74bc43c572f9603fa82b58e831bd13c245613a23cff736/cryptography-46.0.1-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:e46710a240a41d594953012213ea8ca398cd2448fbc5d0f1be8160b5511104a0", size = 4592289, upload-time = "2025-09-17T00:09:56.731Z" }, + { url = "https://files.pythonhosted.org/packages/5a/33/229858f8a5bb22f82468bb285e9f4c44a31978d5f5830bb4ea1cf8a4e454/cryptography-46.0.1-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:84ef1f145de5aee82ea2447224dc23f065ff4cc5791bb3b506615957a6ba8128", size = 4301815, upload-time = "2025-09-17T00:09:58.548Z" }, + { url = "https://files.pythonhosted.org/packages/52/cb/b76b2c87fbd6ed4a231884bea3ce073406ba8e2dae9defad910d33cbf408/cryptography-46.0.1-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9394c7d5a7565ac5f7d9ba38b2617448eba384d7b107b262d63890079fad77ca", size = 4943251, upload-time = "2025-09-17T00:10:00.475Z" }, + { url = "https://files.pythonhosted.org/packages/94/0f/f66125ecf88e4cb5b8017ff43f3a87ede2d064cb54a1c5893f9da9d65093/cryptography-46.0.1-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ed957044e368ed295257ae3d212b95456bd9756df490e1ac4538857f67531fcc", size = 4591247, upload-time = "2025-09-17T00:10:02.874Z" }, + { url = "https://files.pythonhosted.org/packages/f6/22/9f3134ae436b63b463cfdf0ff506a0570da6873adb4bf8c19b8a5b4bac64/cryptography-46.0.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f7de12fa0eee6234de9a9ce0ffcfa6ce97361db7a50b09b65c63ac58e5f22fc7", size = 4428534, upload-time = "2025-09-17T00:10:04.994Z" }, + { url = "https://files.pythonhosted.org/packages/89/39/e6042bcb2638650b0005c752c38ea830cbfbcbb1830e4d64d530000aa8dc/cryptography-46.0.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7fab1187b6c6b2f11a326f33b036f7168f5b996aedd0c059f9738915e4e8f53a", size = 4699541, upload-time = "2025-09-17T00:10:06.925Z" }, + { url = "https://files.pythonhosted.org/packages/68/46/753d457492d15458c7b5a653fc9a84a1c9c7a83af6ebdc94c3fc373ca6e8/cryptography-46.0.1-cp38-abi3-win32.whl", hash = "sha256:45f790934ac1018adeba46a0f7289b2b8fe76ba774a88c7f1922213a56c98bc1", size = 3043779, upload-time = "2025-09-17T00:10:08.951Z" }, + { url = "https://files.pythonhosted.org/packages/2f/50/b6f3b540c2f6ee712feeb5fa780bb11fad76634e71334718568e7695cb55/cryptography-46.0.1-cp38-abi3-win_amd64.whl", hash = "sha256:7176a5ab56fac98d706921f6416a05e5aff7df0e4b91516f450f8627cda22af3", size = 3517226, upload-time = "2025-09-17T00:10:10.769Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e8/77d17d00981cdd27cc493e81e1749a0b8bbfb843780dbd841e30d7f50743/cryptography-46.0.1-cp38-abi3-win_arm64.whl", hash = "sha256:efc9e51c3e595267ff84adf56e9b357db89ab2279d7e375ffcaf8f678606f3d9", size = 2923149, upload-time = "2025-09-17T00:10:13.236Z" }, ] [[package]] @@ -331,55 +407,52 @@ wheels = [ [[package]] name = "django" -version = "5.2.8" +version = "5.2.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref" }, { name = "sqlparse" }, { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/05/a2/933dbbb3dd9990494960f6e64aca2af4c0745b63b7113f59a822df92329e/django-5.2.8.tar.gz", hash = "sha256:23254866a5bb9a2cfa6004e8b809ec6246eba4b58a7589bc2772f1bcc8456c7f", size = 10849032, upload-time = "2025-11-05T14:07:32.778Z" } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/3d/a035a4ee9b1d4d4beee2ae6e8e12fe6dee5514b21f62504e22efcbd9fb46/django-5.2.8-py3-none-any.whl", hash = "sha256:37e687f7bd73ddf043e2b6b97cfe02fcbb11f2dbb3adccc6a2b18c6daa054d7f", size = 8289692, upload-time = "2025-11-05T14:07:28.761Z" }, + { 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" }, ] [[package]] name = "django-allauth" -version = "65.13.1" +version = "65.11.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref" }, { name = "django" }, ] -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" }, -] +sdist = { url = "https://files.pythonhosted.org/packages/b7/19/3671e67b5fcc744c0d9380b0bb6120b7226bc9944bd9affb029b2d510d53/django_allauth-65.11.2.tar.gz", hash = "sha256:7b7e771d3384d0e247d0d6aef31b0cb589f92305b7e975e70056a513525906e7", size = 1916225, upload-time = "2025-09-09T18:37:19.55Z" } [[package]] name = "django-auditlog" -version = "3.3.0" +version = "3.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django" }, { name = "python-dateutil" }, ] -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/e1/46/9da1d94493832fa18d2f6324a76d387fa232001593866987a96047709f4e/django_auditlog-3.2.1.tar.gz", hash = "sha256:63a4c9f7793e94eed804bc31a04d9b0b58244b1d280e2ed273c8b406bff1f779", size = 72926, upload-time = "2025-07-03T20:08:17.734Z" } 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/a7/06/67296d050a72dcd76f57f220df621cb27e5b9282ba7ad0f5f74870dce241/django_auditlog-3.2.1-py3-none-any.whl", hash = "sha256:99603ca9d015f7e9b062b1c34f3e0826a3ce6ae6e5950c81bb7e663f7802a899", size = 38330, upload-time = "2025-07-03T20:07:51.735Z" }, ] [[package]] name = "django-fernet-encrypted-fields" -version = "0.3.1" +version = "0.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "django" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1a/aa/529af3888215b8a660fc3897d6d63eaf1de9aa0699c633ca0ec483d4361c/django_fernet_encrypted_fields-0.3.1.tar.gz", hash = "sha256:5ed328c7f9cc7f2d452bb2e125f3ea2bea3563a259fa943e5a1c626175889a71", size = 5265, upload-time = "2025-11-10T08:39:57.398Z" } +sdist = { url = "https://files.pythonhosted.org/packages/70/b8/b6725f1207693ba9e76223abf87eb9e8de5114cccad8ddd1bce29a195273/django-fernet-encrypted-fields-0.3.0.tar.gz", hash = "sha256:38031bdaf1724a6e885ee137cc66a2bd7dc3726c438e189ea7e44799ec0ba9b3", size = 4021, upload-time = "2025-02-21T02:58:42.049Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/52/7f/4e0b7ed8413fa58e7a77017342e8ab0e977d41cfc376ab9180ae75f216ec/django_fernet_encrypted_fields-0.3.1-py3-none-any.whl", hash = "sha256:3bd2abab02556dc6e15a58a61161ee6c5cdf45a50a8a52d9e035009eb54c6442", size = 5484, upload-time = "2025-11-10T08:39:55.866Z" }, + { url = "https://files.pythonhosted.org/packages/75/8a/2c5d88cd540d83ceaa1cb3191ed35dfed0caacc6fe2ff5fe74c9ecc7776f/django_fernet_encrypted_fields-0.3.0-py3-none-any.whl", hash = "sha256:a17cca5bf3638ee44674e64f30792d5960b1d4d4b291ec478c27515fc4860612", size = 5400, upload-time = "2025-02-21T02:58:40.832Z" }, ] [[package]] @@ -425,14 +498,14 @@ s3 = [ [[package]] name = "django-template-partials" -version = "25.3" +version = "25.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b0/2e/957ee4a6ee0a7d46a18676ba8b01d762ef89d00b7769cc532853f9f989e1/django_template_partials-25.3.tar.gz", hash = "sha256:6d11f7bb049ce3032e6fe3331137b771e34239ce1af18c55ef6a9b667cf2ef36", size = 18052, upload-time = "2025-11-14T08:27:21.917Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c1/7f/9eca482fbfd42f2ae19fa77fa231c0d2ba6ec7caf0ced16926480e1d746b/django_template_partials-25.2.tar.gz", hash = "sha256:55044e4a12d5d3adbc02df0758eb08fcd4e3451203e02a819f9853451696aef6", size = 17787, upload-time = "2025-09-17T13:31:37.761Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/9d/48f8721e48b938ca2e2dde577986624543be6ff9bdccac20ccb747be4287/django_template_partials-25.3-py2.py3-none-any.whl", hash = "sha256:a19334934cf40e4e1218802a4ddfdf22b8f78cc5a0b8c75a18b97e6ea4f3c108", size = 9702, upload-time = "2025-11-14T08:27:20.243Z" }, + { url = "https://files.pythonhosted.org/packages/9f/30/96a9d0e70efd00af9b9c011111b5eba48d17914e5b3d39516d4a7cc7e7fb/django_template_partials-25.2-py2.py3-none-any.whl", hash = "sha256:4c4f6569bd2d016281700a215c09ba4b7a4bbd2da95ce15d8c1ae76d36da44b4", size = 9621, upload-time = "2025-09-17T13:31:36.452Z" }, ] [[package]] @@ -452,6 +525,10 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/74/89/ecf5be9f5c59a0c53bcaa29671742c5e269cc7d0e2622e3f65f41df251bf/djlint-1.36.4.tar.gz", hash = "sha256:17254f218b46fe5a714b224c85074c099bcb74e3b2e1f15c2ddc2cf415a408a1", size = 47849, upload-time = "2024-12-24T13:06:36.36Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/da/83/88b4c885812921739f5529a29085c3762705154d41caf7eb9a8886a3380c/djlint-1.36.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ead475013bcac46095b1bbc8cf97ed2f06e83422335734363f8a76b4ba7e47c2", size = 354384, upload-time = "2024-12-24T13:06:20.809Z" }, + { url = "https://files.pythonhosted.org/packages/32/38/67695f7a150b3d9d62fadb65242213d96024151570c3cf5d966effa68b0e/djlint-1.36.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6c601dfa68ea253311deb4a29a7362b7a64933bdfcfb5a06618f3e70ad1fa835", size = 322971, upload-time = "2024-12-24T13:06:22.185Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7a/cd851393291b12e7fe17cf5d4d8874b8ea133aebbe9235f5314aabc96a52/djlint-1.36.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bda5014f295002363381969864addeb2db13955f1b26e772657c3b273ed7809f", size = 410972, upload-time = "2024-12-24T13:06:24.077Z" }, + { url = "https://files.pythonhosted.org/packages/6c/31/56469120394b970d4f079a552fde21ed27702ca729595ab0ed459eb6d240/djlint-1.36.4-cp313-cp313-win_amd64.whl", hash = "sha256:16ce37e085afe5a30953b2bd87cbe34c37843d94c701fc68a2dda06c1e428ff4", size = 362053, upload-time = "2024-12-24T13:06:25.432Z" }, { url = "https://files.pythonhosted.org/packages/4b/67/f7aeea9be6fb3bd984487af8d0d80225a0b1e5f6f7126e3332d349fb13fe/djlint-1.36.4-py3-none-any.whl", hash = "sha256:e9699b8ac3057a6ed04fb90835b89bee954ed1959c01541ce4f8f729c938afdd", size = 52290, upload-time = "2024-12-24T13:06:33.76Z" }, ] @@ -489,67 +566,67 @@ wheels = [ [[package]] name = "flake8-bugbear" -version = "25.11.29" +version = "24.12.12" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "flake8" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ec/20/2a996e2fca7810bd1b031901d65fc4292630895afcb946ebd00568bdc669/flake8_bugbear-25.11.29.tar.gz", hash = "sha256:b5d06710f3d26e595541ad303ad4d5cb52578bd4bccbb2c2c0b2c72e243dafc8", size = 84896, upload-time = "2025-11-29T20:51:57.75Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/25/48ba712ff589b0149f21135234f9bb45c14d6689acc6151b5e2ff8ac2ae9/flake8_bugbear-24.12.12.tar.gz", hash = "sha256:46273cef0a6b6ff48ca2d69e472f41420a42a46e24b2a8972e4f0d6733d12a64", size = 82907, upload-time = "2024-12-12T16:49:26.307Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/42/c18f199780d99a6f6a64c4a36f4ad28a445d9e11968a6025b21d0c8b6802/flake8_bugbear-25.11.29-py3-none-any.whl", hash = "sha256:9bf15e2970e736d2340da4c0a70493db964061c9c38f708cfe1f7b2d87392298", size = 37861, upload-time = "2025-11-29T20:51:56.439Z" }, + { url = "https://files.pythonhosted.org/packages/b9/21/0a875f75fbe4008bd171e2fefa413536258fe6b4cfaaa087986de74588f4/flake8_bugbear-24.12.12-py3-none-any.whl", hash = "sha256:1b6967436f65ca22a42e5373aaa6f2d87966ade9aa38d4baf2a1be550767545e", size = 36664, upload-time = "2024-12-12T16:49:23.584Z" }, ] [[package]] name = "flake8-pyproject" -version = "1.2.4" +version = "1.2.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "flake8" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/85/6a/cdee9ff7f2b7c6ddc219fd95b7c70c0a3d9f0367a506e9793eedfc72e337/flake8_pyproject-1.2.4-py3-none-any.whl", hash = "sha256:ea34c057f9a9329c76d98723bb2bb498cc6ba8ff9872c4d19932d48c91249a77", size = 5694, upload-time = "2025-11-28T21:40:01.309Z" }, + { url = "https://files.pythonhosted.org/packages/5f/1d/635e86f9f3a96b7ea9e9f19b5efe17a987e765c39ca496e4a893bb999112/flake8_pyproject-1.2.3-py3-none-any.whl", hash = "sha256:6249fe53545205af5e76837644dc80b4c10037e73a0e5db87ff562d75fb5bd4a", size = 4756, upload-time = "2023-03-21T20:51:38.911Z" }, ] [[package]] name = "google-auth" -version = "2.43.0" +version = "2.40.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, { name = "pyasn1-modules" }, { name = "rsa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ff/ef/66d14cf0e01b08d2d51ffc3c20410c4e134a1548fc246a6081eae585a4fe/google_auth-2.43.0.tar.gz", hash = "sha256:88228eee5fc21b62a1b5fe773ca15e67778cb07dc8363adcb4a8827b52d81483", size = 296359, upload-time = "2025-11-06T00:13:36.587Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/9b/e92ef23b84fa10a64ce4831390b7a4c2e53c0132568d99d4ae61d04c8855/google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77", size = 281029, upload-time = "2025-06-04T18:04:57.577Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/d1/385110a9ae86d91cc14c5282c61fe9f4dc41c0b9f7d423c6ad77038c4448/google_auth-2.43.0-py2.py3-none-any.whl", hash = "sha256:af628ba6fa493f75c7e9dbe9373d148ca9f4399b5ea29976519e0a3848eddd16", size = 223114, upload-time = "2025-11-06T00:13:35.209Z" }, + { url = "https://files.pythonhosted.org/packages/17/63/b19553b658a1692443c62bd07e5868adaa0ad746a0751ba62c59568cd45b/google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca", size = 216137, upload-time = "2025-06-04T18:04:55.573Z" }, ] [[package]] name = "idna" -version = "3.11" +version = "3.10" source = { registry = "https://pypi.org/simple" } -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" } +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" } wheels = [ - { 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" }, + { 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" }, ] [[package]] name = "iniconfig" -version = "2.3.0" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, + { 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 = "isort" -version = "7.0.0" +version = "6.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/63/53/4f3c058e3bace40282876f9b553343376ee687f3c35a525dc79dbd450f88/isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187", size = 805049, upload-time = "2025-10-11T13:30:59.107Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/21/1e2a441f74a653a144224d7d21afe8f4169e6c7c20bb13aec3a2dc3815e0/isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450", size = 821955, upload-time = "2025-02-26T21:13:16.955Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/ed/e3705d6d02b4f7aea715a353c8ce193efd0b5db13e204df895d38734c244/isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1", size = 94672, upload-time = "2025-10-11T13:30:57.665Z" }, + { url = "https://files.pythonhosted.org/packages/c1/11/114d0a5f4dabbdcedc1125dee0888514c3c3b16d3e9facad87ed96fad97c/isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615", size = 94186, upload-time = "2025-02-26T21:13:14.911Z" }, ] [[package]] @@ -600,24 +677,25 @@ wheels = [ [[package]] name = "jsonschema-specifications" -version = "2025.9.1" +version = "2025.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "referencing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513, upload-time = "2025-04-23T12:34:07.418Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, + { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" }, ] [[package]] name = "kubernetes" -version = "34.1.0" +version = "33.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "durationpy" }, { name = "google-auth" }, + { name = "oauthlib" }, { name = "python-dateutil" }, { name = "pyyaml" }, { name = "requests" }, @@ -626,9 +704,9 @@ dependencies = [ { name = "urllib3" }, { name = "websocket-client" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ef/55/3f880ef65f559cbed44a9aa20d3bdbc219a2c3a3bac4a30a513029b03ee9/kubernetes-34.1.0.tar.gz", hash = "sha256:8fe8edb0b5d290a2f3ac06596b23f87c658977d46b5f8df9d0f4ea83d0003912", size = 1083771, upload-time = "2025-09-29T20:23:49.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/52/19ebe8004c243fdfa78268a96727c71e08f00ff6fe69a301d0b7fcbce3c2/kubernetes-33.1.0.tar.gz", hash = "sha256:f64d829843a54c251061a8e7a14523b521f2dc5c896cf6d65ccf348648a88993", size = 1036779, upload-time = "2025-06-09T21:57:58.521Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/ec/65f7d563aa4a62dd58777e8f6aa882f15db53b14eb29aba0c28a20f7eb26/kubernetes-34.1.0-py2.py3-none-any.whl", hash = "sha256:bffba2272534e224e6a7a74d582deb0b545b7c9879d2cd9e4aae9481d1f2cc2a", size = 2008380, upload-time = "2025-09-29T20:23:47.684Z" }, + { url = "https://files.pythonhosted.org/packages/89/43/d9bebfc3db7dea6ec80df5cb2aad8d274dd18ec2edd6c4f21f32c237cbbb/kubernetes-33.1.0-py2.py3-none-any.whl", hash = "sha256:544de42b24b64287f7e0aa9513c93cb503f7f40eea39b20f66810011a86eabc5", size = 1941335, upload-time = "2025-06-09T21:57:56.327Z" }, ] [[package]] @@ -687,44 +765,66 @@ wheels = [ [[package]] name = "pillow" -version = "12.0.0" +version = "11.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" }, - { url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" }, - { url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" }, - { url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" }, - { url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" }, - { url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222, upload-time = "2025-10-15T18:23:20.909Z" }, - { url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657, upload-time = "2025-10-15T18:23:23.077Z" }, - { url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" }, - { url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" }, - { url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" }, - { url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" }, - { url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" }, - { url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" }, - { url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" }, - { url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" }, - { url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" }, - { url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001, upload-time = "2025-10-15T18:23:44.29Z" }, - { url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" }, - { url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" }, - { url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" }, - { url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" }, - { url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" }, - { url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" }, + { 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/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" }, + { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" }, + { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" }, + { 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/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" }, + { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" }, + { 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/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" }, + { 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/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" }, + { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" }, + { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" }, ] [[package]] name = "platformdirs" -version = "4.5.0" +version = "4.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, + { 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" }, ] [[package]] @@ -738,21 +838,21 @@ wheels = [ [[package]] name = "psycopg2-binary" -version = "2.9.11" +version = "2.9.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/bdc8274dc0585090b4e3432267d7be4dfbfd8971c0fa59167c711105a6bf/psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", size = 385764, upload-time = "2024-10-16T11:24:58.126Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" }, - { url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" }, - { url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" }, - { url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" }, - { url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" }, - { url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184, upload-time = "2025-10-30T02:55:32.483Z" }, - { url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" }, - { url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" }, - { url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737, upload-time = "2025-10-30T02:55:35.69Z" }, - { url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" }, - { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" }, + { url = "https://files.pythonhosted.org/packages/3e/30/d41d3ba765609c0763505d565c4d12d8f3c79793f0d0f044ff5a28bf395b/psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d", size = 3044699, upload-time = "2024-10-16T11:21:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/35/44/257ddadec7ef04536ba71af6bc6a75ec05c5343004a7ec93006bee66c0bc/psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb", size = 3275245, upload-time = "2024-10-16T11:21:51.989Z" }, + { url = "https://files.pythonhosted.org/packages/1b/11/48ea1cd11de67f9efd7262085588790a95d9dfcd9b8a687d46caf7305c1a/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7", size = 2851631, upload-time = "2024-10-16T11:21:57.584Z" }, + { url = "https://files.pythonhosted.org/packages/62/e0/62ce5ee650e6c86719d621a761fe4bc846ab9eff8c1f12b1ed5741bf1c9b/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d", size = 3082140, upload-time = "2024-10-16T11:22:02.005Z" }, + { url = "https://files.pythonhosted.org/packages/27/ce/63f946c098611f7be234c0dd7cb1ad68b0b5744d34f68062bb3c5aa510c8/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73", size = 3264762, upload-time = "2024-10-16T11:22:06.412Z" }, + { url = "https://files.pythonhosted.org/packages/43/25/c603cd81402e69edf7daa59b1602bd41eb9859e2824b8c0855d748366ac9/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673", size = 3020967, upload-time = "2024-10-16T11:22:11.583Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d6/8708d8c6fca531057fa170cdde8df870e8b6a9b136e82b361c65e42b841e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f", size = 2872326, upload-time = "2024-10-16T11:22:16.406Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ac/5b1ea50fc08a9df82de7e1771537557f07c2632231bbab652c7e22597908/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", size = 2822712, upload-time = "2024-10-16T11:22:21.366Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fc/504d4503b2abc4570fac3ca56eb8fed5e437bf9c9ef13f36b6621db8ef00/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", size = 2920155, upload-time = "2024-10-16T11:22:25.684Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d1/323581e9273ad2c0dbd1902f3fb50c441da86e894b6e25a73c3fda32c57e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", size = 2959356, upload-time = "2024-10-16T11:22:30.562Z" }, + { url = "https://files.pythonhosted.org/packages/08/50/d13ea0a054189ae1bc21af1d85b6f8bb9bbc5572991055d70ad9006fe2d6/psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", size = 2569224, upload-time = "2025-01-04T20:09:19.234Z" }, ] [[package]] @@ -787,11 +887,11 @@ wheels = [ [[package]] name = "pycparser" -version = "2.23" +version = "2.22" 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/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } 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/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, ] [[package]] @@ -823,7 +923,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.1" +version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -832,23 +932,23 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" }, + { 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" }, ] [[package]] name = "pytest-cov" -version = "7.0.0" +version = "6.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage" }, { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/4c/f883ab8f0daad69f47efdf95f55a66b51a8b939c430dadce0611508d9e99/pytest_cov-6.3.0.tar.gz", hash = "sha256:35c580e7800f87ce892e687461166e1ac2bcb8fb9e13aea79032518d6e503ff2", size = 70398, upload-time = "2025-09-06T15:40:14.361Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, + { url = "https://files.pythonhosted.org/packages/80/b4/bb7263e12aade3842b938bc5c6958cae79c5ee18992f9b9349019579da0f/pytest_cov-6.3.0-py3-none-any.whl", hash = "sha256:440db28156d2468cafc0415b4f8e50856a0d11faefa38f30906048fe490f1749", size = 25115, upload-time = "2025-09-06T15:40:12.44Z" }, ] [[package]] @@ -889,86 +989,77 @@ wheels = [ [[package]] name = "pytokens" -version = "0.3.0" +version = "0.1.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/8d/a762be14dae1c3bf280202ba3172020b2b0b4c537f94427435f19c413b72/pytokens-0.3.0.tar.gz", hash = "sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a", size = 17644, upload-time = "2025-11-05T13:36:35.34Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/5f/e959a442435e24f6fb5a01aec6c657079ceaca1b3baf18561c3728d681da/pytokens-0.1.10.tar.gz", hash = "sha256:c9a4bfa0be1d26aebce03e6884ba454e842f186a59ea43a6d3b25af58223c044", size = 12171, upload-time = "2025-02-19T14:51:22.001Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/25/d9db8be44e205a124f6c98bc0324b2bb149b7431c53877fc6d1038dddaf5/pytokens-0.3.0-py3-none-any.whl", hash = "sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3", size = 12195, upload-time = "2025-11-05T13:36:33.183Z" }, + { url = "https://files.pythonhosted.org/packages/60/e5/63bed382f6a7a5ba70e7e132b8b7b8abbcf4888ffa6be4877698dcfbed7d/pytokens-0.1.10-py3-none-any.whl", hash = "sha256:db7b72284e480e69fb085d9f251f66b3d2df8b7166059261258ff35f50fb711b", size = 12046, upload-time = "2025-02-19T14:51:18.694Z" }, ] [[package]] name = "pyyaml" -version = "6.0.3" +version = "6.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, ] [[package]] name = "referencing" -version = "0.37.0" +version = "0.36.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "rpds-py" }, ] -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" } +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" } wheels = [ - { 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" }, + { 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" }, ] [[package]] name = "regex" -version = "2025.11.3" +version = "2025.7.34" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/a9/546676f25e573a4cf00fe8e119b78a37b6a8fe2dc95cda877b30889c9c45/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", size = 414669, upload-time = "2025-11-03T21:34:22.089Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/de/e13fa6dc61d78b30ba47481f99933a3b49a57779d625c392d8036770a60d/regex-2025.7.34.tar.gz", hash = "sha256:9ead9765217afd04a86822dfcd4ed2747dfe426e887da413b15ff0ac2457e21a", size = 400714, upload-time = "2025-07-31T00:21:16.262Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/e9/f6e13de7e0983837f7b6d238ad9458800a874bf37c264f7923e63409944c/regex-2025.11.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9697a52e57576c83139d7c6f213d64485d3df5bf84807c35fa409e6c970801c6", size = 489089, upload-time = "2025-11-03T21:32:50.027Z" }, - { url = "https://files.pythonhosted.org/packages/a3/5c/261f4a262f1fa65141c1b74b255988bd2fa020cc599e53b080667d591cfc/regex-2025.11.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e18bc3f73bd41243c9b38a6d9f2366cd0e0137a9aebe2d8ff76c5b67d4c0a3f4", size = 291059, upload-time = "2025-11-03T21:32:51.682Z" }, - { url = "https://files.pythonhosted.org/packages/8e/57/f14eeb7f072b0e9a5a090d1712741fd8f214ec193dba773cf5410108bb7d/regex-2025.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:61a08bcb0ec14ff4e0ed2044aad948d0659604f824cbd50b55e30b0ec6f09c73", size = 288900, upload-time = "2025-11-03T21:32:53.569Z" }, - { url = "https://files.pythonhosted.org/packages/3c/6b/1d650c45e99a9b327586739d926a1cd4e94666b1bd4af90428b36af66dc7/regex-2025.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9c30003b9347c24bcc210958c5d167b9e4f9be786cb380a7d32f14f9b84674f", size = 799010, upload-time = "2025-11-03T21:32:55.222Z" }, - { url = "https://files.pythonhosted.org/packages/99/ee/d66dcbc6b628ce4e3f7f0cbbb84603aa2fc0ffc878babc857726b8aab2e9/regex-2025.11.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4e1e592789704459900728d88d41a46fe3969b82ab62945560a31732ffc19a6d", size = 864893, upload-time = "2025-11-03T21:32:57.239Z" }, - { url = "https://files.pythonhosted.org/packages/bf/2d/f238229f1caba7ac87a6c4153d79947fb0261415827ae0f77c304260c7d3/regex-2025.11.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6538241f45eb5a25aa575dbba1069ad786f68a4f2773a29a2bd3dd1f9de787be", size = 911522, upload-time = "2025-11-03T21:32:59.274Z" }, - { url = "https://files.pythonhosted.org/packages/bd/3d/22a4eaba214a917c80e04f6025d26143690f0419511e0116508e24b11c9b/regex-2025.11.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce22519c989bb72a7e6b36a199384c53db7722fe669ba891da75907fe3587db", size = 803272, upload-time = "2025-11-03T21:33:01.393Z" }, - { url = "https://files.pythonhosted.org/packages/84/b1/03188f634a409353a84b5ef49754b97dbcc0c0f6fd6c8ede505a8960a0a4/regex-2025.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:66d559b21d3640203ab9075797a55165d79017520685fb407b9234d72ab63c62", size = 787958, upload-time = "2025-11-03T21:33:03.379Z" }, - { url = "https://files.pythonhosted.org/packages/99/6a/27d072f7fbf6fadd59c64d210305e1ff865cc3b78b526fd147db768c553b/regex-2025.11.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:669dcfb2e38f9e8c69507bace46f4889e3abbfd9b0c29719202883c0a603598f", size = 859289, upload-time = "2025-11-03T21:33:05.374Z" }, - { url = "https://files.pythonhosted.org/packages/9a/70/1b3878f648e0b6abe023172dacb02157e685564853cc363d9961bcccde4e/regex-2025.11.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:32f74f35ff0f25a5021373ac61442edcb150731fbaa28286bbc8bb1582c89d02", size = 850026, upload-time = "2025-11-03T21:33:07.131Z" }, - { url = "https://files.pythonhosted.org/packages/dd/d5/68e25559b526b8baab8e66839304ede68ff6727237a47727d240006bd0ff/regex-2025.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e6c7a21dffba883234baefe91bc3388e629779582038f75d2a5be918e250f0ed", size = 789499, upload-time = "2025-11-03T21:33:09.141Z" }, - { url = "https://files.pythonhosted.org/packages/fc/df/43971264857140a350910d4e33df725e8c94dd9dee8d2e4729fa0d63d49e/regex-2025.11.3-cp314-cp314-win32.whl", hash = "sha256:795ea137b1d809eb6836b43748b12634291c0ed55ad50a7d72d21edf1cd565c4", size = 271604, upload-time = "2025-11-03T21:33:10.9Z" }, - { url = "https://files.pythonhosted.org/packages/01/6f/9711b57dc6894a55faf80a4c1b5aa4f8649805cb9c7aef46f7d27e2b9206/regex-2025.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f95fbaa0ee1610ec0fc6b26668e9917a582ba80c52cc6d9ada15e30aa9ab9ad", size = 280320, upload-time = "2025-11-03T21:33:12.572Z" }, - { url = "https://files.pythonhosted.org/packages/f1/7e/f6eaa207d4377481f5e1775cdeb5a443b5a59b392d0065f3417d31d80f87/regex-2025.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:dfec44d532be4c07088c3de2876130ff0fbeeacaa89a137decbbb5f665855a0f", size = 273372, upload-time = "2025-11-03T21:33:14.219Z" }, - { url = "https://files.pythonhosted.org/packages/c3/06/49b198550ee0f5e4184271cee87ba4dfd9692c91ec55289e6282f0f86ccf/regex-2025.11.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ba0d8a5d7f04f73ee7d01d974d47c5834f8a1b0224390e4fe7c12a3a92a78ecc", size = 491985, upload-time = "2025-11-03T21:33:16.555Z" }, - { url = "https://files.pythonhosted.org/packages/ce/bf/abdafade008f0b1c9da10d934034cb670432d6cf6cbe38bbb53a1cfd6cf8/regex-2025.11.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:442d86cf1cfe4faabf97db7d901ef58347efd004934da045c745e7b5bd57ac49", size = 292669, upload-time = "2025-11-03T21:33:18.32Z" }, - { url = "https://files.pythonhosted.org/packages/f9/ef/0c357bb8edbd2ad8e273fcb9e1761bc37b8acbc6e1be050bebd6475f19c1/regex-2025.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fd0a5e563c756de210bb964789b5abe4f114dacae9104a47e1a649b910361536", size = 291030, upload-time = "2025-11-03T21:33:20.048Z" }, - { url = "https://files.pythonhosted.org/packages/79/06/edbb67257596649b8fb088d6aeacbcb248ac195714b18a65e018bf4c0b50/regex-2025.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf3490bcbb985a1ae97b2ce9ad1c0f06a852d5b19dde9b07bdf25bf224248c95", size = 807674, upload-time = "2025-11-03T21:33:21.797Z" }, - { url = "https://files.pythonhosted.org/packages/f4/d9/ad4deccfce0ea336296bd087f1a191543bb99ee1c53093dcd4c64d951d00/regex-2025.11.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3809988f0a8b8c9dcc0f92478d6501fac7200b9ec56aecf0ec21f4a2ec4b6009", size = 873451, upload-time = "2025-11-03T21:33:23.741Z" }, - { url = "https://files.pythonhosted.org/packages/13/75/a55a4724c56ef13e3e04acaab29df26582f6978c000ac9cd6810ad1f341f/regex-2025.11.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f4ff94e58e84aedb9c9fce66d4ef9f27a190285b451420f297c9a09f2b9abee9", size = 914980, upload-time = "2025-11-03T21:33:25.999Z" }, - { url = "https://files.pythonhosted.org/packages/67/1e/a1657ee15bd9116f70d4a530c736983eed997b361e20ecd8f5ca3759d5c5/regex-2025.11.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eb542fd347ce61e1321b0a6b945d5701528dca0cd9759c2e3bb8bd57e47964d", size = 812852, upload-time = "2025-11-03T21:33:27.852Z" }, - { url = "https://files.pythonhosted.org/packages/b8/6f/f7516dde5506a588a561d296b2d0044839de06035bb486b326065b4c101e/regex-2025.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2d5919075a1f2e413c00b056ea0c2f065b3f5fe83c3d07d325ab92dce51d6", size = 795566, upload-time = "2025-11-03T21:33:32.364Z" }, - { url = "https://files.pythonhosted.org/packages/d9/dd/3d10b9e170cc16fb34cb2cef91513cf3df65f440b3366030631b2984a264/regex-2025.11.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3f8bf11a4827cc7ce5a53d4ef6cddd5ad25595d3c1435ef08f76825851343154", size = 868463, upload-time = "2025-11-03T21:33:34.459Z" }, - { url = "https://files.pythonhosted.org/packages/f5/8e/935e6beff1695aa9085ff83195daccd72acc82c81793df480f34569330de/regex-2025.11.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:22c12d837298651e5550ac1d964e4ff57c3f56965fc1812c90c9fb2028eaf267", size = 854694, upload-time = "2025-11-03T21:33:36.793Z" }, - { url = "https://files.pythonhosted.org/packages/92/12/10650181a040978b2f5720a6a74d44f841371a3d984c2083fc1752e4acf6/regex-2025.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ba394a3dda9ad41c7c780f60f6e4a70988741415ae96f6d1bf6c239cf01379", size = 799691, upload-time = "2025-11-03T21:33:39.079Z" }, - { url = "https://files.pythonhosted.org/packages/67/90/8f37138181c9a7690e7e4cb388debbd389342db3c7381d636d2875940752/regex-2025.11.3-cp314-cp314t-win32.whl", hash = "sha256:4bf146dca15cdd53224a1bf46d628bd7590e4a07fbb69e720d561aea43a32b38", size = 274583, upload-time = "2025-11-03T21:33:41.302Z" }, - { url = "https://files.pythonhosted.org/packages/8f/cd/867f5ec442d56beb56f5f854f40abcfc75e11d10b11fdb1869dd39c63aaf/regex-2025.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:adad1a1bcf1c9e76346e091d22d23ac54ef28e1365117d99521631078dfec9de", size = 284286, upload-time = "2025-11-03T21:33:43.324Z" }, - { url = "https://files.pythonhosted.org/packages/20/31/32c0c4610cbc070362bf1d2e4ea86d1ea29014d400a6d6c2486fcfd57766/regex-2025.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c54f768482cef41e219720013cd05933b6f971d9562544d691c68699bf2b6801", size = 274741, upload-time = "2025-11-03T21:33:45.557Z" }, + { url = "https://files.pythonhosted.org/packages/15/16/b709b2119975035169a25aa8e4940ca177b1a2e25e14f8d996d09130368e/regex-2025.7.34-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c3c9740a77aeef3f5e3aaab92403946a8d34437db930a0280e7e81ddcada61f5", size = 485334, upload-time = "2025-07-31T00:19:56.58Z" }, + { url = "https://files.pythonhosted.org/packages/94/a6/c09136046be0595f0331bc58a0e5f89c2d324cf734e0b0ec53cf4b12a636/regex-2025.7.34-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:69ed3bc611540f2ea70a4080f853741ec698be556b1df404599f8724690edbcd", size = 289942, upload-time = "2025-07-31T00:19:57.943Z" }, + { url = "https://files.pythonhosted.org/packages/36/91/08fc0fd0f40bdfb0e0df4134ee37cfb16e66a1044ac56d36911fd01c69d2/regex-2025.7.34-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d03c6f9dcd562c56527c42b8530aad93193e0b3254a588be1f2ed378cdfdea1b", size = 285991, upload-time = "2025-07-31T00:19:59.837Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/99dc8f6f756606f0c214d14c7b6c17270b6bbe26d5c1f05cde9dbb1c551f/regex-2025.7.34-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6164b1d99dee1dfad33f301f174d8139d4368a9fb50bf0a3603b2eaf579963ad", size = 797415, upload-time = "2025-07-31T00:20:01.668Z" }, + { url = "https://files.pythonhosted.org/packages/62/cf/2fcdca1110495458ba4e95c52ce73b361cf1cafd8a53b5c31542cde9a15b/regex-2025.7.34-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1e4f4f62599b8142362f164ce776f19d79bdd21273e86920a7b604a4275b4f59", size = 862487, upload-time = "2025-07-31T00:20:03.142Z" }, + { url = "https://files.pythonhosted.org/packages/90/38/899105dd27fed394e3fae45607c1983e138273ec167e47882fc401f112b9/regex-2025.7.34-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:72a26dcc6a59c057b292f39d41465d8233a10fd69121fa24f8f43ec6294e5415", size = 910717, upload-time = "2025-07-31T00:20:04.727Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f6/4716198dbd0bcc9c45625ac4c81a435d1c4d8ad662e8576dac06bab35b17/regex-2025.7.34-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5273fddf7a3e602695c92716c420c377599ed3c853ea669c1fe26218867002f", size = 801943, upload-time = "2025-07-31T00:20:07.1Z" }, + { url = "https://files.pythonhosted.org/packages/40/5d/cff8896d27e4e3dd11dd72ac78797c7987eb50fe4debc2c0f2f1682eb06d/regex-2025.7.34-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c1844be23cd40135b3a5a4dd298e1e0c0cb36757364dd6cdc6025770363e06c1", size = 786664, upload-time = "2025-07-31T00:20:08.818Z" }, + { url = "https://files.pythonhosted.org/packages/10/29/758bf83cf7b4c34f07ac3423ea03cee3eb3176941641e4ccc05620f6c0b8/regex-2025.7.34-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dde35e2afbbe2272f8abee3b9fe6772d9b5a07d82607b5788e8508974059925c", size = 856457, upload-time = "2025-07-31T00:20:10.328Z" }, + { url = "https://files.pythonhosted.org/packages/d7/30/c19d212b619963c5b460bfed0ea69a092c6a43cba52a973d46c27b3e2975/regex-2025.7.34-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f6e8e7af516a7549412ce57613e859c3be27d55341a894aacaa11703a4c31a", size = 849008, upload-time = "2025-07-31T00:20:11.823Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b8/3c35da3b12c87e3cc00010ef6c3a4ae787cff0bc381aa3d251def219969a/regex-2025.7.34-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:469142fb94a869beb25b5f18ea87646d21def10fbacb0bcb749224f3509476f0", size = 788101, upload-time = "2025-07-31T00:20:13.729Z" }, + { url = "https://files.pythonhosted.org/packages/47/80/2f46677c0b3c2b723b2c358d19f9346e714113865da0f5f736ca1a883bde/regex-2025.7.34-cp313-cp313-win32.whl", hash = "sha256:da7507d083ee33ccea1310447410c27ca11fb9ef18c95899ca57ff60a7e4d8f1", size = 264401, upload-time = "2025-07-31T00:20:15.233Z" }, + { url = "https://files.pythonhosted.org/packages/be/fa/917d64dd074682606a003cba33585c28138c77d848ef72fc77cbb1183849/regex-2025.7.34-cp313-cp313-win_amd64.whl", hash = "sha256:9d644de5520441e5f7e2db63aec2748948cc39ed4d7a87fd5db578ea4043d997", size = 275368, upload-time = "2025-07-31T00:20:16.711Z" }, + { url = "https://files.pythonhosted.org/packages/65/cd/f94383666704170a2154a5df7b16be28f0c27a266bffcd843e58bc84120f/regex-2025.7.34-cp313-cp313-win_arm64.whl", hash = "sha256:7bf1c5503a9f2cbd2f52d7e260acb3131b07b6273c470abb78568174fe6bde3f", size = 268482, upload-time = "2025-07-31T00:20:18.189Z" }, + { url = "https://files.pythonhosted.org/packages/ac/23/6376f3a23cf2f3c00514b1cdd8c990afb4dfbac3cb4a68b633c6b7e2e307/regex-2025.7.34-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:8283afe7042d8270cecf27cca558873168e771183d4d593e3c5fe5f12402212a", size = 485385, upload-time = "2025-07-31T00:20:19.692Z" }, + { url = "https://files.pythonhosted.org/packages/73/5b/6d4d3a0b4d312adbfd6d5694c8dddcf1396708976dd87e4d00af439d962b/regex-2025.7.34-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6c053f9647e3421dd2f5dff8172eb7b4eec129df9d1d2f7133a4386319b47435", size = 289788, upload-time = "2025-07-31T00:20:21.941Z" }, + { url = "https://files.pythonhosted.org/packages/92/71/5862ac9913746e5054d01cb9fb8125b3d0802c0706ef547cae1e7f4428fa/regex-2025.7.34-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a16dd56bbcb7d10e62861c3cd000290ddff28ea142ffb5eb3470f183628011ac", size = 286136, upload-time = "2025-07-31T00:20:26.146Z" }, + { url = "https://files.pythonhosted.org/packages/27/df/5b505dc447eb71278eba10d5ec940769ca89c1af70f0468bfbcb98035dc2/regex-2025.7.34-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69c593ff5a24c0d5c1112b0df9b09eae42b33c014bdca7022d6523b210b69f72", size = 797753, upload-time = "2025-07-31T00:20:27.919Z" }, + { url = "https://files.pythonhosted.org/packages/86/38/3e3dc953d13998fa047e9a2414b556201dbd7147034fbac129392363253b/regex-2025.7.34-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98d0ce170fcde1a03b5df19c5650db22ab58af375aaa6ff07978a85c9f250f0e", size = 863263, upload-time = "2025-07-31T00:20:29.803Z" }, + { url = "https://files.pythonhosted.org/packages/68/e5/3ff66b29dde12f5b874dda2d9dec7245c2051f2528d8c2a797901497f140/regex-2025.7.34-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d72765a4bff8c43711d5b0f5b452991a9947853dfa471972169b3cc0ba1d0751", size = 910103, upload-time = "2025-07-31T00:20:31.313Z" }, + { url = "https://files.pythonhosted.org/packages/9e/fe/14176f2182125977fba3711adea73f472a11f3f9288c1317c59cd16ad5e6/regex-2025.7.34-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4494f8fd95a77eb434039ad8460e64d57baa0434f1395b7da44015bef650d0e4", size = 801709, upload-time = "2025-07-31T00:20:33.323Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0d/80d4e66ed24f1ba876a9e8e31b709f9fd22d5c266bf5f3ab3c1afe683d7d/regex-2025.7.34-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4f42b522259c66e918a0121a12429b2abcf696c6f967fa37bdc7b72e61469f98", size = 786726, upload-time = "2025-07-31T00:20:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/c3ebb30e04a56c046f5c85179dc173818551037daae2c0c940c7b19152cb/regex-2025.7.34-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:aaef1f056d96a0a5d53ad47d019d5b4c66fe4be2da87016e0d43b7242599ffc7", size = 857306, upload-time = "2025-07-31T00:20:37.12Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b2/a4dc5d8b14f90924f27f0ac4c4c4f5e195b723be98adecc884f6716614b6/regex-2025.7.34-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:656433e5b7dccc9bc0da6312da8eb897b81f5e560321ec413500e5367fcd5d47", size = 848494, upload-time = "2025-07-31T00:20:38.818Z" }, + { url = "https://files.pythonhosted.org/packages/0d/21/9ac6e07a4c5e8646a90b56b61f7e9dac11ae0747c857f91d3d2bc7c241d9/regex-2025.7.34-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e91eb2c62c39705e17b4d42d4b86c4e86c884c0d15d9c5a47d0835f8387add8e", size = 787850, upload-time = "2025-07-31T00:20:40.478Z" }, + { url = "https://files.pythonhosted.org/packages/be/6c/d51204e28e7bc54f9a03bb799b04730d7e54ff2718862b8d4e09e7110a6a/regex-2025.7.34-cp314-cp314-win32.whl", hash = "sha256:f978ddfb6216028c8f1d6b0f7ef779949498b64117fc35a939022f67f810bdcb", size = 269730, upload-time = "2025-07-31T00:20:42.253Z" }, + { url = "https://files.pythonhosted.org/packages/74/52/a7e92d02fa1fdef59d113098cb9f02c5d03289a0e9f9e5d4d6acccd10677/regex-2025.7.34-cp314-cp314-win_amd64.whl", hash = "sha256:4b7dc33b9b48fb37ead12ffc7bdb846ac72f99a80373c4da48f64b373a7abeae", size = 278640, upload-time = "2025-07-31T00:20:44.42Z" }, + { url = "https://files.pythonhosted.org/packages/d1/78/a815529b559b1771080faa90c3ab401730661f99d495ab0071649f139ebd/regex-2025.7.34-cp314-cp314-win_arm64.whl", hash = "sha256:4b8c4d39f451e64809912c82392933d80fe2e4a87eeef8859fcc5380d0173c64", size = 271757, upload-time = "2025-07-31T00:20:46.355Z" }, ] [[package]] @@ -1001,39 +1092,68 @@ wheels = [ [[package]] name = "rpds-py" -version = "0.30.0" +version = "0.27.1" source = { registry = "https://pypi.org/simple" } -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" } +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" } wheels = [ - { 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/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, - { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, - { 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/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, - { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { 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/00/95/3c863973d409210da7fb41958172c6b7dbe7fc34e04d3cc1f10bb85e979f/rpds_py-0.27.1-cp313-cp313-win32.whl", hash = "sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3", size = 221462, upload-time = "2025-08-27T12:13:48.742Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2c/5867b14a81dc217b56d95a9f2a40fdbc56a1ab0181b80132beeecbd4b2d6/rpds_py-0.27.1-cp313-cp313-win_amd64.whl", hash = "sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83", size = 232034, upload-time = "2025-08-27T12:13:50.11Z" }, + { url = "https://files.pythonhosted.org/packages/c7/78/3958f3f018c01923823f1e47f1cc338e398814b92d83cd278364446fac66/rpds_py-0.27.1-cp313-cp313-win_arm64.whl", hash = "sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d", size = 222392, upload-time = "2025-08-27T12:13:52.587Z" }, + { 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/e5/ee/375469849e6b429b3516206b4580a79e9ef3eb12920ddbd4492b56eaacbe/rpds_py-0.27.1-cp313-cp313t-win32.whl", hash = "sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688", size = 216714, upload-time = "2025-08-27T12:14:13.629Z" }, + { url = "https://files.pythonhosted.org/packages/21/87/3fc94e47c9bd0742660e84706c311a860dcae4374cf4a03c477e23ce605a/rpds_py-0.27.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797", size = 228943, upload-time = "2025-08-27T12:14:14.937Z" }, + { 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/c5/d6/99228e6bbcf4baa764b18258f519a9035131d91b538d4e0e294313462a98/rpds_py-0.27.1-cp314-cp314-win32.whl", hash = "sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3", size = 221943, upload-time = "2025-08-27T12:14:36.898Z" }, + { url = "https://files.pythonhosted.org/packages/be/07/c802bc6b8e95be83b79bdf23d1aa61d68324cb1006e245d6c58e959e314d/rpds_py-0.27.1-cp314-cp314-win_amd64.whl", hash = "sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456", size = 233739, upload-time = "2025-08-27T12:14:38.386Z" }, + { url = "https://files.pythonhosted.org/packages/c8/89/3e1b1c16d4c2d547c5717377a8df99aee8099ff050f87c45cb4d5fa70891/rpds_py-0.27.1-cp314-cp314-win_arm64.whl", hash = "sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3", size = 223120, upload-time = "2025-08-27T12:14:39.82Z" }, + { 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/04/2e/30b5ea18c01379da6272a92825dd7e53dc9d15c88a19e97932d35d430ef7/rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a", size = 217385, upload-time = "2025-08-27T12:15:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/32/7d/97119da51cb1dd3f2f3c0805f155a3aa4a95fa44fe7d78ae15e69edf4f34/rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772", size = 230097, upload-time = "2025-08-27T12:15:03.961Z" }, ] [[package]] @@ -1059,27 +1179,27 @@ wheels = [ [[package]] name = "s3transfer" -version = "0.16.0" +version = "0.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/05/d52bf1e65044b4e5e27d4e63e8d1579dbdec54fce685908ae09bc3720030/s3transfer-0.13.1.tar.gz", hash = "sha256:c3fdba22ba1bd367922f27ec8032d6a1cf5f10c934fb5d68cf60fd5a23d936cf", size = 150589, upload-time = "2025-07-18T19:22:42.31Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4f/d073e09df851cfa251ef7840007d04db3293a0482ce607d2b993926089be/s3transfer-0.13.1-py3-none-any.whl", hash = "sha256:a981aa7429be23fe6dfc13e80e4020057cbab622b08c0315288758d67cabc724", size = 85308, upload-time = "2025-07-18T19:22:40.947Z" }, ] [[package]] name = "sentry-sdk" -version = "2.46.0" +version = "2.39.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7c/d7/c140a5837649e2bf2ec758494fde1d9a016c76777eab64e75ef38d685bbb/sentry_sdk-2.46.0.tar.gz", hash = "sha256:91821a23460725734b7741523021601593f35731808afc0bb2ba46c27b8acd91", size = 374761, upload-time = "2025-11-24T09:34:13.932Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/72/43294fa4bdd75c51610b5104a3ff834459ba653abb415150aa7826a249dd/sentry_sdk-2.39.0.tar.gz", hash = "sha256:8c185854d111f47f329ab6bc35993f28f7a6b7114db64aa426b326998cfa14e9", size = 348556, upload-time = "2025-09-25T09:15:39.064Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/b6/ce7c502a366f4835b1f9c057753f6989a92d3c70cbadb168193f5fb7499b/sentry_sdk-2.46.0-py2.py3-none-any.whl", hash = "sha256:4eeeb60198074dff8d066ea153fa6f241fef1668c10900ea53a4200abc8da9b1", size = 406266, upload-time = "2025-11-24T09:34:12.114Z" }, + { url = "https://files.pythonhosted.org/packages/dd/44/4356cc64246ba7b2b920f7c97a85c3c52748e213e250b512ee8152eb559d/sentry_sdk-2.39.0-py2.py3-none-any.whl", hash = "sha256:ba655ca5e57b41569b18e2a5552cb3375209760a5d332cdd87c6c3f28f729602", size = 370851, upload-time = "2025-09-25T09:15:36.35Z" }, ] [package.optional-dependencies] @@ -1132,38 +1252,38 @@ dev = [ [package.metadata] requires-dist = [ { name = "argon2-cffi", specifier = ">=25.1.0" }, - { name = "cryptography", specifier = ">=46.0.3" }, - { name = "django", specifier = "==5.2.8" }, - { name = "django-allauth", specifier = ">=65.13.1" }, - { name = "django-auditlog", specifier = ">=3.3.0" }, - { name = "django-fernet-encrypted-fields", specifier = ">=0.3.1" }, + { name = "cryptography", specifier = ">=46.0.1" }, + { name = "django", specifier = "==5.2.7" }, + { name = "django-allauth", specifier = ">=65.11.2" }, + { name = "django-auditlog", specifier = ">=3.2.1" }, + { name = "django-fernet-encrypted-fields", specifier = ">=0.3.0" }, { name = "django-jsonform", specifier = ">=2.23.2" }, { name = "django-scopes", specifier = ">=2.0.0" }, { name = "django-storages", extras = ["s3"], specifier = ">=1.14.6" }, - { name = "django-template-partials", specifier = ">=25.3" }, + { name = "django-template-partials", specifier = ">=25.2" }, { name = "jsonschema", specifier = ">=4.25.1" }, - { name = "kubernetes", specifier = ">=34.1.0" }, - { name = "pillow", specifier = ">=12.0.0" }, - { name = "psycopg2-binary", specifier = ">=2.9.11" }, + { name = "kubernetes", specifier = ">=33.1.0" }, + { name = "pillow", specifier = ">=11.3.0" }, + { name = "psycopg2-binary", specifier = ">=2.9.10" }, { name = "pyjwt", specifier = ">=2.10.1" }, { name = "requests", specifier = ">=2.32.5" }, { name = "rules", specifier = ">=3.5" }, - { name = "sentry-sdk", extras = ["django"], specifier = ">=2.46.0" }, + { name = "sentry-sdk", extras = ["django"], specifier = ">=2.39.0" }, { name = "urlman", specifier = ">=2.0.2" }, ] [package.metadata.requires-dev] dev = [ - { name = "black", specifier = ">=25.11.0" }, + { name = "black", specifier = ">=25.9.0" }, { name = "bumpver", specifier = ">=2025.1131" }, - { name = "coverage", specifier = ">=7.12.0" }, + { name = "coverage", specifier = ">=7.10.7" }, { name = "djlint", specifier = ">=1.36.4" }, { name = "flake8", specifier = ">=7.3.0" }, - { name = "flake8-bugbear", specifier = ">=25.11.29" }, + { name = "flake8-bugbear", specifier = ">=24.12.12" }, { name = "flake8-pyproject", specifier = ">=1.2.3" }, - { name = "isort", specifier = ">=7.0.0" }, - { name = "pytest", specifier = ">=9.0.1" }, - { name = "pytest-cov", specifier = ">=7.0.0" }, + { name = "isort", specifier = ">=6.0.1" }, + { name = "pytest", specifier = ">=8.4.2" }, + { name = "pytest-cov", specifier = ">=6.3.0" }, { name = "pytest-django", specifier = ">=4.11.1" }, { name = "pytest-mock", specifier = ">=3.15.1" }, ] @@ -1179,11 +1299,11 @@ wheels = [ [[package]] name = "sqlparse" -version = "0.5.4" +version = "0.5.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/67/701f86b28d63b2086de47c942eccf8ca2208b3be69715a1119a4e384415a/sqlparse-0.5.4.tar.gz", hash = "sha256:4396a7d3cf1cd679c1be976cf3dc6e0a51d0111e87787e7a8d780e7d5a998f9e", size = 120112, upload-time = "2025-11-28T07:10:18.377Z" } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/70/001ee337f7aa888fb2e3f5fd7592a6afc5283adb1ed44ce8df5764070f22/sqlparse-0.5.4-py3-none-any.whl", hash = "sha256:99a9f0314977b76d776a0fcb8554de91b9bb8a18560631d6bc48721d07023dcb", size = 45933, upload-time = "2025-11-28T07:10:19.73Z" }, + { 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" }, ] [[package]] @@ -1218,11 +1338,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.3.0" +version = "2.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268, upload-time = "2024-12-22T07:47:30.032Z" } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369, upload-time = "2024-12-22T07:47:28.074Z" }, + { 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" }, ] [[package]] @@ -1236,9 +1356,9 @@ wheels = [ [[package]] name = "websocket-client" -version = "1.9.0" +version = "1.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648, upload-time = "2024-04-23T22:16:16.976Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, + { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826, upload-time = "2024-04-23T22:16:14.422Z" }, ]