Compare commits

..

43 commits

Author SHA1 Message Date
f2b38a9f11 Merge pull request 'Update dependency flake8-bugbear to >=25.11.29' (#310) from renovate/flake8-bugbear-25.x into main
All checks were successful
Build and Deploy Staging / build (push) Successful in 40s
Tests / test (push) Successful in 26s
Build and Deploy Staging / deploy (push) Successful in 7s
Reviewed-on: #310
2025-12-02 07:29:19 +00:00
cdc5947a63 Merge pull request 'Update https://github.com/renovatebot/github-action action to v44.0.5' (#312) from renovate/https-github.com-renovatebot-github-action-44.x into main 2025-12-02 07:29:10 +00:00
5ef1fcc28c Merge pull request 'Lock file maintenance' (#311) from renovate/lock-file-maintenance into main
Some checks failed
Tests / test (push) Waiting to run
Build and Deploy Staging / build (push) Has been cancelled
Build and Deploy Staging / deploy (push) Has been cancelled
Reviewed-on: #311
2025-12-02 07:29:06 +00:00
Renovate Bot
0aebbc42f0 Update https://github.com/renovatebot/github-action action to v44.0.5 2025-12-02 03:01:04 +00:00
Renovate Bot
0a47eedc9e Lock file maintenance
All checks were successful
Tests / test (push) Successful in 28s
2025-12-01 03:01:20 +00:00
Renovate Bot
c60a69a305 Update dependency flake8-bugbear to >=25.11.29
All checks were successful
Tests / test (push) Successful in 24s
2025-11-30 03:01:16 +00:00
f9bf71a2c1 Merge pull request 'Lock file maintenance' (#305) from renovate/lock-file-maintenance into main
All checks were successful
Build and Deploy Staging / build (push) Successful in 40s
Tests / test (push) Successful in 25s
Build and Deploy Staging / deploy (push) Successful in 6s
Reviewed-on: #305
2025-11-25 08:43:14 +00:00
1cf77e3b25 Merge pull request 'Update https://github.com/renovatebot/github-action action to v44.0.4' (#306) from renovate/https-github.com-renovatebot-github-action-44.x into main 2025-11-25 08:42:51 +00:00
230b66ef92 Merge pull request 'Update dependency sentry-sdk to >=2.46.0' (#307) from renovate/sentry-sdk-2.x into main
Some checks failed
Tests / test (push) Waiting to run
Build and Deploy Staging / build (push) Has been cancelled
Build and Deploy Staging / deploy (push) Has been cancelled
Reviewed-on: #307
2025-11-25 08:42:47 +00:00
Renovate Bot
0c09ae8eca Update dependency sentry-sdk to >=2.46.0
All checks were successful
Tests / test (push) Successful in 26s
2025-11-25 03:00:52 +00:00
Renovate Bot
8e725932c4 Update https://github.com/renovatebot/github-action action to v44.0.4 2025-11-25 03:00:34 +00:00
Renovate Bot
870acfa81b Lock file maintenance
All checks were successful
Tests / test (push) Successful in 27s
2025-11-24 03:00:48 +00:00
0a454964f0 Merge pull request 'Update actions/checkout action to v6' (#304) from renovate/actions-checkout-6.x into main 2025-11-21 08:48:45 +00:00
31692a333f Merge pull request 'Update dependency django-allauth to >=65.13.1' (#303) from renovate/django-allauth-65.x into main
All checks were successful
Build and Deploy Staging / build (push) Successful in 40s
Tests / test (push) Successful in 25s
Build and Deploy Staging / deploy (push) Successful in 6s
Reviewed-on: #303
2025-11-21 08:48:40 +00:00
Renovate Bot
ea313eca1d Update actions/checkout action to v6 2025-11-21 03:01:10 +00:00
Renovate Bot
1ca02bfb4d Update dependency django-allauth to >=65.13.1
All checks were successful
Tests / test (push) Successful in 24s
2025-11-21 03:01:01 +00:00
8b0c2a8d43 Merge pull request 'Disk size SI units' (#302) from 287-disk-size-unit into main
All checks were successful
Build and Deploy Staging / build (push) Successful in 44s
Tests / test (push) Successful in 25s
Build and Deploy Staging / deploy (push) Successful in 7s
Reviewed-on: #302
2025-11-20 14:59:18 +00:00
0a621f6372 Add default unit for disk size field
All checks were successful
Tests / test (push) Successful in 29s
2025-11-20 12:06:54 +01:00
de6794046d Implement unit fields for numeric fields 2025-11-20 12:06:17 +01:00
a268625d80 Add number addon widget 2025-11-20 11:56:10 +01:00
c0d3a83c9d Merge pull request 'Update dependency coverage to >=7.12.0' (#300) from renovate/coverage-7.x into main
All checks were successful
Build and Deploy Staging / build (push) Successful in 40s
Tests / test (push) Successful in 27s
Build and Deploy Staging / deploy (push) Successful in 7s
Reviewed-on: #300
2025-11-20 08:18:29 +00:00
f72c93ff2c Merge pull request 'Update https://github.com/renovatebot/github-action action to v44.0.3' (#299) from renovate/https-github.com-renovatebot-github-action-44.x into main 2025-11-20 08:02:54 +00:00
bcca366381 Merge pull request 'Update dependency sentry-sdk to >=2.45.0' (#301) from renovate/sentry-sdk-2.x into main
All checks were successful
Build and Deploy Staging / build (push) Successful in 1m3s
Tests / test (push) Successful in 26s
Build and Deploy Staging / deploy (push) Successful in 7s
Reviewed-on: #301
2025-11-20 08:02:50 +00:00
Renovate Bot
aa40027f0a Update dependency sentry-sdk to >=2.45.0
All checks were successful
Tests / test (push) Successful in 29s
2025-11-19 03:01:01 +00:00
Renovate Bot
ae92516747 Update dependency coverage to >=7.12.0
All checks were successful
Tests / test (push) Successful in 29s
2025-11-19 03:00:55 +00:00
Renovate Bot
def01771c4 Update https://github.com/renovatebot/github-action action to v44.0.3 2025-11-18 03:00:49 +00:00
f99f8511bb
bump version 2025.11.13-0 -> 2025.11.17-0
All checks were successful
Build and Deploy Staging / build (push) Successful in 40s
Build and Deploy Antora Docs / build (push) Successful in 1m9s
Tests / test (push) Successful in 25s
Build and Deploy Staging / deploy (push) Successful in 6s
Build and Deploy Antora Docs / deploy (push) Successful in 5s
2025-11-17 11:50:15 +01:00
234a169128 Merge pull request 'Exoscale offboarding MVP' (#282) from 262-exoscale-offboarding into main
All checks were successful
Build and Deploy Staging / build (push) Successful in 42s
Build and Deploy Production / build (release) Successful in 42s
Build and Deploy Production / deploy (release) Successful in 15s
Tests / test (push) Successful in 27s
Build and Deploy Staging / deploy (push) Successful in 6s
Reviewed-on: #282
2025-11-17 10:49:23 +00:00
3e17e03da9
support hardcoded suspend plan for exoscale
All checks were successful
Tests / test (push) Successful in 27s
2025-11-17 11:48:19 +01:00
208f3c357d
odoo wants html for linebreaks 2025-11-17 11:38:02 +01:00
e4c64c4a17
Extract odoo helpdesk ticket creation
All checks were successful
Tests / test (push) Successful in 28s
2025-11-17 09:44:55 +01:00
61f1065bc6
Start work on implementing Exoscale offboarding
rel #262
2025-11-17 09:44:55 +01:00
3d62595663 Merge pull request 'Update dependency django-template-partials to >=25.3' (#297) from renovate/django-template-partials-25.x into main
All checks were successful
Build and Deploy Staging / build (push) Successful in 39s
Tests / test (push) Successful in 26s
Build and Deploy Staging / deploy (push) Successful in 7s
Reviewed-on: #297
2025-11-17 08:44:27 +00:00
10fe5a9b2a Merge pull request 'Lock file maintenance' (#298) from renovate/lock-file-maintenance into main
Some checks failed
Tests / test (push) Waiting to run
Build and Deploy Staging / build (push) Has been cancelled
Build and Deploy Staging / deploy (push) Has been cancelled
Reviewed-on: #298
2025-11-17 08:44:14 +00:00
68335a098d Merge pull request 'Allow admins to disable the expert mode form' (#296) from 289-hide-expert-mode into main
Some checks failed
Tests / test (push) Waiting to run
Build and Deploy Staging / build (push) Has been cancelled
Build and Deploy Staging / deploy (push) Has been cancelled
Reviewed-on: #296
2025-11-17 08:43:32 +00:00
dd2097a281 Merge pull request 'Support single (non-array) FQDN values' (#295) from 288-single-fqdn into main
Some checks failed
Build and Deploy Staging / deploy (push) Blocked by required conditions
Build and Deploy Staging / build (push) Successful in 41s
Tests / test (push) Has been cancelled
Reviewed-on: #295
2025-11-17 08:42:36 +00:00
Renovate Bot
6996e56de7 Lock file maintenance
All checks were successful
Tests / test (push) Successful in 23s
2025-11-17 03:01:02 +00:00
Renovate Bot
e8d6c1acd5 Update dependency django-template-partials to >=25.3
All checks were successful
Tests / test (push) Successful in 24s
2025-11-15 03:01:04 +00:00
1dbeb31985 Allow admins to disable the expert mode form
All checks were successful
Tests / test (push) Successful in 27s
closes #289
2025-11-14 17:31:18 +01:00
3f9fdd0e48 Make sure fqdn generation works with single values
All checks were successful
Tests / test (push) Successful in 27s
2025-11-14 16:52:14 +01:00
4097261efe Support single-value FQDN in "open" button 2025-11-14 16:41:44 +01:00
63f5982350
improve release workflow with tags
All checks were successful
Build and Deploy Staging / build (push) Successful in 39s
Tests / test (push) Successful in 26s
Build and Deploy Staging / deploy (push) Successful in 6s
2025-11-13 14:11:46 +01:00
325d274cea
bump version 2025.10.27-0 -> 2025.11.13-0
All checks were successful
Build and Deploy Staging / build (push) Successful in 40s
Build and Deploy Antora Docs / build (push) Successful in 1m8s
Tests / test (push) Successful in 26s
Build and Deploy Staging / deploy (push) Successful in 7s
Build and Deploy Antora Docs / deploy (push) Successful in 4s
2025-11-13 11:05:43 +01:00
30 changed files with 944 additions and 224 deletions

View file

@ -12,6 +12,9 @@ on:
- "pyproject.toml"
- "uv.lock"
workflow_dispatch:
release:
types:
- published
jobs:
build:
@ -23,7 +26,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@ -69,7 +72,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Determine image tag
id: determine-tag

View file

@ -22,7 +22,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@ -53,7 +53,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Deploy to OpenShift
uses: docker://quay.io/appuio/oc:v4.19

View file

@ -17,7 +17,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@ -49,7 +49,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Deploy to OpenShift
uses: docker://quay.io/appuio/oc:v4.19

View file

@ -11,7 +11,7 @@ jobs:
container: catthehacker/ubuntu:act-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
@ -19,7 +19,7 @@ jobs:
node-version: "24"
- name: Renovate
uses: https://github.com/renovatebot/github-action@v44.0.2
uses: https://github.com/renovatebot/github-action@v44.0.5
with:
token: ${{ secrets.RENOVATE_TOKEN }}
env:

View file

@ -18,7 +18,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6

View file

@ -4,7 +4,7 @@
The Servala Self-Service Portal
Latest release: 2025.10.27-0
Latest release: 2025.11.17-0
## Documentation

View file

@ -1,5 +1,82 @@
= 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

View file

@ -138,4 +138,12 @@ 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

View file

@ -8,13 +8,13 @@ dependencies = [
"argon2-cffi>=25.1.0",
"cryptography>=46.0.3",
"django==5.2.8",
"django-allauth>=65.13.0",
"django-allauth>=65.13.1",
"django-auditlog>=3.3.0",
"django-fernet-encrypted-fields>=0.3.1",
"django-jsonform>=2.23.2",
"django-scopes>=2.0.0",
"django-storages[s3]>=1.14.6",
"django-template-partials>=25.2",
"django-template-partials>=25.3",
"jsonschema>=4.25.1",
"kubernetes>=34.1.0",
"pillow>=12.0.0",
@ -22,7 +22,7 @@ dependencies = [
"pyjwt>=2.10.1",
"requests>=2.32.5",
"rules>=3.5",
"sentry-sdk[django]>=2.44.0",
"sentry-sdk[django]>=2.46.0",
"urlman>=2.0.2",
]
@ -30,10 +30,10 @@ dependencies = [
dev = [
"black>=25.11.0",
"bumpver>=2025.1131",
"coverage>=7.11.3",
"coverage>=7.12.0",
"djlint>=1.36.4",
"flake8>=7.3.0",
"flake8-bugbear>=25.10.21",
"flake8-bugbear>=25.11.29",
"flake8-pyproject>=1.2.3",
"isort>=7.0.0",
"pytest>=9.0.1",
@ -61,7 +61,7 @@ testpaths = "src/tests"
pythonpath = "src"
[tool.bumpver]
current_version = "2025.10.27-0"
current_version = "2025.11.17-0"
version_pattern = "YYYY.0M.0D-INC0"
commit_message = "bump version {old_version} -> {new_version}"
tag_message = "{new_version}"
@ -69,7 +69,7 @@ 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
tag = false
push = true
[tool.bumpver.file_patterns]

View file

@ -1 +1 @@
__version__ = "2025.10.27-0"
__version__ = "2025.11.17-0"

View file

@ -6,6 +6,7 @@ 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
@ -19,7 +20,8 @@ from servala.core.models import (
OrganizationRole,
User,
)
from servala.core.models.service import Service, ServiceOffering
from servala.core.models.service import Service, ServiceInstance, ServiceOffering
from servala.core.odoo import create_helpdesk_ticket
logger = logging.getLogger(__name__)
@ -28,9 +30,7 @@ logger = logging.getLogger(__name__)
@method_decorator(login_not_required, name="dispatch")
class OSBServiceInstanceView(OSBBasicAuthPermission, View):
"""
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
OSB API endpoint for service instance management via Exoscale.
"""
def _error(self, error):
@ -177,3 +177,169 @@ 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("<br/>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 = "<br/>".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}"
)

View file

@ -322,7 +322,7 @@ class ServiceDefinitionAdmin(admin.ModelAdmin):
(
_("Form Configuration"),
{
"fields": ("form_config",),
"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."
),

View file

@ -1,10 +1,12 @@
from contextlib import suppress
from django import forms
from django.core.validators import MaxValueValidator, MinValueValidator
from django.forms.models import ModelForm, ModelFormMetaclass
from servala.core.crd.utils import deslugify
from servala.core.models import ControlPlaneCRD
from servala.frontend.forms.widgets import DynamicArrayWidget
from servala.frontend.forms.widgets import DynamicArrayWidget, NumberInputWithAddon
# Fields that must be present in every form
MANDATORY_FIELDS = ["name"]
@ -25,6 +27,11 @@ DEFAULT_FIELD_CONFIGS = {
"help_text": "Domain names for accessing this service",
"required": False,
},
"spec.parameters.size.disk": {
"type": "number",
"label": "Disk size",
"addon_text": "Gi",
},
}
@ -335,6 +342,19 @@ class CustomFormMixin(FormGeneratorMixin):
if field_type == "number":
min_val = field_config.get("min_value")
max_val = field_config.get("max_value")
unit = field_config.get("addon_text")
if unit:
field.widget = NumberInputWithAddon(addon_text=unit)
field.addon_text = unit
value = self.initial.get(field_name)
if value and isinstance(value, str) and value.endswith(unit):
numeric_value = value[: -len(unit)]
with suppress(ValueError):
if "." in numeric_value:
self.initial[field_name] = float(numeric_value)
else:
self.initial[field_name] = int(numeric_value)
validators = []
if min_val is not None:
@ -406,6 +426,11 @@ class CustomFormMixin(FormGeneratorMixin):
mapping = field_name
value = self.cleaned_data.get(field_name)
field = self.fields[field_name]
if addon_text := getattr(field, "addon_text", None):
value = f"{value}{addon_text}"
parts = mapping.split(".")
current = nested
for part in parts[:-1]:

View file

@ -33,7 +33,10 @@ class Migration(migrations.Migration):
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.',
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",
),

View file

@ -0,0 +1,25 @@
# 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",
),
),
]

View file

@ -370,6 +370,14 @@ class ServiceDefinition(ServalaModelMixin, models.Model):
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,
@ -961,5 +969,21 @@ 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)

View file

@ -207,3 +207,19 @@ 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])

View file

@ -2,6 +2,7 @@ import json
from django import forms
from django.core.exceptions import ValidationError
from django.forms.widgets import NumberInput
class DynamicArrayWidget(forms.Widget):
@ -216,3 +217,21 @@ 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

View file

@ -0,0 +1,11 @@
<div class="input-group">
<input type="{{ widget.type }}"
name="{{ widget.name }}"
{% if widget.value != None %}value="{{ widget.value }}"{% endif %}
{% if widget.attrs.id %}id="{{ widget.attrs.id }}"{% endif %}
{% for name, value in widget.attrs.items %} {% if value is not False and name != "id" %} {{ name }}{% if value is not True %}="{{ value }}"{% endif %}
{% endif %}
{% endfor %}
class="form-control{% if widget.attrs.class %} {{ widget.attrs.class }}{% endif %}" />
<span class="input-group-text">{{ widget.addon_text }}</span>
</div>

View file

@ -7,8 +7,8 @@
{% endblock html_title %}
{% block page_title_extra %}
<div>
{% if instance.spec.parameters.service.fqdn %}
<a href="https://{{ instance.spec.parameters.service.fqdn.0 }}"
{% if instance.fqdn_url %}
<a href="https://{{ instance.fqdn_url }}"
target="_blank"
rel="noopener noreferrer"
class="btn btn-success me-1 mb-1">

View file

@ -13,7 +13,7 @@
<a href="{{ instance.urls.base }}" class="btn btn-secondary me-1 mb-1">{% translate "Back" %}</a>
{% endblock page_title_extra %}
{% partialdef service-form %}
{% if form %}
{% if form or custom_form %}
<div class="card">
<div class="card-header d-flex align-items-center"></div>
<div class="card-body">
@ -31,7 +31,7 @@
{% block content %}
<section class="section">
<div class="card">
{% if not form %}
{% if not form and not custom_form %}
<div class="alert alert-warning" role="alert">
{% translate "Cannot update this service instance because its details could not be retrieved from the underlying system. It might have been deleted externally." %}
</div>

View file

@ -6,7 +6,7 @@
{% if form_action %}action="{{ form_action }}"{% endif %}>
{% csrf_token %}
{% include "frontend/forms/errors.html" %}
{% if form %}
{% if form and expert_form and not hide_expert_mode %}
<div class="mb-3 text-end">
<a href="#"
class="text-muted small"
@ -81,7 +81,7 @@
</div>
{% endif %}
</div>
{% if expert_form %}
{% if expert_form and not hide_expert_mode %}
<div id="expert-form-container"
class="expert-crd-form"
style="{% if form %}display:none{% endif %}">
@ -144,6 +144,6 @@
</div>
</form>
<script defer src="{% static 'js/bootstrap-tabs.js' %}"></script>
{% if form %}
{% if form and not hide_expert_mode %}
<script defer src="{% static 'js/expert-mode.js' %}"></script>
{% endif %}

View file

@ -167,7 +167,11 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView
}
def get_instance_form(self, ignore_data=False):
if not self.context_object or not self.context_object.model_form_class:
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(
@ -187,11 +191,21 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView
# 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):
context = super().get_context_data(**kwargs)
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)
@ -441,6 +455,8 @@ class ServiceInstanceUpdateView(
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()
@ -477,8 +493,19 @@ class ServiceInstanceUpdateView(
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()

View file

@ -1,11 +1,10 @@
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 CLIENT
from servala.core.odoo import create_helpdesk_ticket
from servala.frontend.forms.support import SupportForm
from servala.frontend.views.mixins import OrganizationViewMixin
@ -24,21 +23,16 @@ 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.
if organization.odoo_sale_order_id:
ticket_data["sale_order_id"] = organization.odoo_sale_order_id
CLIENT.execute("helpdesk.ticket", "create", [ticket_data])
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,
)
messages.success(
self.request,
_(

View file

@ -8,6 +8,7 @@ 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",
]

View file

@ -1,9 +1,21 @@
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`)
if (!nameField || !fqdnFieldContainer) return
const fqdnField = fqdnFieldContainer.querySelector('input.array-item-input');
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) => {
@ -14,9 +26,12 @@ const initializeFqdnGeneration = (prefix) => {
nameField.addEventListener('input', function() {
if (!fqdnField.dataset.manuallyEdited) {
fqdnField.value = generateFqdn(this.value);
const container = fqdnField.closest('.dynamic-array-widget');
if (container && window.updateHiddenInput) {
window.updateHiddenInput(container);
if (isArrayField) {
// Update hidden input for array fields
const container = fqdnField.closest('.dynamic-array-widget');
if (container && window.updateHiddenInput) {
window.updateHiddenInput(container);
}
}
}
});
@ -27,9 +42,12 @@ const initializeFqdnGeneration = (prefix) => {
if (nameField.value && !fqdnField.value) {
fqdnField.value = generateFqdn(nameField.value);
const container = fqdnField.closest('.dynamic-array-widget');
if (container && window.updateHiddenInput) {
window.updateHiddenInput(container);
if (isArrayField) {
// Update hidden input for array fields
const container = fqdnField.closest('.dynamic-array-widget');
if (container && window.updateHiddenInput) {
window.updateHiddenInput(container);
}
}
}
}

View file

@ -5,6 +5,12 @@ 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
@ -451,3 +457,290 @@ 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"]

View file

@ -10,28 +10,6 @@ from servala.core.forms import ServiceDefinitionAdminForm
from servala.core.models import ControlPlaneCRD
def test_custom_model_form_class_is_none_when_no_form_config():
crd = Mock(spec=ControlPlaneCRD)
service_def = Mock()
service_def.form_config = None
crd.service_definition = service_def
crd.django_model = Mock()
if not (
crd.django_model
and crd.service_definition
and crd.service_definition.form_config
and crd.service_definition.form_config.get("fieldsets")
):
result = None
else:
result = generate_custom_form_class(
crd.service_definition.form_config, crd.django_model
)
assert result is None
def test_custom_model_form_class_returns_class_when_form_config_exists():
crd = Mock(spec=ControlPlaneCRD)
@ -60,18 +38,9 @@ def test_custom_model_form_class_returns_class_when_form_config_exists():
app_label = "test"
crd.django_model = TestModel
if not (
crd.django_model
and crd.service_definition
and crd.service_definition.form_config
and crd.service_definition.form_config.get("fieldsets")
):
result = None
else:
result = generate_custom_form_class(
crd.service_definition.form_config, crd.django_model
)
result = generate_custom_form_class(
crd.service_definition.form_config, crd.django_model
)
assert result is not None
assert hasattr(result, "form_config")
@ -1084,3 +1053,43 @@ def test_empty_values_dont_override_default_configs():
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"

View file

@ -2,6 +2,7 @@ import pytest
from servala.core.models.service import CloudProvider, ServiceOffering
@pytest.mark.parametrize(
"url,redirect",
(

272
uv.lock generated
View file

@ -47,11 +47,11 @@ wheels = [
[[package]]
name = "asgiref"
version = "3.10.0"
version = "3.11.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/46/08/4dfec9b90758a59acc6be32ac82e98d1fbfc321cb5cfa410436dbacf821c/asgiref-3.10.0.tar.gz", hash = "sha256:d89f2d8cd8b56dada7d52fa7dc8075baa08fb836560710d38c292a7a3f78c04e", size = 37483, upload-time = "2025-10-05T09:15:06.557Z" }
sdist = { url = "https://files.pythonhosted.org/packages/76/b9/4db2509eabd14b4a8c71d1b24c8d5734c52b8560a7b1e1a8b56c8d25568b/asgiref-3.11.0.tar.gz", hash = "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4", size = 37969, upload-time = "2025-11-19T15:32:20.106Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/17/9c/fc2331f538fbf7eedba64b2052e99ccf9ba9d6888e2f41441ee28847004b/asgiref-3.10.0-py3-none-any.whl", hash = "sha256:aef8a81283a34d0ab31630c9b7dfe70c812c95eba78171367ca8745e88124734", size = 24050, upload-time = "2025-10-05T09:15:05.11Z" },
{ url = "https://files.pythonhosted.org/packages/91/be/317c2c55b8bbec407257d45f5c8d1b6867abc76d12043f2d3d58c538a4ea/asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d", size = 24096, upload-time = "2025-11-19T15:32:19.004Z" },
]
[[package]]
@ -86,30 +86,30 @@ wheels = [
[[package]]
name = "boto3"
version = "1.40.64"
version = "1.42.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "botocore" },
{ name = "jmespath" },
{ name = "s3transfer" },
]
sdist = { url = "https://files.pythonhosted.org/packages/08/d2/e508e5f42dc1c8a7412f5170751e626a18ed32c6e95c5df30bde6c5addf1/boto3-1.40.64.tar.gz", hash = "sha256:b92d6961c352f2bb8710c9892557d4b0e11258b70967d4e740e1c97375bcd779", size = 111543, upload-time = "2025-10-31T19:33:24.336Z" }
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" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/65/c2/27da558ceb90d17b1e4c0cca5dab29f8aea7f63242a1005a8f54230ce5e6/boto3-1.40.64-py3-none-any.whl", hash = "sha256:35ca3dd80dd90d5f4e8ed032440f28790696fdf50f48c0d16a09a75675f9112f", size = 139321, upload-time = "2025-10-31T19:33:22.92Z" },
{ 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" },
]
[[package]]
name = "botocore"
version = "1.40.64"
version = "1.41.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jmespath" },
{ name = "python-dateutil" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c1/15/109cb31c156a64bfaf4c809d2638fd95d8ba39b6deb7f1d0526c05257fd7/botocore-1.40.64.tar.gz", hash = "sha256:a13af4009f6912eafe32108f6fa584fb26e24375149836c2bcaaaaec9a7a9e58", size = 14409921, upload-time = "2025-10-31T19:33:12.291Z" }
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" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8f/c5/70bec18aef3fe9af63847d8766f81864b20daacd1dc7bf0c1d1ad90c7e98/botocore-1.40.64-py3-none-any.whl", hash = "sha256:6902b3dadfba1fbacc9648171bef3942530d8f823ff2bdb0e585a332323f89fc", size = 14072939, upload-time = "2025-10-31T19:33:09.081Z" },
{ 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" },
]
[[package]]
@ -129,20 +129,20 @@ wheels = [
[[package]]
name = "cachetools"
version = "6.2.1"
version = "6.2.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cc/7e/b975b5814bd36faf009faebe22c1072a1fa1168db34d285ef0ba071ad78c/cachetools-6.2.1.tar.gz", hash = "sha256:3f391e4bd8f8bf0931169baf7456cc822705f4e2a31f840d218f445b9a854201", size = 31325, upload-time = "2025-10-12T14:55:30.139Z" }
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" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/96/c5/1e741d26306c42e2bf6ab740b2202872727e0f606033c9dd713f8b93f5a8/cachetools-6.2.1-py3-none-any.whl", hash = "sha256:09868944b6dde876dfd44e1d47e18484541eaf12f26f29b7af91b26cc892d701", size = 11280, upload-time = "2025-10-12T14:55:28.382Z" },
{ 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" },
]
[[package]]
name = "certifi"
version = "2025.10.5"
version = "2025.11.12"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" }
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" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" },
{ 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" },
]
[[package]]
@ -205,14 +205,14 @@ wheels = [
[[package]]
name = "click"
version = "8.3.0"
version = "8.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" }
sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" },
{ url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
]
[[package]]
@ -226,37 +226,37 @@ wheels = [
[[package]]
name = "coverage"
version = "7.11.3"
version = "7.12.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d2/59/9698d57a3b11704c7b89b21d69e9d23ecf80d538cabb536c8b63f4a12322/coverage-7.11.3.tar.gz", hash = "sha256:0f59387f5e6edbbffec2281affb71cdc85e0776c1745150a3ab9b6c1d016106b", size = 815210, upload-time = "2025-11-10T00:13:17.18Z" }
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" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/84/d6/634ec396e45aded1772dccf6c236e3e7c9604bc47b816e928f32ce7987d1/coverage-7.11.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fdc5255eb4815babcdf236fa1a806ccb546724c8a9b129fd1ea4a5448a0bf07c", size = 216746, upload-time = "2025-11-10T00:12:23.089Z" },
{ url = "https://files.pythonhosted.org/packages/28/76/1079547f9d46f9c7c7d0dad35b6873c98bc5aa721eeabceafabd722cd5e7/coverage-7.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fe3425dc6021f906c6325d3c415e048e7cdb955505a94f1eb774dafc779ba203", size = 217077, upload-time = "2025-11-10T00:12:24.863Z" },
{ url = "https://files.pythonhosted.org/packages/2d/71/6ad80d6ae0d7cb743b9a98df8bb88b1ff3dc54491508a4a97549c2b83400/coverage-7.11.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4ca5f876bf41b24378ee67c41d688155f0e54cdc720de8ef9ad6544005899240", size = 248122, upload-time = "2025-11-10T00:12:26.553Z" },
{ url = "https://files.pythonhosted.org/packages/20/1d/784b87270784b0b88e4beec9d028e8d58f73ae248032579c63ad2ac6f69a/coverage-7.11.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9061a3e3c92b27fd8036dafa26f25d95695b6aa2e4514ab16a254f297e664f83", size = 250638, upload-time = "2025-11-10T00:12:28.555Z" },
{ url = "https://files.pythonhosted.org/packages/f5/26/b6dd31e23e004e9de84d1a8672cd3d73e50f5dae65dbd0f03fa2cdde6100/coverage-7.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abcea3b5f0dc44e1d01c27090bc32ce6ffb7aa665f884f1890710454113ea902", size = 251972, upload-time = "2025-11-10T00:12:30.246Z" },
{ url = "https://files.pythonhosted.org/packages/c9/ef/f9c64d76faac56b82daa036b34d4fe9ab55eb37f22062e68e9470583e688/coverage-7.11.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:68c4eb92997dbaaf839ea13527be463178ac0ddd37a7ac636b8bc11a51af2428", size = 248147, upload-time = "2025-11-10T00:12:32.195Z" },
{ url = "https://files.pythonhosted.org/packages/b6/eb/5b666f90a8f8053bd264a1ce693d2edef2368e518afe70680070fca13ecd/coverage-7.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:149eccc85d48c8f06547534068c41d69a1a35322deaa4d69ba1561e2e9127e75", size = 249995, upload-time = "2025-11-10T00:12:33.969Z" },
{ url = "https://files.pythonhosted.org/packages/eb/7b/871e991ffb5d067f8e67ffb635dabba65b231d6e0eb724a4a558f4a702a5/coverage-7.11.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:08c0bcf932e47795c49f0406054824b9d45671362dfc4269e0bc6e4bff010704", size = 247948, upload-time = "2025-11-10T00:12:36.341Z" },
{ url = "https://files.pythonhosted.org/packages/0a/8b/ce454f0af9609431b06dbe5485fc9d1c35ddc387e32ae8e374f49005748b/coverage-7.11.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:39764c6167c82d68a2d8c97c33dba45ec0ad9172570860e12191416f4f8e6e1b", size = 247770, upload-time = "2025-11-10T00:12:38.167Z" },
{ url = "https://files.pythonhosted.org/packages/61/8f/79002cb58a61dfbd2085de7d0a46311ef2476823e7938db80284cedd2428/coverage-7.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3224c7baf34e923ffc78cb45e793925539d640d42c96646db62dbd61bbcfa131", size = 249431, upload-time = "2025-11-10T00:12:40.354Z" },
{ url = "https://files.pythonhosted.org/packages/58/cc/d06685dae97468ed22999440f2f2f5060940ab0e7952a7295f236d98cce7/coverage-7.11.3-cp314-cp314-win32.whl", hash = "sha256:c713c1c528284d636cd37723b0b4c35c11190da6f932794e145fc40f8210a14a", size = 219508, upload-time = "2025-11-10T00:12:42.231Z" },
{ url = "https://files.pythonhosted.org/packages/5f/ed/770cd07706a3598c545f62d75adf2e5bd3791bffccdcf708ec383ad42559/coverage-7.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:c381a252317f63ca0179d2c7918e83b99a4ff3101e1b24849b999a00f9cd4f86", size = 220325, upload-time = "2025-11-10T00:12:44.065Z" },
{ url = "https://files.pythonhosted.org/packages/ee/ac/6a1c507899b6fb1b9a56069954365f655956bcc648e150ce64c2b0ecbed8/coverage-7.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:3e33a968672be1394eded257ec10d4acbb9af2ae263ba05a99ff901bb863557e", size = 218899, upload-time = "2025-11-10T00:12:46.18Z" },
{ url = "https://files.pythonhosted.org/packages/9a/58/142cd838d960cd740654d094f7b0300d7b81534bb7304437d2439fb685fb/coverage-7.11.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f9c96a29c6d65bd36a91f5634fef800212dff69dacdb44345c4c9783943ab0df", size = 217471, upload-time = "2025-11-10T00:12:48.392Z" },
{ url = "https://files.pythonhosted.org/packages/bc/2c/2f44d39eb33e41ab3aba80571daad32e0f67076afcf27cb443f9e5b5a3ee/coverage-7.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2ec27a7a991d229213c8070d31e3ecf44d005d96a9edc30c78eaeafaa421c001", size = 217742, upload-time = "2025-11-10T00:12:50.182Z" },
{ url = "https://files.pythonhosted.org/packages/32/76/8ebc66c3c699f4de3174a43424c34c086323cd93c4930ab0f835731c443a/coverage-7.11.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:72c8b494bd20ae1c58528b97c4a67d5cfeafcb3845c73542875ecd43924296de", size = 259120, upload-time = "2025-11-10T00:12:52.451Z" },
{ url = "https://files.pythonhosted.org/packages/19/89/78a3302b9595f331b86e4f12dfbd9252c8e93d97b8631500888f9a3a2af7/coverage-7.11.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:60ca149a446da255d56c2a7a813b51a80d9497a62250532598d249b3cdb1a926", size = 261229, upload-time = "2025-11-10T00:12:54.667Z" },
{ url = "https://files.pythonhosted.org/packages/07/59/1a9c0844dadef2a6efac07316d9781e6c5a3f3ea7e5e701411e99d619bfd/coverage-7.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb5069074db19a534de3859c43eec78e962d6d119f637c41c8e028c5ab3f59dd", size = 263642, upload-time = "2025-11-10T00:12:56.841Z" },
{ url = "https://files.pythonhosted.org/packages/37/86/66c15d190a8e82eee777793cabde730640f555db3c020a179625a2ad5320/coverage-7.11.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac5d5329c9c942bbe6295f4251b135d860ed9f86acd912d418dce186de7c19ac", size = 258193, upload-time = "2025-11-10T00:12:58.687Z" },
{ url = "https://files.pythonhosted.org/packages/c7/c7/4a4aeb25cb6f83c3ec4763e5f7cc78da1c6d4ef9e22128562204b7f39390/coverage-7.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e22539b676fafba17f0a90ac725f029a309eb6e483f364c86dcadee060429d46", size = 261107, upload-time = "2025-11-10T00:13:00.502Z" },
{ url = "https://files.pythonhosted.org/packages/ed/91/b986b5035f23cf0272446298967ecdd2c3c0105ee31f66f7e6b6948fd7f8/coverage-7.11.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2376e8a9c889016f25472c452389e98bc6e54a19570b107e27cde9d47f387b64", size = 258717, upload-time = "2025-11-10T00:13:02.747Z" },
{ url = "https://files.pythonhosted.org/packages/f0/c7/6c084997f5a04d050c513545d3344bfa17bd3b67f143f388b5757d762b0b/coverage-7.11.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4234914b8c67238a3c4af2bba648dc716aa029ca44d01f3d51536d44ac16854f", size = 257541, upload-time = "2025-11-10T00:13:04.689Z" },
{ url = "https://files.pythonhosted.org/packages/3b/c5/38e642917e406930cb67941210a366ccffa767365c8f8d9ec0f465a8b218/coverage-7.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f0b4101e2b3c6c352ff1f70b3a6fcc7c17c1ab1a91ccb7a33013cb0782af9820", size = 259872, upload-time = "2025-11-10T00:13:06.559Z" },
{ url = "https://files.pythonhosted.org/packages/b7/67/5e812979d20c167f81dbf9374048e0193ebe64c59a3d93d7d947b07865fa/coverage-7.11.3-cp314-cp314t-win32.whl", hash = "sha256:305716afb19133762e8cf62745c46c4853ad6f9eeba54a593e373289e24ea237", size = 220289, upload-time = "2025-11-10T00:13:08.635Z" },
{ url = "https://files.pythonhosted.org/packages/24/3a/b72573802672b680703e0df071faadfab7dcd4d659aaaffc4626bc8bbde8/coverage-7.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:9245bd392572b9f799261c4c9e7216bafc9405537d0f4ce3ad93afe081a12dc9", size = 221398, upload-time = "2025-11-10T00:13:10.734Z" },
{ url = "https://files.pythonhosted.org/packages/f8/4e/649628f28d38bad81e4e8eb3f78759d20ac173e3c456ac629123815feb40/coverage-7.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:9a1d577c20b4334e5e814c3d5fe07fa4a8c3ae42a601945e8d7940bab811d0bd", size = 219435, upload-time = "2025-11-10T00:13:12.712Z" },
{ url = "https://files.pythonhosted.org/packages/19/8f/92bdd27b067204b99f396a1414d6342122f3e2663459baf787108a6b8b84/coverage-7.11.3-py3-none-any.whl", hash = "sha256:351511ae28e2509c8d8cae5311577ea7dd511ab8e746ffc8814a0896c3d33fbe", size = 208478, upload-time = "2025-11-10T00:13:14.908Z" },
{ 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" },
]
[[package]]
@ -345,15 +345,15 @@ wheels = [
[[package]]
name = "django-allauth"
version = "65.13.0"
version = "65.13.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7c/05/36b9de6d0109948717ee0fa8076d5b57396bc838d5239f5b44b7d4c29fb0/django_allauth-65.13.0.tar.gz", hash = "sha256:7d7b7e7ad603eb3864c142f051e2cce7be2f9a9c6945a51172ec83d48c6c843b", size = 1987616, upload-time = "2025-10-31T10:20:03.954Z" }
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/ff/17/f2fd703781aeeb6d314059408df77360f09625cc3ce85f264b104443108c/django_allauth-65.13.0-py3-none-any.whl", hash = "sha256:119c0cf1cc2e0d1a0fe2f13588f30951d64989256084de2d60f13ab9308f9fa0", size = 1787213, upload-time = "2025-10-31T10:20:00.587Z" },
{ url = "https://files.pythonhosted.org/packages/d8/98/9d44ae1468abfdb521d651fb67f914165c7812dfdd97be16190c9b1cc246/django_allauth-65.13.1-py3-none-any.whl", hash = "sha256:2887294beedfd108b4b52ebd182e0ed373deaeb927fc5a22f77bbde3174704a6", size = 1787349, upload-time = "2025-11-20T16:34:37.354Z" },
]
[[package]]
@ -425,14 +425,14 @@ s3 = [
[[package]]
name = "django-template-partials"
version = "25.2"
version = "25.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
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" }
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" }
wheels = [
{ 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" },
{ 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" },
]
[[package]]
@ -489,40 +489,40 @@ wheels = [
[[package]]
name = "flake8-bugbear"
version = "25.10.21"
version = "25.11.29"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "flake8" },
]
sdist = { url = "https://files.pythonhosted.org/packages/30/54/0f6e431adbc67fd420540e386cb20b57e73e8aeb393f0ae2311e91b4548f/flake8_bugbear-25.10.21.tar.gz", hash = "sha256:2876afcaed8bfb3464cf33e3ec42cc3bec0a004165b84400dc3392b0547c2714", size = 83080, upload-time = "2025-10-22T01:27:03.63Z" }
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" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/09/0e/8ba976f7d477cad69cc7af08dc7b0163181a5e19a82fe721f954e369c067/flake8_bugbear-25.10.21-py3-none-any.whl", hash = "sha256:f1c5654f9d9d3e62e90da1f0335551fdbc565c51749713177dbcfb9edb105405", size = 37257, upload-time = "2025-10-22T01:27:02.105Z" },
{ 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" },
]
[[package]]
name = "flake8-pyproject"
version = "1.2.3"
version = "1.2.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "flake8" },
]
wheels = [
{ 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" },
{ 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" },
]
[[package]]
name = "google-auth"
version = "2.42.1"
version = "2.43.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cachetools" },
{ name = "pyasn1-modules" },
{ name = "rsa" },
]
sdist = { url = "https://files.pythonhosted.org/packages/25/6b/22a77135757c3a7854c9f008ffed6bf4e8851616d77faf13147e9ab5aae6/google_auth-2.42.1.tar.gz", hash = "sha256:30178b7a21aa50bffbdc1ffcb34ff770a2f65c712170ecd5446c4bef4dc2b94e", size = 295541, upload-time = "2025-10-30T16:42:19.381Z" }
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" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/92/05/adeb6c495aec4f9d93f9e2fc29eeef6e14d452bba11d15bdb874ce1d5b10/google_auth-2.42.1-py2.py3-none-any.whl", hash = "sha256:eb73d71c91fc95dbd221a2eb87477c278a355e7367a35c0d84e6b0e5f9b4ad11", size = 222550, upload-time = "2025-10-30T16:42:17.878Z" },
{ 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" },
]
[[package]]
@ -937,38 +937,38 @@ wheels = [
[[package]]
name = "regex"
version = "2025.10.23"
version = "2025.11.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f8/c8/1d2160d36b11fbe0a61acb7c3c81ab032d9ec8ad888ac9e0a61b85ab99dd/regex-2025.10.23.tar.gz", hash = "sha256:8cbaf8ceb88f96ae2356d01b9adf5e6306fa42fa6f7eab6b97794e37c959ac26", size = 401266, upload-time = "2025-10-21T15:58:20.23Z" }
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" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/73/f6/0caf29fec943f201fbc8822879c99d31e59c1d51a983d9843ee5cf398539/regex-2025.10.23-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5b5cb5b6344c4c4c24b2dc87b0bfee78202b07ef7633385df70da7fcf6f7cec6", size = 488960, upload-time = "2025-10-21T15:56:40.849Z" },
{ url = "https://files.pythonhosted.org/packages/8e/7d/ebb7085b8fa31c24ce0355107cea2b92229d9050552a01c5d291c42aecea/regex-2025.10.23-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a6ce7973384c37bdf0f371a843f95a6e6f4e1489e10e0cf57330198df72959c5", size = 290932, upload-time = "2025-10-21T15:56:42.875Z" },
{ url = "https://files.pythonhosted.org/packages/27/41/43906867287cbb5ca4cee671c3cc8081e15deef86a8189c3aad9ac9f6b4d/regex-2025.10.23-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2ee3663f2c334959016b56e3bd0dd187cbc73f948e3a3af14c3caaa0c3035d10", size = 288766, upload-time = "2025-10-21T15:56:44.894Z" },
{ url = "https://files.pythonhosted.org/packages/ab/9e/ea66132776700fc77a39b1056e7a5f1308032fead94507e208dc6716b7cd/regex-2025.10.23-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2003cc82a579107e70d013482acce8ba773293f2db534fb532738395c557ff34", size = 798884, upload-time = "2025-10-21T15:56:47.178Z" },
{ url = "https://files.pythonhosted.org/packages/d5/99/aed1453687ab63819a443930770db972c5c8064421f0d9f5da9ad029f26b/regex-2025.10.23-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:182c452279365a93a9f45874f7f191ec1c51e1f1eb41bf2b16563f1a40c1da3a", size = 864768, upload-time = "2025-10-21T15:56:49.793Z" },
{ url = "https://files.pythonhosted.org/packages/99/5d/732fe747a1304805eb3853ce6337eea16b169f7105a0d0dd9c6a5ffa9948/regex-2025.10.23-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b1249e9ff581c5b658c8f0437f883b01f1edcf424a16388591e7c05e5e9e8b0c", size = 911394, upload-time = "2025-10-21T15:56:52.186Z" },
{ url = "https://files.pythonhosted.org/packages/5e/48/58a1f6623466522352a6efa153b9a3714fc559d9f930e9bc947b4a88a2c3/regex-2025.10.23-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b841698f93db3ccc36caa1900d2a3be281d9539b822dc012f08fc80b46a3224", size = 803145, upload-time = "2025-10-21T15:56:55.142Z" },
{ url = "https://files.pythonhosted.org/packages/ea/f6/7dea79be2681a5574ab3fc237aa53b2c1dfd6bd2b44d4640b6c76f33f4c1/regex-2025.10.23-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:956d89e0c92d471e8f7eee73f73fdff5ed345886378c45a43175a77538a1ffe4", size = 787831, upload-time = "2025-10-21T15:56:57.203Z" },
{ url = "https://files.pythonhosted.org/packages/3a/ad/07b76950fbbe65f88120ca2d8d845047c401450f607c99ed38862904671d/regex-2025.10.23-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5c259cb363299a0d90d63b5c0d7568ee98419861618a95ee9d91a41cb9954462", size = 859162, upload-time = "2025-10-21T15:56:59.195Z" },
{ url = "https://files.pythonhosted.org/packages/41/87/374f3b2021b22aa6a4fc0b750d63f9721e53d1631a238f7a1c343c1cd288/regex-2025.10.23-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:185d2b18c062820b3a40d8fefa223a83f10b20a674bf6e8c4a432e8dfd844627", size = 849899, upload-time = "2025-10-21T15:57:01.747Z" },
{ url = "https://files.pythonhosted.org/packages/12/4a/7f7bb17c5a5a9747249807210e348450dab9212a46ae6d23ebce86ba6a2b/regex-2025.10.23-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:281d87fa790049c2b7c1b4253121edd80b392b19b5a3d28dc2a77579cb2a58ec", size = 789372, upload-time = "2025-10-21T15:57:04.018Z" },
{ url = "https://files.pythonhosted.org/packages/c9/dd/9c7728ff544fea09bbc8635e4c9e7c423b11c24f1a7a14e6ac4831466709/regex-2025.10.23-cp314-cp314-win32.whl", hash = "sha256:63b81eef3656072e4ca87c58084c7a9c2b81d41a300b157be635a8a675aacfb8", size = 271451, upload-time = "2025-10-21T15:57:06.266Z" },
{ url = "https://files.pythonhosted.org/packages/48/f8/ef7837ff858eb74079c4804c10b0403c0b740762e6eedba41062225f7117/regex-2025.10.23-cp314-cp314-win_amd64.whl", hash = "sha256:0967c5b86f274800a34a4ed862dfab56928144d03cb18821c5153f8777947796", size = 280173, upload-time = "2025-10-21T15:57:08.206Z" },
{ url = "https://files.pythonhosted.org/packages/8e/d0/d576e1dbd9885bfcd83d0e90762beea48d9373a6f7ed39170f44ed22e336/regex-2025.10.23-cp314-cp314-win_arm64.whl", hash = "sha256:c70dfe58b0a00b36aa04cdb0f798bf3e0adc31747641f69e191109fd8572c9a9", size = 273206, upload-time = "2025-10-21T15:57:10.367Z" },
{ url = "https://files.pythonhosted.org/packages/a6/d0/2025268315e8b2b7b660039824cb7765a41623e97d4cd421510925400487/regex-2025.10.23-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1f5799ea1787aa6de6c150377d11afad39a38afd033f0c5247aecb997978c422", size = 491854, upload-time = "2025-10-21T15:57:12.526Z" },
{ url = "https://files.pythonhosted.org/packages/44/35/5681c2fec5e8b33454390af209c4353dfc44606bf06d714b0b8bd0454ffe/regex-2025.10.23-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a9639ab7540cfea45ef57d16dcbea2e22de351998d614c3ad2f9778fa3bdd788", size = 292542, upload-time = "2025-10-21T15:57:15.158Z" },
{ url = "https://files.pythonhosted.org/packages/5d/17/184eed05543b724132e4a18149e900f5189001fcfe2d64edaae4fbaf36b4/regex-2025.10.23-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:08f52122c352eb44c3421dab78b9b73a8a77a282cc8314ae576fcaa92b780d10", size = 290903, upload-time = "2025-10-21T15:57:17.108Z" },
{ url = "https://files.pythonhosted.org/packages/25/d0/5e3347aa0db0de382dddfa133a7b0ae72f24b4344f3989398980b44a3924/regex-2025.10.23-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebf1baebef1c4088ad5a5623decec6b52950f0e4d7a0ae4d48f0a99f8c9cb7d7", size = 807546, upload-time = "2025-10-21T15:57:19.179Z" },
{ url = "https://files.pythonhosted.org/packages/d2/bb/40c589bbdce1be0c55e9f8159789d58d47a22014f2f820cf2b517a5cd193/regex-2025.10.23-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:16b0f1c2e2d566c562d5c384c2b492646be0a19798532fdc1fdedacc66e3223f", size = 873322, upload-time = "2025-10-21T15:57:21.36Z" },
{ url = "https://files.pythonhosted.org/packages/fe/56/a7e40c01575ac93360e606278d359f91829781a9f7fb6e5aa435039edbda/regex-2025.10.23-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7ada5d9dceafaab92646aa00c10a9efd9b09942dd9b0d7c5a4b73db92cc7e61", size = 914855, upload-time = "2025-10-21T15:57:24.044Z" },
{ url = "https://files.pythonhosted.org/packages/5c/4b/d55587b192763db3163c3f508b3b67b31bb6f5e7a0e08b83013d0a59500a/regex-2025.10.23-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a36b4005770044bf08edecc798f0e41a75795b9e7c9c12fe29da8d792ef870c", size = 812724, upload-time = "2025-10-21T15:57:26.123Z" },
{ url = "https://files.pythonhosted.org/packages/33/20/18bac334955fbe99d17229f4f8e98d05e4a501ac03a442be8facbb37c304/regex-2025.10.23-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:af7b2661dcc032da1fae82069b5ebf2ac1dfcd5359ef8b35e1367bfc92181432", size = 795439, upload-time = "2025-10-21T15:57:28.497Z" },
{ url = "https://files.pythonhosted.org/packages/67/46/c57266be9df8549c7d85deb4cb82280cb0019e46fff677534c5fa1badfa4/regex-2025.10.23-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:1cb976810ac1416a67562c2e5ba0accf6f928932320fef302e08100ed681b38e", size = 868336, upload-time = "2025-10-21T15:57:30.867Z" },
{ url = "https://files.pythonhosted.org/packages/b8/f3/bd5879e41ef8187fec5e678e94b526a93f99e7bbe0437b0f2b47f9101694/regex-2025.10.23-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:1a56a54be3897d62f54290190fbcd754bff6932934529fbf5b29933da28fcd43", size = 854567, upload-time = "2025-10-21T15:57:33.062Z" },
{ url = "https://files.pythonhosted.org/packages/e6/57/2b6bbdbd2f24dfed5b028033aa17ad8f7d86bb28f1a892cac8b3bc89d059/regex-2025.10.23-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8f3e6d202fb52c2153f532043bbcf618fd177df47b0b306741eb9b60ba96edc3", size = 799565, upload-time = "2025-10-21T15:57:35.153Z" },
{ url = "https://files.pythonhosted.org/packages/c7/ba/a6168f542ba73b151ed81237adf6b869c7b2f7f8d51618111296674e20ee/regex-2025.10.23-cp314-cp314t-win32.whl", hash = "sha256:1fa1186966b2621b1769fd467c7b22e317e6ba2d2cdcecc42ea3089ef04a8521", size = 274428, upload-time = "2025-10-21T15:57:37.996Z" },
{ url = "https://files.pythonhosted.org/packages/ef/a0/c84475e14a2829e9b0864ebf77c3f7da909df9d8acfe2bb540ff0072047c/regex-2025.10.23-cp314-cp314t-win_amd64.whl", hash = "sha256:08a15d40ce28362eac3e78e83d75475147869c1ff86bc93285f43b4f4431a741", size = 284140, upload-time = "2025-10-21T15:57:40.027Z" },
{ url = "https://files.pythonhosted.org/packages/51/33/6a08ade0eee5b8ba79386869fa6f77afeb835b60510f3525db987e2fffc4/regex-2025.10.23-cp314-cp314t-win_arm64.whl", hash = "sha256:a93e97338e1c8ea2649e130dcfbe8cd69bba5e1e163834752ab64dcb4de6d5ed", size = 274497, upload-time = "2025-10-21T15:57:42.389Z" },
{ 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" },
]
[[package]]
@ -1001,39 +1001,39 @@ wheels = [
[[package]]
name = "rpds-py"
version = "0.28.0"
version = "0.30.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/48/dc/95f074d43452b3ef5d06276696ece4b3b5d696e7c9ad7173c54b1390cd70/rpds_py-0.28.0.tar.gz", hash = "sha256:abd4df20485a0983e2ca334a216249b6186d6e3c1627e106651943dbdb791aea", size = 27419, upload-time = "2025-10-22T22:24:29.327Z" }
sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/08/47/ffe8cd7a6a02833b10623bf765fbb57ce977e9a4318ca0e8cf97e9c3d2b3/rpds_py-0.28.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:dcdcb890b3ada98a03f9f2bb108489cdc7580176cb73b4f2d789e9a1dac1d472", size = 353830, upload-time = "2025-10-22T22:23:17.03Z" },
{ url = "https://files.pythonhosted.org/packages/f9/9f/890f36cbd83a58491d0d91ae0db1702639edb33fb48eeb356f80ecc6b000/rpds_py-0.28.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f274f56a926ba2dc02976ca5b11c32855cbd5925534e57cfe1fda64e04d1add2", size = 341819, upload-time = "2025-10-22T22:23:18.57Z" },
{ url = "https://files.pythonhosted.org/packages/09/e3/921eb109f682aa24fb76207698fbbcf9418738f35a40c21652c29053f23d/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fe0438ac4a29a520ea94c8c7f1754cdd8feb1bc490dfda1bfd990072363d527", size = 373127, upload-time = "2025-10-22T22:23:20.216Z" },
{ url = "https://files.pythonhosted.org/packages/23/13/bce4384d9f8f4989f1a9599c71b7a2d877462e5fd7175e1f69b398f729f4/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8a358a32dd3ae50e933347889b6af9a1bdf207ba5d1a3f34e1a38cd3540e6733", size = 382767, upload-time = "2025-10-22T22:23:21.787Z" },
{ url = "https://files.pythonhosted.org/packages/23/e1/579512b2d89a77c64ccef5a0bc46a6ef7f72ae0cf03d4b26dcd52e57ee0a/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e80848a71c78aa328fefaba9c244d588a342c8e03bda518447b624ea64d1ff56", size = 517585, upload-time = "2025-10-22T22:23:23.699Z" },
{ url = "https://files.pythonhosted.org/packages/62/3c/ca704b8d324a2591b0b0adcfcaadf9c862375b11f2f667ac03c61b4fd0a6/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f586db2e209d54fe177e58e0bc4946bea5fb0102f150b1b2f13de03e1f0976f8", size = 399828, upload-time = "2025-10-22T22:23:25.713Z" },
{ url = "https://files.pythonhosted.org/packages/da/37/e84283b9e897e3adc46b4c88bb3f6ec92a43bd4d2f7ef5b13459963b2e9c/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ae8ee156d6b586e4292491e885d41483136ab994e719a13458055bec14cf370", size = 375509, upload-time = "2025-10-22T22:23:27.32Z" },
{ url = "https://files.pythonhosted.org/packages/1a/c2/a980beab869d86258bf76ec42dec778ba98151f253a952b02fe36d72b29c/rpds_py-0.28.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a805e9b3973f7e27f7cab63a6b4f61d90f2e5557cff73b6e97cd5b8540276d3d", size = 392014, upload-time = "2025-10-22T22:23:29.332Z" },
{ url = "https://files.pythonhosted.org/packages/da/b5/b1d3c5f9d3fa5aeef74265f9c64de3c34a0d6d5cd3c81c8b17d5c8f10ed4/rpds_py-0.28.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5d3fd16b6dc89c73a4da0b4ac8b12a7ecc75b2864b95c9e5afed8003cb50a728", size = 402410, upload-time = "2025-10-22T22:23:31.14Z" },
{ url = "https://files.pythonhosted.org/packages/74/ae/cab05ff08dfcc052afc73dcb38cbc765ffc86f94e966f3924cd17492293c/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6796079e5d24fdaba6d49bda28e2c47347e89834678f2bc2c1b4fc1489c0fb01", size = 553593, upload-time = "2025-10-22T22:23:32.834Z" },
{ url = "https://files.pythonhosted.org/packages/70/80/50d5706ea2a9bfc9e9c5f401d91879e7c790c619969369800cde202da214/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:76500820c2af232435cbe215e3324c75b950a027134e044423f59f5b9a1ba515", size = 576925, upload-time = "2025-10-22T22:23:34.47Z" },
{ url = "https://files.pythonhosted.org/packages/ab/12/85a57d7a5855a3b188d024b099fd09c90db55d32a03626d0ed16352413ff/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bbdc5640900a7dbf9dd707fe6388972f5bbd883633eb68b76591044cfe346f7e", size = 542444, upload-time = "2025-10-22T22:23:36.093Z" },
{ url = "https://files.pythonhosted.org/packages/6c/65/10643fb50179509150eb94d558e8837c57ca8b9adc04bd07b98e57b48f8c/rpds_py-0.28.0-cp314-cp314-win32.whl", hash = "sha256:adc8aa88486857d2b35d75f0640b949759f79dc105f50aa2c27816b2e0dd749f", size = 207968, upload-time = "2025-10-22T22:23:37.638Z" },
{ url = "https://files.pythonhosted.org/packages/b4/84/0c11fe4d9aaea784ff4652499e365963222481ac647bcd0251c88af646eb/rpds_py-0.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:66e6fa8e075b58946e76a78e69e1a124a21d9a48a5b4766d15ba5b06869d1fa1", size = 218876, upload-time = "2025-10-22T22:23:39.179Z" },
{ url = "https://files.pythonhosted.org/packages/0f/e0/3ab3b86ded7bb18478392dc3e835f7b754cd446f62f3fc96f4fe2aca78f6/rpds_py-0.28.0-cp314-cp314-win_arm64.whl", hash = "sha256:a6fe887c2c5c59413353b7c0caff25d0e566623501ccfff88957fa438a69377d", size = 212506, upload-time = "2025-10-22T22:23:40.755Z" },
{ url = "https://files.pythonhosted.org/packages/51/ec/d5681bb425226c3501eab50fc30e9d275de20c131869322c8a1729c7b61c/rpds_py-0.28.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7a69df082db13c7070f7b8b1f155fa9e687f1d6aefb7b0e3f7231653b79a067b", size = 355433, upload-time = "2025-10-22T22:23:42.259Z" },
{ url = "https://files.pythonhosted.org/packages/be/ec/568c5e689e1cfb1ea8b875cffea3649260955f677fdd7ddc6176902d04cd/rpds_py-0.28.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b1cde22f2c30ebb049a9e74c5374994157b9b70a16147d332f89c99c5960737a", size = 342601, upload-time = "2025-10-22T22:23:44.372Z" },
{ url = "https://files.pythonhosted.org/packages/32/fe/51ada84d1d2a1d9d8f2c902cfddd0133b4a5eb543196ab5161d1c07ed2ad/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5338742f6ba7a51012ea470bd4dc600a8c713c0c72adaa0977a1b1f4327d6592", size = 372039, upload-time = "2025-10-22T22:23:46.025Z" },
{ url = "https://files.pythonhosted.org/packages/07/c1/60144a2f2620abade1a78e0d91b298ac2d9b91bc08864493fa00451ef06e/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1460ebde1bcf6d496d80b191d854adedcc619f84ff17dc1c6d550f58c9efbba", size = 382407, upload-time = "2025-10-22T22:23:48.098Z" },
{ url = "https://files.pythonhosted.org/packages/45/ed/091a7bbdcf4038a60a461df50bc4c82a7ed6d5d5e27649aab61771c17585/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e3eb248f2feba84c692579257a043a7699e28a77d86c77b032c1d9fbb3f0219c", size = 518172, upload-time = "2025-10-22T22:23:50.16Z" },
{ url = "https://files.pythonhosted.org/packages/54/dd/02cc90c2fd9c2ef8016fd7813bfacd1c3a1325633ec8f244c47b449fc868/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3bbba5def70b16cd1c1d7255666aad3b290fbf8d0fe7f9f91abafb73611a91", size = 399020, upload-time = "2025-10-22T22:23:51.81Z" },
{ url = "https://files.pythonhosted.org/packages/ab/81/5d98cc0329bbb911ccecd0b9e19fbf7f3a5de8094b4cda5e71013b2dd77e/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3114f4db69ac5a1f32e7e4d1cbbe7c8f9cf8217f78e6e002cedf2d54c2a548ed", size = 377451, upload-time = "2025-10-22T22:23:53.711Z" },
{ url = "https://files.pythonhosted.org/packages/b4/07/4d5bcd49e3dfed2d38e2dcb49ab6615f2ceb9f89f5a372c46dbdebb4e028/rpds_py-0.28.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4b0cb8a906b1a0196b863d460c0222fb8ad0f34041568da5620f9799b83ccf0b", size = 390355, upload-time = "2025-10-22T22:23:55.299Z" },
{ url = "https://files.pythonhosted.org/packages/3f/79/9f14ba9010fee74e4f40bf578735cfcbb91d2e642ffd1abe429bb0b96364/rpds_py-0.28.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf681ac76a60b667106141e11a92a3330890257e6f559ca995fbb5265160b56e", size = 403146, upload-time = "2025-10-22T22:23:56.929Z" },
{ url = "https://files.pythonhosted.org/packages/39/4c/f08283a82ac141331a83a40652830edd3a4a92c34e07e2bbe00baaea2f5f/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1e8ee6413cfc677ce8898d9cde18cc3a60fc2ba756b0dec5b71eb6eb21c49fa1", size = 552656, upload-time = "2025-10-22T22:23:58.62Z" },
{ url = "https://files.pythonhosted.org/packages/61/47/d922fc0666f0dd8e40c33990d055f4cc6ecff6f502c2d01569dbed830f9b/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b3072b16904d0b5572a15eb9d31c1954e0d3227a585fc1351aa9878729099d6c", size = 576782, upload-time = "2025-10-22T22:24:00.312Z" },
{ url = "https://files.pythonhosted.org/packages/d3/0c/5bafdd8ccf6aa9d3bfc630cfece457ff5b581af24f46a9f3590f790e3df2/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b670c30fd87a6aec281c3c9896d3bae4b205fd75d79d06dc87c2503717e46092", size = 544671, upload-time = "2025-10-22T22:24:02.297Z" },
{ url = "https://files.pythonhosted.org/packages/2c/37/dcc5d8397caa924988693519069d0beea077a866128719351a4ad95e82fc/rpds_py-0.28.0-cp314-cp314t-win32.whl", hash = "sha256:8014045a15b4d2b3476f0a287fcc93d4f823472d7d1308d47884ecac9e612be3", size = 205749, upload-time = "2025-10-22T22:24:03.848Z" },
{ url = "https://files.pythonhosted.org/packages/d7/69/64d43b21a10d72b45939a28961216baeb721cc2a430f5f7c3bfa21659a53/rpds_py-0.28.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7a4e59c90d9c27c561eb3160323634a9ff50b04e4f7820600a2beb0ac90db578", size = 216233, upload-time = "2025-10-22T22:24:05.471Z" },
{ 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" },
]
[[package]]
@ -1059,27 +1059,27 @@ wheels = [
[[package]]
name = "s3transfer"
version = "0.14.0"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "botocore" },
]
sdist = { url = "https://files.pythonhosted.org/packages/62/74/8d69dcb7a9efe8baa2046891735e5dfe433ad558ae23d9e3c14c633d1d58/s3transfer-0.14.0.tar.gz", hash = "sha256:eff12264e7c8b4985074ccce27a3b38a485bb7f7422cc8046fee9be4983e4125", size = 151547, upload-time = "2025-09-09T19:23:31.089Z" }
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" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/48/f0/ae7ca09223a81a1d890b2557186ea015f6e0502e9b8cb8e1813f1d8cfa4e/s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456", size = 85712, upload-time = "2025-09-09T19:23:30.041Z" },
{ 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" },
]
[[package]]
name = "sentry-sdk"
version = "2.44.0"
version = "2.46.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/62/26/ff7d93a14a0ec309021dca2fb7c62669d4f6f5654aa1baf60797a16681e0/sentry_sdk-2.44.0.tar.gz", hash = "sha256:5b1fe54dfafa332e900b07dd8f4dfe35753b64e78e7d9b1655a28fd3065e2493", size = 371464, upload-time = "2025-11-11T09:35:56.075Z" }
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" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a8/56/c16bda4d53012c71fa1b588edde603c6b455bc8206bf6de7b83388fcce75/sentry_sdk-2.44.0-py2.py3-none-any.whl", hash = "sha256:9e36a0372b881e8f92fdbff4564764ce6cec4b7f25424d0a3a8d609c9e4651a7", size = 402352, upload-time = "2025-11-11T09:35:54.1Z" },
{ 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" },
]
[package.optional-dependencies]
@ -1134,13 +1134,13 @@ 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.0" },
{ name = "django-allauth", specifier = ">=65.13.1" },
{ name = "django-auditlog", specifier = ">=3.3.0" },
{ name = "django-fernet-encrypted-fields", specifier = ">=0.3.1" },
{ 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.2" },
{ name = "django-template-partials", specifier = ">=25.3" },
{ name = "jsonschema", specifier = ">=4.25.1" },
{ name = "kubernetes", specifier = ">=34.1.0" },
{ name = "pillow", specifier = ">=12.0.0" },
@ -1148,7 +1148,7 @@ requires-dist = [
{ name = "pyjwt", specifier = ">=2.10.1" },
{ name = "requests", specifier = ">=2.32.5" },
{ name = "rules", specifier = ">=3.5" },
{ name = "sentry-sdk", extras = ["django"], specifier = ">=2.44.0" },
{ name = "sentry-sdk", extras = ["django"], specifier = ">=2.46.0" },
{ name = "urlman", specifier = ">=2.0.2" },
]
@ -1156,10 +1156,10 @@ requires-dist = [
dev = [
{ name = "black", specifier = ">=25.11.0" },
{ name = "bumpver", specifier = ">=2025.1131" },
{ name = "coverage", specifier = ">=7.11.3" },
{ name = "coverage", specifier = ">=7.12.0" },
{ name = "djlint", specifier = ">=1.36.4" },
{ name = "flake8", specifier = ">=7.3.0" },
{ name = "flake8-bugbear", specifier = ">=25.10.21" },
{ name = "flake8-bugbear", specifier = ">=25.11.29" },
{ name = "flake8-pyproject", specifier = ">=1.2.3" },
{ name = "isort", specifier = ">=7.0.0" },
{ name = "pytest", specifier = ">=9.0.1" },
@ -1179,11 +1179,11 @@ wheels = [
[[package]]
name = "sqlparse"
version = "0.5.3"
version = "0.5.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e5/40/edede8dd6977b0d3da179a342c198ed100dd2aba4be081861ee5911e4da4/sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", size = 84999, upload-time = "2024-12-10T12:05:30.728Z" }
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" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415, upload-time = "2024-12-10T12:05:27.824Z" },
{ url = "https://files.pythonhosted.org/packages/25/70/001ee337f7aa888fb2e3f5fd7592a6afc5283adb1ed44ce8df5764070f22/sqlparse-0.5.4-py3-none-any.whl", hash = "sha256:99a9f0314977b76d776a0fcb8554de91b9bb8a18560631d6bc48721d07023dcb", size = 45933, upload-time = "2025-11-28T07:10:19.73Z" },
]
[[package]]