From 9dcaa135698fabb24be6fc4607d22fbf97ed489a Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 26 Sep 2025 08:13:31 +0200 Subject: [PATCH 001/185] fix links to exoscale docs --- docs/modules/ROOT/pages/exoscale-osb.adoc | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/modules/ROOT/pages/exoscale-osb.adoc b/docs/modules/ROOT/pages/exoscale-osb.adoc index 540d5df..66c3963 100644 --- a/docs/modules/ROOT/pages/exoscale-osb.adoc +++ b/docs/modules/ROOT/pages/exoscale-osb.adoc @@ -59,7 +59,7 @@ This Organization will be tied to Exoscale services, it can only see services fr Exoscale uses the OpenServiceBroker (OSB) API for service provisioning and deprovisioning with external vendors, that's why we're integrating this way. -The API currently https://community.exoscale.com/documentation/vendor/marketplace-managed-services-provision/#open-service-broker-api-osbapi[has the following features^]: +The API currently https://community.exoscale.com/vendor/marketplace-managed-svc-prov/#open-service-broker-api-osbapi[has the following features^]: * Service instance provisioning * Service instance update (plan changes, suspensions, user sync) @@ -155,7 +155,7 @@ https://github.com/openservicebrokerapi/servicebroker/blob/master/spec.md#respon Sources: -* https://community.exoscale.com/documentation/vendor/marketplace-managed-services-provision/#provisioning[Exoscale docs - Provisioning^] +* https://community.exoscale.com/vendor/marketplace-managed-svc-prov/#provisioning[Exoscale docs - Provisioning^] * https://github.com/openservicebrokerapi/servicebroker/blob/master/spec.md#provisioning[OSB API Spec^] In the Servala Portal an Organization is created by the OSB API if it doesn't exist yet. @@ -222,7 +222,7 @@ https://github.com/openservicebrokerapi/servicebroker/blob/master/spec.md#respon Sources: -* https://community.exoscale.com/documentation/vendor/marketplace-managed-services-provision/#service-instance-update[Exoscale docs - Service Instance Update^] +* https://community.exoscale.com/vendor/marketplace-managed-svc-prov/#service-instance-update[Exoscale docs - Service Instance Update^] * https://github.com/openservicebrokerapi/servicebroker/blob/master/spec.md#updating-a-service-instance[OSB API Spec^] When the suspension plan is triggered, we send an E-Mail to customers@vshn.ch with all the information we have, so that we can check back with Exoscale what to do. @@ -245,7 +245,7 @@ sequenceDiagram participant VCA as Servala OSB API participant CCP as Servala Portal participant CP as Control Plane - + EU->>EP: Disable VSHN Service EP->>VCA: OSB API "DELETE" VCA->>CCP: Disable Service @@ -268,7 +268,7 @@ https://github.com/openservicebrokerapi/servicebroker/blob/master/spec.md#respon Sources: -* https://community.exoscale.com/documentation/vendor/marketplace-managed-services-provision/#deprovisioning[Exoscale docs - Deprovisioning^] +* https://community.exoscale.com/vendor/marketplace-managed-svc-prov/#de-provisioning[Exoscale docs - Deprovisioning^] * https://github.com/openservicebrokerapi/servicebroker/blob/master/spec.md#deprovisioning[OSB API Spec^] When all services are deleted (none exists anymore), an email is sent to customer@vshn.ch for the final closure of the organization. @@ -280,7 +280,7 @@ See also <>, which details the single service deprovisioning. === User Synchronization -We don't do https://community.exoscale.com/documentation/vendor/marketplace-managed-services-provision/#user-sync[user synchronization^] from Exoscale to VSHN. +We don't do https://community.exoscale.com/vendor/marketplace-managed-svc-prov/#user-sync[user synchronization^] from Exoscale to VSHN. ____ When user sync is disabled, only the information of the user that made the product purchase will be provided. The information will never be updated. @@ -410,6 +410,6 @@ POST /orgs/:uuid/usage <1> * https://kb.vshn.ch/appuio-cloud/references/architecture/invitations.html[APPUiO Invitations] * https://github.com/vshn/crossplane-service-broker[Crossplane Service Broker (Code)^] - https://kb.vshn.ch/app-catalog/csp/spks/crossplane/overview.html[Crossplane Service Broker (Docs)^] * https://github.com/vshn/swisscom-service-broker[Swisscom Service Broker^] -* https://community.exoscale.com/documentation/vendor/marketplace-managed-services/[Exoscale Vendor Documentation - Managed Services^] -* https://community.exoscale.com/documentation/vendor/marketplace-managed-services-billing/[Exoscale Vendor Documentation - Managed Services Billing^] -* https://community.exoscale.com/documentation/vendor/marketplace-managed-services-provision/[Exoscale Vendor Documentation - Managed Services Provisioning^] +* https://community.exoscale.com/vendor/marketplace-managed-svc/[Exoscale Vendor Documentation - Managed Services^] +* https://community.exoscale.com/vendor/marketplace-managed-svc-bill/[Exoscale Vendor Documentation - Managed Services Billing^] +* https://community.exoscale.com/vendor/marketplace-managed-svc-prov/[Exoscale Vendor Documentation - Managed Services Provisioning^] From a4ff4ecfdcbf6b4567143e7f3b75b98fed9f7c09 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 26 Sep 2025 08:50:00 +0200 Subject: [PATCH 002/185] use objectstorage on prod --- .../production/portal-deployment.yaml | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/deployment/kustomize/overlays/production/portal-deployment.yaml b/deployment/kustomize/overlays/production/portal-deployment.yaml index ce8c03a..bc6050f 100644 --- a/deployment/kustomize/overlays/production/portal-deployment.yaml +++ b/deployment/kustomize/overlays/production/portal-deployment.yaml @@ -12,3 +12,23 @@ spec: value: production - name: SERVALA_ALLOWED_HOSTS value: portal.servala.com + - name: SERVALA_STORAGE_BUCKET_NAME + valueFrom: + secretKeyRef: + name: portal-storage-creds + key: BUCKET_NAME + - name: SERVALA_S3_ENDPOINT_URL + valueFrom: + secretKeyRef: + name: portal-storage-creds + key: ENDPOINT_URL + - name: SERVALA_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: portal-storage-creds + key: AWS_ACCESS_KEY_ID + - name: SERVALA_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: portal-storage-creds + key: AWS_SECRET_ACCESS_KEY From b89e215b35aed15a48d161df4b8af69ce8c42022 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 26 Sep 2025 09:06:16 +0200 Subject: [PATCH 003/185] correct instance name of prod in deployment --- deployment/kustomize/overlays/production/kustomization.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/kustomize/overlays/production/kustomization.yaml b/deployment/kustomize/overlays/production/kustomization.yaml index eb0f847..0566da5 100644 --- a/deployment/kustomize/overlays/production/kustomization.yaml +++ b/deployment/kustomize/overlays/production/kustomization.yaml @@ -3,7 +3,7 @@ kind: Kustomization labels: - includeSelectors: true pairs: - app.kubernetes.io/instance: test + app.kubernetes.io/instance: prod app.kubernetes.io/name: servala resources: - ../../base/portal From d8ba9130fbf72d496245e42d47793e1d6376b1ce Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 26 Sep 2025 13:00:21 +0200 Subject: [PATCH 004/185] set resources for prod and format file --- .../overlays/production/kustomization.yaml | 18 +++++++++--------- .../overlays/production/portal-deployment.yaml | 7 +++++++ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/deployment/kustomize/overlays/production/kustomization.yaml b/deployment/kustomize/overlays/production/kustomization.yaml index 0566da5..4e81d57 100644 --- a/deployment/kustomize/overlays/production/kustomization.yaml +++ b/deployment/kustomize/overlays/production/kustomization.yaml @@ -1,14 +1,14 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization labels: - - includeSelectors: true - pairs: - app.kubernetes.io/instance: prod - app.kubernetes.io/name: servala +- includeSelectors: true + pairs: + app.kubernetes.io/instance: prod + app.kubernetes.io/name: servala resources: - - ../../base/portal - - ../../base/database - - ingress.yaml +- ../../base/portal +- ../../base/database +- ingress.yaml patches: - - path: portal-deployment.yaml - - path: objectstorage.yaml +- path: portal-deployment.yaml +- path: objectstorage.yaml diff --git a/deployment/kustomize/overlays/production/portal-deployment.yaml b/deployment/kustomize/overlays/production/portal-deployment.yaml index bc6050f..47574f4 100644 --- a/deployment/kustomize/overlays/production/portal-deployment.yaml +++ b/deployment/kustomize/overlays/production/portal-deployment.yaml @@ -32,3 +32,10 @@ spec: secretKeyRef: name: portal-storage-creds key: AWS_SECRET_ACCESS_KEY + resources: + limits: + cpu: 2 + memory: 2Gi + requests: + cpu: 500m + memory: 512Mi From 26ba06e60d8f1ddcbcb242c85d83fb2d063a64e0 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 26 Sep 2025 13:46:47 +0000 Subject: [PATCH 005/185] Update dependency coverage to >=7.10.7 --- pyproject.toml | 2 +- uv.lock | 104 ++++++++++++++++++++++++++----------------------- 2 files changed, 57 insertions(+), 49 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 45d2531..c171559 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ dependencies = [ dev = [ "black>=25.1.0", "bumpver>=2025.1131", - "coverage>=7.10.6", + "coverage>=7.10.7", "djlint>=1.36.4", "flake8>=7.3.0", "flake8-bugbear>=24.12.12", diff --git a/uv.lock b/uv.lock index 18924a0..5846088 100644 --- a/uv.lock +++ b/uv.lock @@ -252,55 +252,63 @@ wheels = [ [[package]] name = "coverage" -version = "7.10.6" +version = "7.10.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/14/70/025b179c993f019105b79575ac6edb5e084fb0f0e63f15cdebef4e454fb5/coverage-7.10.6.tar.gz", hash = "sha256:f644a3ae5933a552a29dbb9aa2f90c677a875f80ebea028e5a52a4f429044b90", size = 823736, upload-time = "2025-08-29T15:35:16.668Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/e7/917e5953ea29a28c1057729c1d5af9084ab6d9c66217523fd0e10f14d8f6/coverage-7.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ffea0575345e9ee0144dfe5701aa17f3ba546f8c3bb48db62ae101afb740e7d6", size = 217351, upload-time = "2025-08-29T15:33:45.438Z" }, - { url = "https://files.pythonhosted.org/packages/eb/86/2e161b93a4f11d0ea93f9bebb6a53f113d5d6e416d7561ca41bb0a29996b/coverage-7.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:95d91d7317cde40a1c249d6b7382750b7e6d86fad9d8eaf4fa3f8f44cf171e80", size = 217600, upload-time = "2025-08-29T15:33:47.269Z" }, - { url = "https://files.pythonhosted.org/packages/0e/66/d03348fdd8df262b3a7fb4ee5727e6e4936e39e2f3a842e803196946f200/coverage-7.10.6-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e23dd5408fe71a356b41baa82892772a4cefcf758f2ca3383d2aa39e1b7a003", size = 248600, upload-time = "2025-08-29T15:33:48.953Z" }, - { url = "https://files.pythonhosted.org/packages/73/dd/508420fb47d09d904d962f123221bc249f64b5e56aa93d5f5f7603be475f/coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f3f56e4cb573755e96a16501a98bf211f100463d70275759e73f3cbc00d4f27", size = 251206, upload-time = "2025-08-29T15:33:50.697Z" }, - { url = "https://files.pythonhosted.org/packages/e9/1f/9020135734184f439da85c70ea78194c2730e56c2d18aee6e8ff1719d50d/coverage-7.10.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db4a1d897bbbe7339946ffa2fe60c10cc81c43fab8b062d3fcb84188688174a4", size = 252478, upload-time = "2025-08-29T15:33:52.303Z" }, - { url = "https://files.pythonhosted.org/packages/a4/a4/3d228f3942bb5a2051fde28c136eea23a761177dc4ff4ef54533164ce255/coverage-7.10.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8fd7879082953c156d5b13c74aa6cca37f6a6f4747b39538504c3f9c63d043d", size = 250637, upload-time = "2025-08-29T15:33:53.67Z" }, - { url = "https://files.pythonhosted.org/packages/36/e3/293dce8cdb9a83de971637afc59b7190faad60603b40e32635cbd15fbf61/coverage-7.10.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:28395ca3f71cd103b8c116333fa9db867f3a3e1ad6a084aa3725ae002b6583bc", size = 248529, upload-time = "2025-08-29T15:33:55.022Z" }, - { url = "https://files.pythonhosted.org/packages/90/26/64eecfa214e80dd1d101e420cab2901827de0e49631d666543d0e53cf597/coverage-7.10.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:61c950fc33d29c91b9e18540e1aed7d9f6787cc870a3e4032493bbbe641d12fc", size = 250143, upload-time = "2025-08-29T15:33:56.386Z" }, - { url = "https://files.pythonhosted.org/packages/3e/70/bd80588338f65ea5b0d97e424b820fb4068b9cfb9597fbd91963086e004b/coverage-7.10.6-cp313-cp313-win32.whl", hash = "sha256:160c00a5e6b6bdf4e5984b0ef21fc860bc94416c41b7df4d63f536d17c38902e", size = 219770, upload-time = "2025-08-29T15:33:58.063Z" }, - { url = "https://files.pythonhosted.org/packages/a7/14/0b831122305abcc1060c008f6c97bbdc0a913ab47d65070a01dc50293c2b/coverage-7.10.6-cp313-cp313-win_amd64.whl", hash = "sha256:628055297f3e2aa181464c3808402887643405573eb3d9de060d81531fa79d32", size = 220566, upload-time = "2025-08-29T15:33:59.766Z" }, - { url = "https://files.pythonhosted.org/packages/83/c6/81a83778c1f83f1a4a168ed6673eeedc205afb562d8500175292ca64b94e/coverage-7.10.6-cp313-cp313-win_arm64.whl", hash = "sha256:df4ec1f8540b0bcbe26ca7dd0f541847cc8a108b35596f9f91f59f0c060bfdd2", size = 219195, upload-time = "2025-08-29T15:34:01.191Z" }, - { url = "https://files.pythonhosted.org/packages/d7/1c/ccccf4bf116f9517275fa85047495515add43e41dfe8e0bef6e333c6b344/coverage-7.10.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c9a8b7a34a4de3ed987f636f71881cd3b8339f61118b1aa311fbda12741bff0b", size = 218059, upload-time = "2025-08-29T15:34:02.91Z" }, - { url = "https://files.pythonhosted.org/packages/92/97/8a3ceff833d27c7492af4f39d5da6761e9ff624831db9e9f25b3886ddbca/coverage-7.10.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd5af36092430c2b075cee966719898f2ae87b636cefb85a653f1d0ba5d5393", size = 218287, upload-time = "2025-08-29T15:34:05.106Z" }, - { url = "https://files.pythonhosted.org/packages/92/d8/50b4a32580cf41ff0423777a2791aaf3269ab60c840b62009aec12d3970d/coverage-7.10.6-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0353b0f0850d49ada66fdd7d0c7cdb0f86b900bb9e367024fd14a60cecc1e27", size = 259625, upload-time = "2025-08-29T15:34:06.575Z" }, - { url = "https://files.pythonhosted.org/packages/7e/7e/6a7df5a6fb440a0179d94a348eb6616ed4745e7df26bf2a02bc4db72c421/coverage-7.10.6-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d6b9ae13d5d3e8aeca9ca94198aa7b3ebbc5acfada557d724f2a1f03d2c0b0df", size = 261801, upload-time = "2025-08-29T15:34:08.006Z" }, - { url = "https://files.pythonhosted.org/packages/3a/4c/a270a414f4ed5d196b9d3d67922968e768cd971d1b251e1b4f75e9362f75/coverage-7.10.6-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:675824a363cc05781b1527b39dc2587b8984965834a748177ee3c37b64ffeafb", size = 264027, upload-time = "2025-08-29T15:34:09.806Z" }, - { url = "https://files.pythonhosted.org/packages/9c/8b/3210d663d594926c12f373c5370bf1e7c5c3a427519a8afa65b561b9a55c/coverage-7.10.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:692d70ea725f471a547c305f0d0fc6a73480c62fb0da726370c088ab21aed282", size = 261576, upload-time = "2025-08-29T15:34:11.585Z" }, - { url = "https://files.pythonhosted.org/packages/72/d0/e1961eff67e9e1dba3fc5eb7a4caf726b35a5b03776892da8d79ec895775/coverage-7.10.6-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:851430a9a361c7a8484a36126d1d0ff8d529d97385eacc8dfdc9bfc8c2d2cbe4", size = 259341, upload-time = "2025-08-29T15:34:13.159Z" }, - { url = "https://files.pythonhosted.org/packages/3a/06/d6478d152cd189b33eac691cba27a40704990ba95de49771285f34a5861e/coverage-7.10.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d9369a23186d189b2fc95cc08b8160ba242057e887d766864f7adf3c46b2df21", size = 260468, upload-time = "2025-08-29T15:34:14.571Z" }, - { url = "https://files.pythonhosted.org/packages/ed/73/737440247c914a332f0b47f7598535b29965bf305e19bbc22d4c39615d2b/coverage-7.10.6-cp313-cp313t-win32.whl", hash = "sha256:92be86fcb125e9bda0da7806afd29a3fd33fdf58fba5d60318399adf40bf37d0", size = 220429, upload-time = "2025-08-29T15:34:16.394Z" }, - { url = "https://files.pythonhosted.org/packages/bd/76/b92d3214740f2357ef4a27c75a526eb6c28f79c402e9f20a922c295c05e2/coverage-7.10.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6b3039e2ca459a70c79523d39347d83b73f2f06af5624905eba7ec34d64d80b5", size = 221493, upload-time = "2025-08-29T15:34:17.835Z" }, - { url = "https://files.pythonhosted.org/packages/fc/8e/6dcb29c599c8a1f654ec6cb68d76644fe635513af16e932d2d4ad1e5ac6e/coverage-7.10.6-cp313-cp313t-win_arm64.whl", hash = "sha256:3fb99d0786fe17b228eab663d16bee2288e8724d26a199c29325aac4b0319b9b", size = 219757, upload-time = "2025-08-29T15:34:19.248Z" }, - { url = "https://files.pythonhosted.org/packages/d3/aa/76cf0b5ec00619ef208da4689281d48b57f2c7fde883d14bf9441b74d59f/coverage-7.10.6-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6008a021907be8c4c02f37cdc3ffb258493bdebfeaf9a839f9e71dfdc47b018e", size = 217331, upload-time = "2025-08-29T15:34:20.846Z" }, - { url = "https://files.pythonhosted.org/packages/65/91/8e41b8c7c505d398d7730206f3cbb4a875a35ca1041efc518051bfce0f6b/coverage-7.10.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5e75e37f23eb144e78940b40395b42f2321951206a4f50e23cfd6e8a198d3ceb", size = 217607, upload-time = "2025-08-29T15:34:22.433Z" }, - { url = "https://files.pythonhosted.org/packages/87/7f/f718e732a423d442e6616580a951b8d1ec3575ea48bcd0e2228386805e79/coverage-7.10.6-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0f7cb359a448e043c576f0da00aa8bfd796a01b06aa610ca453d4dde09cc1034", size = 248663, upload-time = "2025-08-29T15:34:24.425Z" }, - { url = "https://files.pythonhosted.org/packages/e6/52/c1106120e6d801ac03e12b5285e971e758e925b6f82ee9b86db3aa10045d/coverage-7.10.6-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c68018e4fc4e14b5668f1353b41ccf4bc83ba355f0e1b3836861c6f042d89ac1", size = 251197, upload-time = "2025-08-29T15:34:25.906Z" }, - { url = "https://files.pythonhosted.org/packages/3d/ec/3a8645b1bb40e36acde9c0609f08942852a4af91a937fe2c129a38f2d3f5/coverage-7.10.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd4b2b0707fc55afa160cd5fc33b27ccbf75ca11d81f4ec9863d5793fc6df56a", size = 252551, upload-time = "2025-08-29T15:34:27.337Z" }, - { url = "https://files.pythonhosted.org/packages/a1/70/09ecb68eeb1155b28a1d16525fd3a9b65fbe75337311a99830df935d62b6/coverage-7.10.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cec13817a651f8804a86e4f79d815b3b28472c910e099e4d5a0e8a3b6a1d4cb", size = 250553, upload-time = "2025-08-29T15:34:29.065Z" }, - { url = "https://files.pythonhosted.org/packages/c6/80/47df374b893fa812e953b5bc93dcb1427a7b3d7a1a7d2db33043d17f74b9/coverage-7.10.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f2a6a8e06bbda06f78739f40bfb56c45d14eb8249d0f0ea6d4b3d48e1f7c695d", size = 248486, upload-time = "2025-08-29T15:34:30.897Z" }, - { url = "https://files.pythonhosted.org/packages/4a/65/9f98640979ecee1b0d1a7164b589de720ddf8100d1747d9bbdb84be0c0fb/coverage-7.10.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:081b98395ced0d9bcf60ada7661a0b75f36b78b9d7e39ea0790bb4ed8da14747", size = 249981, upload-time = "2025-08-29T15:34:32.365Z" }, - { url = "https://files.pythonhosted.org/packages/1f/55/eeb6603371e6629037f47bd25bef300387257ed53a3c5fdb159b7ac8c651/coverage-7.10.6-cp314-cp314-win32.whl", hash = "sha256:6937347c5d7d069ee776b2bf4e1212f912a9f1f141a429c475e6089462fcecc5", size = 220054, upload-time = "2025-08-29T15:34:34.124Z" }, - { url = "https://files.pythonhosted.org/packages/15/d1/a0912b7611bc35412e919a2cd59ae98e7ea3b475e562668040a43fb27897/coverage-7.10.6-cp314-cp314-win_amd64.whl", hash = "sha256:adec1d980fa07e60b6ef865f9e5410ba760e4e1d26f60f7e5772c73b9a5b0713", size = 220851, upload-time = "2025-08-29T15:34:35.651Z" }, - { url = "https://files.pythonhosted.org/packages/ef/2d/11880bb8ef80a45338e0b3e0725e4c2d73ffbb4822c29d987078224fd6a5/coverage-7.10.6-cp314-cp314-win_arm64.whl", hash = "sha256:a80f7aef9535442bdcf562e5a0d5a5538ce8abe6bb209cfbf170c462ac2c2a32", size = 219429, upload-time = "2025-08-29T15:34:37.16Z" }, - { url = "https://files.pythonhosted.org/packages/83/c0/1f00caad775c03a700146f55536ecd097a881ff08d310a58b353a1421be0/coverage-7.10.6-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:0de434f4fbbe5af4fa7989521c655c8c779afb61c53ab561b64dcee6149e4c65", size = 218080, upload-time = "2025-08-29T15:34:38.919Z" }, - { url = "https://files.pythonhosted.org/packages/a9/c4/b1c5d2bd7cc412cbeb035e257fd06ed4e3e139ac871d16a07434e145d18d/coverage-7.10.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6e31b8155150c57e5ac43ccd289d079eb3f825187d7c66e755a055d2c85794c6", size = 218293, upload-time = "2025-08-29T15:34:40.425Z" }, - { url = "https://files.pythonhosted.org/packages/3f/07/4468d37c94724bf6ec354e4ec2f205fda194343e3e85fd2e59cec57e6a54/coverage-7.10.6-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:98cede73eb83c31e2118ae8d379c12e3e42736903a8afcca92a7218e1f2903b0", size = 259800, upload-time = "2025-08-29T15:34:41.996Z" }, - { url = "https://files.pythonhosted.org/packages/82/d8/f8fb351be5fee31690cd8da768fd62f1cfab33c31d9f7baba6cd8960f6b8/coverage-7.10.6-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f863c08f4ff6b64fa8045b1e3da480f5374779ef187f07b82e0538c68cb4ff8e", size = 261965, upload-time = "2025-08-29T15:34:43.61Z" }, - { url = "https://files.pythonhosted.org/packages/e8/70/65d4d7cfc75c5c6eb2fed3ee5cdf420fd8ae09c4808723a89a81d5b1b9c3/coverage-7.10.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b38261034fda87be356f2c3f42221fdb4171c3ce7658066ae449241485390d5", size = 264220, upload-time = "2025-08-29T15:34:45.387Z" }, - { url = "https://files.pythonhosted.org/packages/98/3c/069df106d19024324cde10e4ec379fe2fb978017d25e97ebee23002fbadf/coverage-7.10.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e93b1476b79eae849dc3872faeb0bf7948fd9ea34869590bc16a2a00b9c82a7", size = 261660, upload-time = "2025-08-29T15:34:47.288Z" }, - { url = "https://files.pythonhosted.org/packages/fc/8a/2974d53904080c5dc91af798b3a54a4ccb99a45595cc0dcec6eb9616a57d/coverage-7.10.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ff8a991f70f4c0cf53088abf1e3886edcc87d53004c7bb94e78650b4d3dac3b5", size = 259417, upload-time = "2025-08-29T15:34:48.779Z" }, - { url = "https://files.pythonhosted.org/packages/30/38/9616a6b49c686394b318974d7f6e08f38b8af2270ce7488e879888d1e5db/coverage-7.10.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ac765b026c9f33044419cbba1da913cfb82cca1b60598ac1c7a5ed6aac4621a0", size = 260567, upload-time = "2025-08-29T15:34:50.718Z" }, - { url = "https://files.pythonhosted.org/packages/76/16/3ed2d6312b371a8cf804abf4e14895b70e4c3491c6e53536d63fd0958a8d/coverage-7.10.6-cp314-cp314t-win32.whl", hash = "sha256:441c357d55f4936875636ef2cfb3bee36e466dcf50df9afbd398ce79dba1ebb7", size = 220831, upload-time = "2025-08-29T15:34:52.653Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e5/d38d0cb830abede2adb8b147770d2a3d0e7fecc7228245b9b1ae6c24930a/coverage-7.10.6-cp314-cp314t-win_amd64.whl", hash = "sha256:073711de3181b2e204e4870ac83a7c4853115b42e9cd4d145f2231e12d670930", size = 221950, upload-time = "2025-08-29T15:34:54.212Z" }, - { url = "https://files.pythonhosted.org/packages/f4/51/e48e550f6279349895b0ffcd6d2a690e3131ba3a7f4eafccc141966d4dea/coverage-7.10.6-cp314-cp314t-win_arm64.whl", hash = "sha256:137921f2bac5559334ba66122b753db6dc5d1cf01eb7b64eb412bb0d064ef35b", size = 219969, upload-time = "2025-08-29T15:34:55.83Z" }, - { url = "https://files.pythonhosted.org/packages/44/0c/50db5379b615854b5cf89146f8f5bd1d5a9693d7f3a987e269693521c404/coverage-7.10.6-py3-none-any.whl", hash = "sha256:92c4ecf6bf11b2e85fd4d8204814dc26e6a19f0c9d938c207c5cb0eadfcabbe3", size = 208986, upload-time = "2025-08-29T15:35:14.506Z" }, + { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, + { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, + { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, + { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, + { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, + { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, + { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, + { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, + { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, + { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, + { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, + { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, + { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, + { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, + { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, + { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, + { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, + { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, + { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, + { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, + { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, ] [[package]] @@ -1201,7 +1209,7 @@ requires-dist = [ dev = [ { name = "black", specifier = ">=25.1.0" }, { name = "bumpver", specifier = ">=2025.1131" }, - { name = "coverage", specifier = ">=7.10.6" }, + { name = "coverage", specifier = ">=7.10.7" }, { name = "djlint", specifier = ">=1.36.4" }, { name = "flake8", specifier = ">=7.3.0" }, { name = "flake8-bugbear", specifier = ">=24.12.12" }, From 5c687dd3f7a5cae0adaf7ca26f06a98e1f3ee36c Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 26 Sep 2025 13:46:53 +0000 Subject: [PATCH 006/185] Update https://github.com/renovatebot/github-action action to v43.0.13 --- .forgejo/workflows/renovate.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/renovate.yaml b/.forgejo/workflows/renovate.yaml index e4c2d97..82d0a22 100644 --- a/.forgejo/workflows/renovate.yaml +++ b/.forgejo/workflows/renovate.yaml @@ -19,7 +19,7 @@ jobs: node-version: "22" - name: Renovate - uses: https://github.com/renovatebot/github-action@v43.0.11 + uses: https://github.com/renovatebot/github-action@v43.0.13 with: token: ${{ secrets.RENOVATE_TOKEN }} env: From 2f5d42f27cffde26249be24c322a7203c69ac5af Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 26 Sep 2025 15:49:43 +0200 Subject: [PATCH 007/185] bump checkout --- .forgejo/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/tests.yaml b/.forgejo/workflows/tests.yaml index 79afa42..60d76e0 100644 --- a/.forgejo/workflows/tests.yaml +++ b/.forgejo/workflows/tests.yaml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Install uv uses: https://github.com/astral-sh/setup-uv@v6 From 1018c8726e590ac1b6f99b1a4f8071e8bb54683c Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 26 Sep 2025 15:52:17 +0200 Subject: [PATCH 008/185] bump checkout everwhere and use newer node for uv --- .forgejo/workflows/build-deploy-prod.yaml | 4 ++-- .forgejo/workflows/build-deploy-staging.yaml | 4 ++-- .forgejo/workflows/docs.yaml | 4 ++-- .forgejo/workflows/renovate.yaml | 2 +- .forgejo/workflows/tests.yaml | 5 +++++ 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/.forgejo/workflows/build-deploy-prod.yaml b/.forgejo/workflows/build-deploy-prod.yaml index 87f03ae..e728d32 100644 --- a/.forgejo/workflows/build-deploy-prod.yaml +++ b/.forgejo/workflows/build-deploy-prod.yaml @@ -23,7 +23,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -69,7 +69,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Determine image tag id: determine-tag diff --git a/.forgejo/workflows/build-deploy-staging.yaml b/.forgejo/workflows/build-deploy-staging.yaml index 5f8aba7..943b035 100644 --- a/.forgejo/workflows/build-deploy-staging.yaml +++ b/.forgejo/workflows/build-deploy-staging.yaml @@ -22,7 +22,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -53,7 +53,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Deploy to OpenShift uses: docker://quay.io/appuio/oc:v4.18 diff --git a/.forgejo/workflows/docs.yaml b/.forgejo/workflows/docs.yaml index 5a17adc..cfbdee5 100644 --- a/.forgejo/workflows/docs.yaml +++ b/.forgejo/workflows/docs.yaml @@ -17,7 +17,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -49,7 +49,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Deploy to OpenShift uses: docker://quay.io/appuio/oc:v4.18 diff --git a/.forgejo/workflows/renovate.yaml b/.forgejo/workflows/renovate.yaml index 82d0a22..36a8a74 100644 --- a/.forgejo/workflows/renovate.yaml +++ b/.forgejo/workflows/renovate.yaml @@ -11,7 +11,7 @@ jobs: container: catthehacker/ubuntu:act-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Node.js uses: actions/setup-node@v5 diff --git a/.forgejo/workflows/tests.yaml b/.forgejo/workflows/tests.yaml index 60d76e0..2b2cddf 100644 --- a/.forgejo/workflows/tests.yaml +++ b/.forgejo/workflows/tests.yaml @@ -20,6 +20,11 @@ jobs: - name: Checkout repository uses: actions/checkout@v5 + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: "20" + - name: Install uv uses: https://github.com/astral-sh/setup-uv@v6 From 179680cd394f75d294198ff1bd411917ee018936 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 27 Sep 2025 03:01:19 +0000 Subject: [PATCH 009/185] Update dependency black to >=25.9.0 --- pyproject.toml | 2 +- uv.lock | 26 ++++++++++++++++++-------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c171559..89ed3be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ [dependency-groups] dev = [ - "black>=25.1.0", + "black>=25.9.0", "bumpver>=2025.1131", "coverage>=7.10.7", "djlint>=1.36.4", diff --git a/uv.lock b/uv.lock index 5846088..461777b 100644 --- a/uv.lock +++ b/uv.lock @@ -97,7 +97,7 @@ wheels = [ [[package]] name = "black" -version = "25.1.0" +version = "25.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -105,14 +105,15 @@ dependencies = [ { name = "packaging" }, { name = "pathspec" }, { name = "platformdirs" }, + { name = "pytokens" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449, upload-time = "2025-01-29T04:15:40.373Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/43/20b5c90612d7bdb2bdbcceeb53d588acca3bb8f0e4c5d5c751a2c8fdd55a/black-25.9.0.tar.gz", hash = "sha256:0474bca9a0dd1b51791fcc507a4e02078a1c63f6d4e4ae5544b9848c7adfb619", size = 648393, upload-time = "2025-09-19T00:27:37.758Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673, upload-time = "2025-01-29T05:37:20.574Z" }, - { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190, upload-time = "2025-01-29T05:37:22.106Z" }, - { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926, upload-time = "2025-01-29T04:18:58.564Z" }, - { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613, upload-time = "2025-01-29T04:19:27.63Z" }, - { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646, upload-time = "2025-01-29T04:15:38.082Z" }, + { url = "https://files.pythonhosted.org/packages/48/99/3acfea65f5e79f45472c45f87ec13037b506522719cd9d4ac86484ff51ac/black-25.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0172a012f725b792c358d57fe7b6b6e8e67375dd157f64fa7a3097b3ed3e2175", size = 1742165, upload-time = "2025-09-19T00:34:10.402Z" }, + { url = "https://files.pythonhosted.org/packages/3a/18/799285282c8236a79f25d590f0222dbd6850e14b060dfaa3e720241fd772/black-25.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3bec74ee60f8dfef564b573a96b8930f7b6a538e846123d5ad77ba14a8d7a64f", size = 1581259, upload-time = "2025-09-19T00:32:49.685Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ce/883ec4b6303acdeca93ee06b7622f1fa383c6b3765294824165d49b1a86b/black-25.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b756fc75871cb1bcac5499552d771822fd9db5a2bb8db2a7247936ca48f39831", size = 1655583, upload-time = "2025-09-19T00:30:44.505Z" }, + { url = "https://files.pythonhosted.org/packages/21/17/5c253aa80a0639ccc427a5c7144534b661505ae2b5a10b77ebe13fa25334/black-25.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:846d58e3ce7879ec1ffe816bb9df6d006cd9590515ed5d17db14e17666b2b357", size = 1343428, upload-time = "2025-09-19T00:32:13.839Z" }, + { url = "https://files.pythonhosted.org/packages/1b/46/863c90dcd3f9d41b109b7f19032ae0db021f0b2a81482ba0a1e28c84de86/black-25.9.0-py3-none-any.whl", hash = "sha256:474b34c1342cdc157d307b56c4c65bce916480c4a8f6551fdc6bf9b486a7c4ae", size = 203363, upload-time = "2025-09-19T00:27:35.724Z" }, ] [[package]] @@ -930,6 +931,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] +[[package]] +name = "pytokens" +version = "0.1.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/5f/e959a442435e24f6fb5a01aec6c657079ceaca1b3baf18561c3728d681da/pytokens-0.1.10.tar.gz", hash = "sha256:c9a4bfa0be1d26aebce03e6884ba454e842f186a59ea43a6d3b25af58223c044", size = 12171, upload-time = "2025-02-19T14:51:22.001Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/e5/63bed382f6a7a5ba70e7e132b8b7b8abbcf4888ffa6be4877698dcfbed7d/pytokens-0.1.10-py3-none-any.whl", hash = "sha256:db7b72284e480e69fb085d9f251f66b3d2df8b7166059261258ff35f50fb711b", size = 12046, upload-time = "2025-02-19T14:51:18.694Z" }, +] + [[package]] name = "pyyaml" version = "6.0.2" @@ -1207,7 +1217,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ - { name = "black", specifier = ">=25.1.0" }, + { name = "black", specifier = ">=25.9.0" }, { name = "bumpver", specifier = ">=2025.1131" }, { name = "coverage", specifier = ">=7.10.7" }, { name = "djlint", specifier = ">=1.36.4" }, From 2f2a423a3cf3fdcceabf884d67298ff1d8233ef3 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 27 Sep 2025 03:01:24 +0000 Subject: [PATCH 010/185] Update dependency django-template-partials to >=25.2 --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c171559..163b442 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ dependencies = [ "django-jsonform>=2.23.2", "django-scopes>=2.0.0", "django-storages[s3]>=1.14.6", - "django-template-partials>=25.1", + "django-template-partials>=25.2", "jsonschema>=4.25.1", "kubernetes>=33.1.0", "pillow>=11.3.0", diff --git a/uv.lock b/uv.lock index 5846088..74d52b1 100644 --- a/uv.lock +++ b/uv.lock @@ -453,14 +453,14 @@ s3 = [ [[package]] name = "django-template-partials" -version = "25.1" +version = "25.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/24/91/08f3278cc7402499c73ac655e64d73cdf31e820180dc5701a6f04e0c142a/django_template_partials-25.1.tar.gz", hash = "sha256:200c3b395dc6d0d89d17baf2d7f9afee88a6ba9633ebf593b61317b35fafed1f", size = 17272, upload-time = "2025-08-06T09:36:26.551Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c1/7f/9eca482fbfd42f2ae19fa77fa231c0d2ba6ec7caf0ced16926480e1d746b/django_template_partials-25.2.tar.gz", hash = "sha256:55044e4a12d5d3adbc02df0758eb08fcd4e3451203e02a819f9853451696aef6", size = 17787, upload-time = "2025-09-17T13:31:37.761Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/7b/f9939d5b8d8c5613e7e14293756f655b7153520285bde966ea360167b010/django_template_partials-25.1-py2.py3-none-any.whl", hash = "sha256:c32137a972a8e4d5102422f5cd6176bea33df5b84745e9ceb3ede9aa004bd6ea", size = 9398, upload-time = "2025-08-06T09:36:25.284Z" }, + { url = "https://files.pythonhosted.org/packages/9f/30/96a9d0e70efd00af9b9c011111b5eba48d17914e5b3d39516d4a7cc7e7fb/django_template_partials-25.2-py2.py3-none-any.whl", hash = "sha256:4c4f6569bd2d016281700a215c09ba4b7a4bbd2da95ce15d8c1ae76d36da44b4", size = 9621, upload-time = "2025-09-17T13:31:36.452Z" }, ] [[package]] @@ -1193,7 +1193,7 @@ requires-dist = [ { 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.1" }, + { name = "django-template-partials", specifier = ">=25.2" }, { name = "jsonschema", specifier = ">=4.25.1" }, { name = "kubernetes", specifier = ">=33.1.0" }, { name = "pillow", specifier = ">=11.3.0" }, From 41a3b3dbee143f9b13fcccbc80d19667b5d17610 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sun, 28 Sep 2025 03:01:14 +0000 Subject: [PATCH 011/185] Update dependency sentry-sdk to >=2.39.0 --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c171559..f262d3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ "pyjwt>=2.10.1", "requests>=2.32.5", "rules>=3.5", - "sentry-sdk[django]>=2.37.1", + "sentry-sdk[django]>=2.39.0", "urlman>=2.0.2", ] diff --git a/uv.lock b/uv.lock index 5846088..aa7c9b2 100644 --- a/uv.lock +++ b/uv.lock @@ -1125,15 +1125,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.37.1" +version = "2.39.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/78/be/ffc232c32d0be18f8e4eff7a22dffc1f1fef2894703d64cc281a80e75da6/sentry_sdk-2.37.1.tar.gz", hash = "sha256:531751da91aa62a909b42a7be155b41f6bb0de9df6ae98441d23b95de2f98475", size = 346235, upload-time = "2025-09-09T13:48:27.137Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/72/43294fa4bdd75c51610b5104a3ff834459ba653abb415150aa7826a249dd/sentry_sdk-2.39.0.tar.gz", hash = "sha256:8c185854d111f47f329ab6bc35993f28f7a6b7114db64aa426b326998cfa14e9", size = 348556, upload-time = "2025-09-25T09:15:39.064Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/c3/cba447ab531331d165d9003c04473be944a308ad916ca2345b5ef1969ed9/sentry_sdk-2.37.1-py2.py3-none-any.whl", hash = "sha256:baaaea6608ed3a639766a69ded06b254b106d32ad9d180bdbe58f3db9364592b", size = 368307, upload-time = "2025-09-09T13:48:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/dd/44/4356cc64246ba7b2b920f7c97a85c3c52748e213e250b512ee8152eb559d/sentry_sdk-2.39.0-py2.py3-none-any.whl", hash = "sha256:ba655ca5e57b41569b18e2a5552cb3375209760a5d332cdd87c6c3f28f729602", size = 370851, upload-time = "2025-09-25T09:15:36.35Z" }, ] [package.optional-dependencies] @@ -1201,7 +1201,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.37.1" }, + { name = "sentry-sdk", extras = ["django"], specifier = ">=2.39.0" }, { name = "urlman", specifier = ">=2.0.2" }, ] From 8f29b6d83f8c45d17f944dd5298cb3936411b08c Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sun, 28 Sep 2025 03:01:22 +0000 Subject: [PATCH 012/185] Update dependency cryptography to v46 --- pyproject.toml | 2 +- uv.lock | 126 +++++++++++++++++++++++++++++++++---------------- 2 files changed, 86 insertions(+), 42 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c171559..abfe45b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ readme = "README.md" requires-python = ">=3.13" dependencies = [ "argon2-cffi>=25.1.0", - "cryptography>=45.0.7", + "cryptography>=46.0.1", "django==5.2.6", "django-allauth>=65.11.2", "django-auditlog>=3.2.1", diff --git a/uv.lock b/uv.lock index 5846088..c2203cc 100644 --- a/uv.lock +++ b/uv.lock @@ -178,24 +178,47 @@ wheels = [ [[package]] name = "cffi" -version = "1.17.1" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser" }, + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, - { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, - { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, - { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, - { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, - { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, - { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, - { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, - { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, - { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] [[package]] @@ -313,37 +336,58 @@ wheels = [ [[package]] name = "cryptography" -version = "45.0.7" +version = "46.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a7/35/c495bffc2056f2dadb32434f1feedd79abde2a7f8363e1974afa9c33c7e2/cryptography-45.0.7.tar.gz", hash = "sha256:4b1654dfc64ea479c242508eb8c724044f1e964a47d1d1cacc5132292d851971", size = 744980, upload-time = "2025-09-01T11:15:03.146Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/62/e3664e6ffd7743e1694b244dde70b43a394f6f7fbcacf7014a8ff5197c73/cryptography-46.0.1.tar.gz", hash = "sha256:ed570874e88f213437f5cf758f9ef26cbfc3f336d889b1e592ee11283bb8d1c7", size = 749198, upload-time = "2025-09-17T00:10:35.797Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/91/925c0ac74362172ae4516000fe877912e33b5983df735ff290c653de4913/cryptography-45.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3be4f21c6245930688bd9e162829480de027f8bf962ede33d4f8ba7d67a00cee", size = 7041105, upload-time = "2025-09-01T11:13:59.684Z" }, - { url = "https://files.pythonhosted.org/packages/fc/63/43641c5acce3a6105cf8bd5baeceeb1846bb63067d26dae3e5db59f1513a/cryptography-45.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:67285f8a611b0ebc0857ced2081e30302909f571a46bfa7a3cc0ad303fe015c6", size = 4205799, upload-time = "2025-09-01T11:14:02.517Z" }, - { url = "https://files.pythonhosted.org/packages/bc/29/c238dd9107f10bfde09a4d1c52fd38828b1aa353ced11f358b5dd2507d24/cryptography-45.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:577470e39e60a6cd7780793202e63536026d9b8641de011ed9d8174da9ca5339", size = 4430504, upload-time = "2025-09-01T11:14:04.522Z" }, - { url = "https://files.pythonhosted.org/packages/62/62/24203e7cbcc9bd7c94739428cd30680b18ae6b18377ae66075c8e4771b1b/cryptography-45.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4bd3e5c4b9682bc112d634f2c6ccc6736ed3635fc3319ac2bb11d768cc5a00d8", size = 4209542, upload-time = "2025-09-01T11:14:06.309Z" }, - { url = "https://files.pythonhosted.org/packages/cd/e3/e7de4771a08620eef2389b86cd87a2c50326827dea5528feb70595439ce4/cryptography-45.0.7-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:465ccac9d70115cd4de7186e60cfe989de73f7bb23e8a7aa45af18f7412e75bf", size = 3889244, upload-time = "2025-09-01T11:14:08.152Z" }, - { url = "https://files.pythonhosted.org/packages/96/b8/bca71059e79a0bb2f8e4ec61d9c205fbe97876318566cde3b5092529faa9/cryptography-45.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:16ede8a4f7929b4b7ff3642eba2bf79aa1d71f24ab6ee443935c0d269b6bc513", size = 4461975, upload-time = "2025-09-01T11:14:09.755Z" }, - { url = "https://files.pythonhosted.org/packages/58/67/3f5b26937fe1218c40e95ef4ff8d23c8dc05aa950d54200cc7ea5fb58d28/cryptography-45.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8978132287a9d3ad6b54fcd1e08548033cc09dc6aacacb6c004c73c3eb5d3ac3", size = 4209082, upload-time = "2025-09-01T11:14:11.229Z" }, - { url = "https://files.pythonhosted.org/packages/0e/e4/b3e68a4ac363406a56cf7b741eeb80d05284d8c60ee1a55cdc7587e2a553/cryptography-45.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b6a0e535baec27b528cb07a119f321ac024592388c5681a5ced167ae98e9fff3", size = 4460397, upload-time = "2025-09-01T11:14:12.924Z" }, - { url = "https://files.pythonhosted.org/packages/22/49/2c93f3cd4e3efc8cb22b02678c1fad691cff9dd71bb889e030d100acbfe0/cryptography-45.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a24ee598d10befaec178efdff6054bc4d7e883f615bfbcd08126a0f4931c83a6", size = 4337244, upload-time = "2025-09-01T11:14:14.431Z" }, - { url = "https://files.pythonhosted.org/packages/04/19/030f400de0bccccc09aa262706d90f2ec23d56bc4eb4f4e8268d0ddf3fb8/cryptography-45.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa26fa54c0a9384c27fcdc905a2fb7d60ac6e47d14bc2692145f2b3b1e2cfdbd", size = 4568862, upload-time = "2025-09-01T11:14:16.185Z" }, - { url = "https://files.pythonhosted.org/packages/29/56/3034a3a353efa65116fa20eb3c990a8c9f0d3db4085429040a7eef9ada5f/cryptography-45.0.7-cp311-abi3-win32.whl", hash = "sha256:bef32a5e327bd8e5af915d3416ffefdbe65ed975b646b3805be81b23580b57b8", size = 2936578, upload-time = "2025-09-01T11:14:17.638Z" }, - { url = "https://files.pythonhosted.org/packages/b3/61/0ab90f421c6194705a99d0fa9f6ee2045d916e4455fdbb095a9c2c9a520f/cryptography-45.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:3808e6b2e5f0b46d981c24d79648e5c25c35e59902ea4391a0dcb3e667bf7443", size = 3405400, upload-time = "2025-09-01T11:14:18.958Z" }, - { url = "https://files.pythonhosted.org/packages/63/e8/c436233ddf19c5f15b25ace33979a9dd2e7aa1a59209a0ee8554179f1cc0/cryptography-45.0.7-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bfb4c801f65dd61cedfc61a83732327fafbac55a47282e6f26f073ca7a41c3b2", size = 7021824, upload-time = "2025-09-01T11:14:20.954Z" }, - { url = "https://files.pythonhosted.org/packages/bc/4c/8f57f2500d0ccd2675c5d0cc462095adf3faa8c52294ba085c036befb901/cryptography-45.0.7-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:81823935e2f8d476707e85a78a405953a03ef7b7b4f55f93f7c2d9680e5e0691", size = 4202233, upload-time = "2025-09-01T11:14:22.454Z" }, - { url = "https://files.pythonhosted.org/packages/eb/ac/59b7790b4ccaed739fc44775ce4645c9b8ce54cbec53edf16c74fd80cb2b/cryptography-45.0.7-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3994c809c17fc570c2af12c9b840d7cea85a9fd3e5c0e0491f4fa3c029216d59", size = 4423075, upload-time = "2025-09-01T11:14:24.287Z" }, - { url = "https://files.pythonhosted.org/packages/b8/56/d4f07ea21434bf891faa088a6ac15d6d98093a66e75e30ad08e88aa2b9ba/cryptography-45.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dad43797959a74103cb59c5dac71409f9c27d34c8a05921341fb64ea8ccb1dd4", size = 4204517, upload-time = "2025-09-01T11:14:25.679Z" }, - { url = "https://files.pythonhosted.org/packages/e8/ac/924a723299848b4c741c1059752c7cfe09473b6fd77d2920398fc26bfb53/cryptography-45.0.7-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ce7a453385e4c4693985b4a4a3533e041558851eae061a58a5405363b098fcd3", size = 3882893, upload-time = "2025-09-01T11:14:27.1Z" }, - { url = "https://files.pythonhosted.org/packages/83/dc/4dab2ff0a871cc2d81d3ae6d780991c0192b259c35e4d83fe1de18b20c70/cryptography-45.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b04f85ac3a90c227b6e5890acb0edbaf3140938dbecf07bff618bf3638578cf1", size = 4450132, upload-time = "2025-09-01T11:14:28.58Z" }, - { url = "https://files.pythonhosted.org/packages/12/dd/b2882b65db8fc944585d7fb00d67cf84a9cef4e77d9ba8f69082e911d0de/cryptography-45.0.7-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:48c41a44ef8b8c2e80ca4527ee81daa4c527df3ecbc9423c41a420a9559d0e27", size = 4204086, upload-time = "2025-09-01T11:14:30.572Z" }, - { url = "https://files.pythonhosted.org/packages/5d/fa/1d5745d878048699b8eb87c984d4ccc5da4f5008dfd3ad7a94040caca23a/cryptography-45.0.7-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f3df7b3d0f91b88b2106031fd995802a2e9ae13e02c36c1fc075b43f420f3a17", size = 4449383, upload-time = "2025-09-01T11:14:32.046Z" }, - { url = "https://files.pythonhosted.org/packages/36/8b/fc61f87931bc030598e1876c45b936867bb72777eac693e905ab89832670/cryptography-45.0.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd342f085542f6eb894ca00ef70236ea46070c8a13824c6bde0dfdcd36065b9b", size = 4332186, upload-time = "2025-09-01T11:14:33.95Z" }, - { url = "https://files.pythonhosted.org/packages/0b/11/09700ddad7443ccb11d674efdbe9a832b4455dc1f16566d9bd3834922ce5/cryptography-45.0.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1993a1bb7e4eccfb922b6cd414f072e08ff5816702a0bdb8941c247a6b1b287c", size = 4561639, upload-time = "2025-09-01T11:14:35.343Z" }, - { url = "https://files.pythonhosted.org/packages/71/ed/8f4c1337e9d3b94d8e50ae0b08ad0304a5709d483bfcadfcc77a23dbcb52/cryptography-45.0.7-cp37-abi3-win32.whl", hash = "sha256:18fcf70f243fe07252dcb1b268a687f2358025ce32f9f88028ca5c364b123ef5", size = 2926552, upload-time = "2025-09-01T11:14:36.929Z" }, - { url = "https://files.pythonhosted.org/packages/bc/ff/026513ecad58dacd45d1d24ebe52b852165a26e287177de1d545325c0c25/cryptography-45.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:7285a89df4900ed3bfaad5679b1e668cb4b38a8de1ccbfc84b05f34512da0a90", size = 3392742, upload-time = "2025-09-01T11:14:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/4c/8c/44ee01267ec01e26e43ebfdae3f120ec2312aa72fa4c0507ebe41a26739f/cryptography-46.0.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:1cd6d50c1a8b79af1a6f703709d8973845f677c8e97b1268f5ff323d38ce8475", size = 7285044, upload-time = "2025-09-17T00:08:36.807Z" }, + { url = "https://files.pythonhosted.org/packages/22/59/9ae689a25047e0601adfcb159ec4f83c0b4149fdb5c3030cc94cd218141d/cryptography-46.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0ff483716be32690c14636e54a1f6e2e1b7bf8e22ca50b989f88fa1b2d287080", size = 4308182, upload-time = "2025-09-17T00:08:39.388Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/ca6cc9df7118f2fcd142c76b1da0f14340d77518c05b1ebfbbabca6b9e7d/cryptography-46.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9873bf7c1f2a6330bdfe8621e7ce64b725784f9f0c3a6a55c3047af5849f920e", size = 4572393, upload-time = "2025-09-17T00:08:41.663Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a3/0f5296f63815d8e985922b05c31f77ce44787b3127a67c0b7f70f115c45f/cryptography-46.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0dfb7c88d4462a0cfdd0d87a3c245a7bc3feb59de101f6ff88194f740f72eda6", size = 4308400, upload-time = "2025-09-17T00:08:43.559Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8c/74fcda3e4e01be1d32775d5b4dd841acaac3c1b8fa4d0774c7ac8d52463d/cryptography-46.0.1-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e22801b61613ebdebf7deb18b507919e107547a1d39a3b57f5f855032dd7cfb8", size = 4015786, upload-time = "2025-09-17T00:08:45.758Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b8/85d23287baeef273b0834481a3dd55bbed3a53587e3b8d9f0898235b8f91/cryptography-46.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:757af4f6341ce7a1e47c326ca2a81f41d236070217e5fbbad61bbfe299d55d28", size = 4982606, upload-time = "2025-09-17T00:08:47.602Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d3/de61ad5b52433b389afca0bc70f02a7a1f074651221f599ce368da0fe437/cryptography-46.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f7a24ea78de345cfa7f6a8d3bde8b242c7fac27f2bd78fa23474ca38dfaeeab9", size = 4604234, upload-time = "2025-09-17T00:08:49.879Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1f/dbd4d6570d84748439237a7478d124ee0134bf166ad129267b7ed8ea6d22/cryptography-46.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e8776dac9e660c22241b6587fae51a67b4b0147daa4d176b172c3ff768ad736", size = 4307669, upload-time = "2025-09-17T00:08:52.321Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fd/ca0a14ce7f0bfe92fa727aacaf2217eb25eb7e4ed513b14d8e03b26e63ed/cryptography-46.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9f40642a140c0c8649987027867242b801486865277cbabc8c6059ddef16dc8b", size = 4947579, upload-time = "2025-09-17T00:08:54.697Z" }, + { url = "https://files.pythonhosted.org/packages/89/6b/09c30543bb93401f6f88fce556b3bdbb21e55ae14912c04b7bf355f5f96c/cryptography-46.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:449ef2b321bec7d97ef2c944173275ebdab78f3abdd005400cc409e27cd159ab", size = 4603669, upload-time = "2025-09-17T00:08:57.16Z" }, + { url = "https://files.pythonhosted.org/packages/23/9a/38cb01cb09ce0adceda9fc627c9cf98eb890fc8d50cacbe79b011df20f8a/cryptography-46.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2dd339ba3345b908fa3141ddba4025568fa6fd398eabce3ef72a29ac2d73ad75", size = 4435828, upload-time = "2025-09-17T00:08:59.606Z" }, + { url = "https://files.pythonhosted.org/packages/0f/53/435b5c36a78d06ae0bef96d666209b0ecd8f8181bfe4dda46536705df59e/cryptography-46.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7411c910fb2a412053cf33cfad0153ee20d27e256c6c3f14d7d7d1d9fec59fd5", size = 4709553, upload-time = "2025-09-17T00:09:01.832Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c4/0da6e55595d9b9cd3b6eb5dc22f3a07ded7f116a3ea72629cab595abb804/cryptography-46.0.1-cp311-abi3-win32.whl", hash = "sha256:cbb8e769d4cac884bb28e3ff620ef1001b75588a5c83c9c9f1fdc9afbe7f29b0", size = 3058327, upload-time = "2025-09-17T00:09:03.726Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/cd29a35e0d6e78a0ee61793564c8cff0929c38391cb0de27627bdc7525aa/cryptography-46.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:92e8cfe8bd7dd86eac0a677499894862cd5cc2fd74de917daa881d00871ac8e7", size = 3523893, upload-time = "2025-09-17T00:09:06.272Z" }, + { url = "https://files.pythonhosted.org/packages/f2/dd/eea390f3e78432bc3d2f53952375f8b37cb4d37783e626faa6a51e751719/cryptography-46.0.1-cp311-abi3-win_arm64.whl", hash = "sha256:db5597a4c7353b2e5fb05a8e6cb74b56a4658a2b7bf3cb6b1821ae7e7fd6eaa0", size = 2932145, upload-time = "2025-09-17T00:09:08.568Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fb/c73588561afcd5e24b089952bd210b14676c0c5bf1213376350ae111945c/cryptography-46.0.1-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:4c49eda9a23019e11d32a0eb51a27b3e7ddedde91e099c0ac6373e3aacc0d2ee", size = 7193928, upload-time = "2025-09-17T00:09:10.595Z" }, + { url = "https://files.pythonhosted.org/packages/26/34/0ff0bb2d2c79f25a2a63109f3b76b9108a906dd2a2eb5c1d460b9938adbb/cryptography-46.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9babb7818fdd71394e576cf26c5452df77a355eac1a27ddfa24096665a27f8fd", size = 4293515, upload-time = "2025-09-17T00:09:12.861Z" }, + { url = "https://files.pythonhosted.org/packages/df/b7/d4f848aee24ecd1be01db6c42c4a270069a4f02a105d9c57e143daf6cf0f/cryptography-46.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9f2c4cc63be3ef43c0221861177cee5d14b505cd4d4599a89e2cd273c4d3542a", size = 4545619, upload-time = "2025-09-17T00:09:15.397Z" }, + { url = "https://files.pythonhosted.org/packages/44/a5/42fedefc754fd1901e2d95a69815ea4ec8a9eed31f4c4361fcab80288661/cryptography-46.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:41c281a74df173876da1dc9a9b6953d387f06e3d3ed9284e3baae3ab3f40883a", size = 4299160, upload-time = "2025-09-17T00:09:17.155Z" }, + { url = "https://files.pythonhosted.org/packages/86/a1/cd21174f56e769c831fbbd6399a1b7519b0ff6280acec1b826d7b072640c/cryptography-46.0.1-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0a17377fa52563d730248ba1f68185461fff36e8bc75d8787a7dd2e20a802b7a", size = 3994491, upload-time = "2025-09-17T00:09:18.971Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2f/a8cbfa1c029987ddc746fd966711d4fa71efc891d37fbe9f030fe5ab4eec/cryptography-46.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:0d1922d9280e08cde90b518a10cd66831f632960a8d08cb3418922d83fce6f12", size = 4960157, upload-time = "2025-09-17T00:09:20.923Z" }, + { url = "https://files.pythonhosted.org/packages/67/ae/63a84e6789e0d5a2502edf06b552bcb0fa9ff16147265d5c44a211942abe/cryptography-46.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:af84e8e99f1a82cea149e253014ea9dc89f75b82c87bb6c7242203186f465129", size = 4577263, upload-time = "2025-09-17T00:09:23.356Z" }, + { url = "https://files.pythonhosted.org/packages/ef/8f/1b9fa8e92bd9cbcb3b7e1e593a5232f2c1e6f9bd72b919c1a6b37d315f92/cryptography-46.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ef648d2c690703501714588b2ba640facd50fd16548133b11b2859e8655a69da", size = 4298703, upload-time = "2025-09-17T00:09:25.566Z" }, + { url = "https://files.pythonhosted.org/packages/c3/af/bb95db070e73fea3fae31d8a69ac1463d89d1c084220f549b00dd01094a8/cryptography-46.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:e94eb5fa32a8a9f9bf991f424f002913e3dd7c699ef552db9b14ba6a76a6313b", size = 4926363, upload-time = "2025-09-17T00:09:27.451Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3b/d8fb17ffeb3a83157a1cc0aa5c60691d062aceecba09c2e5e77ebfc1870c/cryptography-46.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:534b96c0831855e29fc3b069b085fd185aa5353033631a585d5cd4dd5d40d657", size = 4576958, upload-time = "2025-09-17T00:09:29.924Z" }, + { url = "https://files.pythonhosted.org/packages/d9/46/86bc3a05c10c8aa88c8ae7e953a8b4e407c57823ed201dbcba55c4d655f4/cryptography-46.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9b55038b5c6c47559aa33626d8ecd092f354e23de3c6975e4bb205df128a2a0", size = 4422507, upload-time = "2025-09-17T00:09:32.222Z" }, + { url = "https://files.pythonhosted.org/packages/a8/4e/387e5a21dfd2b4198e74968a541cfd6128f66f8ec94ed971776e15091ac3/cryptography-46.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ec13b7105117dbc9afd023300fb9954d72ca855c274fe563e72428ece10191c0", size = 4683964, upload-time = "2025-09-17T00:09:34.118Z" }, + { url = "https://files.pythonhosted.org/packages/25/a3/f9f5907b166adb8f26762071474b38bbfcf89858a5282f032899075a38a1/cryptography-46.0.1-cp314-cp314t-win32.whl", hash = "sha256:504e464944f2c003a0785b81668fe23c06f3b037e9cb9f68a7c672246319f277", size = 3029705, upload-time = "2025-09-17T00:09:36.381Z" }, + { url = "https://files.pythonhosted.org/packages/12/66/4d3a4f1850db2e71c2b1628d14b70b5e4c1684a1bd462f7fffb93c041c38/cryptography-46.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c52fded6383f7e20eaf70a60aeddd796b3677c3ad2922c801be330db62778e05", size = 3502175, upload-time = "2025-09-17T00:09:38.261Z" }, + { url = "https://files.pythonhosted.org/packages/52/c7/9f10ad91435ef7d0d99a0b93c4360bea3df18050ff5b9038c489c31ac2f5/cryptography-46.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:9495d78f52c804b5ec8878b5b8c7873aa8e63db9cd9ee387ff2db3fffe4df784", size = 2912354, upload-time = "2025-09-17T00:09:40.078Z" }, + { url = "https://files.pythonhosted.org/packages/98/e5/fbd632385542a3311915976f88e0dfcf09e62a3fc0aff86fb6762162a24d/cryptography-46.0.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:d84c40bdb8674c29fa192373498b6cb1e84f882889d21a471b45d1f868d8d44b", size = 7255677, upload-time = "2025-09-17T00:09:42.407Z" }, + { url = "https://files.pythonhosted.org/packages/56/3e/13ce6eab9ad6eba1b15a7bd476f005a4c1b3f299f4c2f32b22408b0edccf/cryptography-46.0.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9ed64e5083fa806709e74fc5ea067dfef9090e5b7a2320a49be3c9df3583a2d8", size = 4301110, upload-time = "2025-09-17T00:09:45.614Z" }, + { url = "https://files.pythonhosted.org/packages/a2/67/65dc233c1ddd688073cf7b136b06ff4b84bf517ba5529607c9d79720fc67/cryptography-46.0.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:341fb7a26bc9d6093c1b124b9f13acc283d2d51da440b98b55ab3f79f2522ead", size = 4562369, upload-time = "2025-09-17T00:09:47.601Z" }, + { url = "https://files.pythonhosted.org/packages/17/db/d64ae4c6f4e98c3dac5bf35dd4d103f4c7c345703e43560113e5e8e31b2b/cryptography-46.0.1-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6ef1488967e729948d424d09c94753d0167ce59afba8d0f6c07a22b629c557b2", size = 4302126, upload-time = "2025-09-17T00:09:49.335Z" }, + { url = "https://files.pythonhosted.org/packages/3d/19/5f1eea17d4805ebdc2e685b7b02800c4f63f3dd46cfa8d4c18373fea46c8/cryptography-46.0.1-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7823bc7cdf0b747ecfb096d004cc41573c2f5c7e3a29861603a2871b43d3ef32", size = 4009431, upload-time = "2025-09-17T00:09:51.239Z" }, + { url = "https://files.pythonhosted.org/packages/81/b5/229ba6088fe7abccbfe4c5edb96c7a5ad547fac5fdd0d40aa6ea540b2985/cryptography-46.0.1-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:f736ab8036796f5a119ff8211deda416f8c15ce03776db704a7a4e17381cb2ef", size = 4980739, upload-time = "2025-09-17T00:09:54.181Z" }, + { url = "https://files.pythonhosted.org/packages/3a/9c/50aa38907b201e74bc43c572f9603fa82b58e831bd13c245613a23cff736/cryptography-46.0.1-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:e46710a240a41d594953012213ea8ca398cd2448fbc5d0f1be8160b5511104a0", size = 4592289, upload-time = "2025-09-17T00:09:56.731Z" }, + { url = "https://files.pythonhosted.org/packages/5a/33/229858f8a5bb22f82468bb285e9f4c44a31978d5f5830bb4ea1cf8a4e454/cryptography-46.0.1-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:84ef1f145de5aee82ea2447224dc23f065ff4cc5791bb3b506615957a6ba8128", size = 4301815, upload-time = "2025-09-17T00:09:58.548Z" }, + { url = "https://files.pythonhosted.org/packages/52/cb/b76b2c87fbd6ed4a231884bea3ce073406ba8e2dae9defad910d33cbf408/cryptography-46.0.1-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9394c7d5a7565ac5f7d9ba38b2617448eba384d7b107b262d63890079fad77ca", size = 4943251, upload-time = "2025-09-17T00:10:00.475Z" }, + { url = "https://files.pythonhosted.org/packages/94/0f/f66125ecf88e4cb5b8017ff43f3a87ede2d064cb54a1c5893f9da9d65093/cryptography-46.0.1-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ed957044e368ed295257ae3d212b95456bd9756df490e1ac4538857f67531fcc", size = 4591247, upload-time = "2025-09-17T00:10:02.874Z" }, + { url = "https://files.pythonhosted.org/packages/f6/22/9f3134ae436b63b463cfdf0ff506a0570da6873adb4bf8c19b8a5b4bac64/cryptography-46.0.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f7de12fa0eee6234de9a9ce0ffcfa6ce97361db7a50b09b65c63ac58e5f22fc7", size = 4428534, upload-time = "2025-09-17T00:10:04.994Z" }, + { url = "https://files.pythonhosted.org/packages/89/39/e6042bcb2638650b0005c752c38ea830cbfbcbb1830e4d64d530000aa8dc/cryptography-46.0.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7fab1187b6c6b2f11a326f33b036f7168f5b996aedd0c059f9738915e4e8f53a", size = 4699541, upload-time = "2025-09-17T00:10:06.925Z" }, + { url = "https://files.pythonhosted.org/packages/68/46/753d457492d15458c7b5a653fc9a84a1c9c7a83af6ebdc94c3fc373ca6e8/cryptography-46.0.1-cp38-abi3-win32.whl", hash = "sha256:45f790934ac1018adeba46a0f7289b2b8fe76ba774a88c7f1922213a56c98bc1", size = 3043779, upload-time = "2025-09-17T00:10:08.951Z" }, + { url = "https://files.pythonhosted.org/packages/2f/50/b6f3b540c2f6ee712feeb5fa780bb11fad76634e71334718568e7695cb55/cryptography-46.0.1-cp38-abi3-win_amd64.whl", hash = "sha256:7176a5ab56fac98d706921f6416a05e5aff7df0e4b91516f450f8627cda22af3", size = 3517226, upload-time = "2025-09-17T00:10:10.769Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e8/77d17d00981cdd27cc493e81e1749a0b8bbfb843780dbd841e30d7f50743/cryptography-46.0.1-cp38-abi3-win_arm64.whl", hash = "sha256:efc9e51c3e595267ff84adf56e9b357db89ab2279d7e375ffcaf8f678606f3d9", size = 2923149, upload-time = "2025-09-17T00:10:13.236Z" }, ] [[package]] @@ -1185,7 +1229,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "argon2-cffi", specifier = ">=25.1.0" }, - { name = "cryptography", specifier = ">=45.0.7" }, + { name = "cryptography", specifier = ">=46.0.1" }, { name = "django", specifier = "==5.2.6" }, { name = "django-allauth", specifier = ">=65.11.2" }, { name = "django-auditlog", specifier = ">=3.2.1" }, From b46ded3dca6b4754f2c6c0bcc6235176efbfb679 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 29 Sep 2025 03:00:56 +0000 Subject: [PATCH 013/185] Update https://github.com/renovatebot/github-action action to v43.0.14 --- .forgejo/workflows/renovate.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/renovate.yaml b/.forgejo/workflows/renovate.yaml index 36a8a74..97f1c2b 100644 --- a/.forgejo/workflows/renovate.yaml +++ b/.forgejo/workflows/renovate.yaml @@ -19,7 +19,7 @@ jobs: node-version: "22" - name: Renovate - uses: https://github.com/renovatebot/github-action@v43.0.13 + uses: https://github.com/renovatebot/github-action@v43.0.14 with: token: ${{ secrets.RENOVATE_TOKEN }} env: From 6c4ac644c6d2647c879f121a61ea2158fc173e52 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 29 Sep 2025 03:01:11 +0000 Subject: [PATCH 014/185] Update dependency node to v22 --- .forgejo/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/tests.yaml b/.forgejo/workflows/tests.yaml index 2b2cddf..767e526 100644 --- a/.forgejo/workflows/tests.yaml +++ b/.forgejo/workflows/tests.yaml @@ -23,7 +23,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v5 with: - node-version: "20" + node-version: "22" - name: Install uv uses: https://github.com/astral-sh/setup-uv@v6 From 356d91a76066ab50305f377e83e9938742b3d898 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 2 Oct 2025 03:01:14 +0000 Subject: [PATCH 015/185] Update dependency django to v5.2.7 --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d671a34..a4061d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ requires-python = ">=3.13" dependencies = [ "argon2-cffi>=25.1.0", "cryptography>=46.0.1", - "django==5.2.6", + "django==5.2.7", "django-allauth>=65.11.2", "django-auditlog>=3.2.1", "django-fernet-encrypted-fields>=0.3.0", diff --git a/uv.lock b/uv.lock index c99a53c..d22a7c5 100644 --- a/uv.lock +++ b/uv.lock @@ -407,16 +407,16 @@ wheels = [ [[package]] name = "django" -version = "5.2.6" +version = "5.2.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref" }, { name = "sqlparse" }, { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4c/8c/2a21594337250a171d45dda926caa96309d5136becd1f48017247f9cdea0/django-5.2.6.tar.gz", hash = "sha256:da5e00372763193d73cecbf71084a3848458cecf4cee36b9a1e8d318d114a87b", size = 10858861, upload-time = "2025-09-03T13:04:03.23Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/96/bd84e2bb997994de8bcda47ae4560991084e86536541d7214393880f01a8/django-5.2.7.tar.gz", hash = "sha256:e0f6f12e2551b1716a95a63a1366ca91bbcd7be059862c1b18f989b1da356cdd", size = 10865812, upload-time = "2025-10-01T14:22:12.081Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/af/6593f6d21404e842007b40fdeb81e73c20b6649b82d020bb0801b270174c/django-5.2.6-py3-none-any.whl", hash = "sha256:60549579b1174a304b77e24a93d8d9fafe6b6c03ac16311f3e25918ea5a20058", size = 8303111, upload-time = "2025-09-03T13:03:47.808Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ef/81f3372b5dd35d8d354321155d1a38894b2b766f576d0abffac4d8ae78d9/django-5.2.7-py3-none-any.whl", hash = "sha256:59a13a6515f787dec9d97a0438cd2efac78c8aca1c80025244b0fe507fe0754b", size = 8307145, upload-time = "2025-10-01T14:22:49.476Z" }, ] [[package]] @@ -1240,7 +1240,7 @@ dev = [ requires-dist = [ { name = "argon2-cffi", specifier = ">=25.1.0" }, { name = "cryptography", specifier = ">=46.0.1" }, - { name = "django", specifier = "==5.2.6" }, + { name = "django", specifier = "==5.2.7" }, { name = "django-allauth", specifier = ">=65.11.2" }, { name = "django-auditlog", specifier = ">=3.2.1" }, { name = "django-fernet-encrypted-fields", specifier = ">=0.3.0" }, From e8ba12e813e1611b201269254660a40d4e190088 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 30 Sep 2025 03:01:06 +0000 Subject: [PATCH 016/185] Update quay.io/appuio/oc Docker tag to v4.19 --- .forgejo/workflows/build-deploy-prod.yaml | 4 ++-- .forgejo/workflows/build-deploy-staging.yaml | 2 +- .forgejo/workflows/docs.yaml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.forgejo/workflows/build-deploy-prod.yaml b/.forgejo/workflows/build-deploy-prod.yaml index e728d32..53d1e32 100644 --- a/.forgejo/workflows/build-deploy-prod.yaml +++ b/.forgejo/workflows/build-deploy-prod.yaml @@ -87,7 +87,7 @@ jobs: esac - name: Deploy to OpenShift - uses: docker://quay.io/appuio/oc:v4.18 + uses: docker://quay.io/appuio/oc:v4.19 with: entrypoint: /bin/bash args: | @@ -104,7 +104,7 @@ jobs: OPENSHIFT_URL: ${{ secrets.OPENSHIFT_URL }} - name: Verify deployment - uses: docker://quay.io/appuio/oc:v4.18 + uses: docker://quay.io/appuio/oc:v4.19 with: entrypoint: /bin/bash args: | diff --git a/.forgejo/workflows/build-deploy-staging.yaml b/.forgejo/workflows/build-deploy-staging.yaml index 943b035..93c77b2 100644 --- a/.forgejo/workflows/build-deploy-staging.yaml +++ b/.forgejo/workflows/build-deploy-staging.yaml @@ -56,7 +56,7 @@ jobs: uses: actions/checkout@v5 - name: Deploy to OpenShift - uses: docker://quay.io/appuio/oc:v4.18 + uses: docker://quay.io/appuio/oc:v4.19 with: entrypoint: /bin/bash args: | diff --git a/.forgejo/workflows/docs.yaml b/.forgejo/workflows/docs.yaml index cfbdee5..0b6c77c 100644 --- a/.forgejo/workflows/docs.yaml +++ b/.forgejo/workflows/docs.yaml @@ -52,7 +52,7 @@ jobs: uses: actions/checkout@v5 - name: Deploy to OpenShift - uses: docker://quay.io/appuio/oc:v4.18 + uses: docker://quay.io/appuio/oc:v4.19 with: entrypoint: /bin/bash args: | From b2c5317da2ee70d7a6449120ad78c5f8c765dc79 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Fri, 26 Sep 2025 10:09:42 +0200 Subject: [PATCH 017/185] Add OSB settings --- .env.example | 4 ++++ src/servala/core/checks.py | 2 ++ src/servala/settings.py | 5 +++++ src/servala/urls.py | 1 + 4 files changed, 12 insertions(+) diff --git a/.env.example b/.env.example index ab361d7..c052830 100644 --- a/.env.example +++ b/.env.example @@ -68,3 +68,7 @@ SERVALA_ODOO_USERNAME='' SERVALA_ODOO_PASSWORD='' # Helpdesk team ID for support tickets in Odoo. Defaults to 5. SERVALA_ODOO_HELPDESK_TEAM_ID='5' + +# OSB API authentication settings +SERVALA_OSB_USERNAME='' +SERVALA_OSB_PASSWORD='' diff --git a/src/servala/core/checks.py b/src/servala/core/checks.py index d1bb708..b8f5f74 100644 --- a/src/servala/core/checks.py +++ b/src/servala/core/checks.py @@ -84,3 +84,5 @@ def check_servala_production_settings(app_configs, **kwargs): id="servala.W001", ) ) + + return errors diff --git a/src/servala/settings.py b/src/servala/settings.py index 81cdc9c..3f6894f 100644 --- a/src/servala/settings.py +++ b/src/servala/settings.py @@ -75,6 +75,9 @@ EMAIL_USE_SSL = os.environ.get("SERVALA_EMAIL_SSL", "False") == "True" SERVALA_DEFAULT_ORIGIN = int(os.environ.get("SERVALA_DEFAULT_ORIGIN", "1")) +OSB_USERNAME = os.environ.get("SERVALA_OSB_USERNAME") +OSB_PASSWORD = os.environ.get("SERVALA_OSB_PASSWORD") + SOCIALACCOUNT_PROVIDERS = { "openid_connect": { "APPS": [ @@ -159,10 +162,12 @@ INSTALLED_APPS = [ "allauth.socialaccount.providers.openid_connect", "auditlog", "servala.core", + "servala.api", ] MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", + "servala.api.authentication.OSBBasicAuthentication", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", diff --git a/src/servala/urls.py b/src/servala/urls.py index 43526c8..58628f1 100644 --- a/src/servala/urls.py +++ b/src/servala/urls.py @@ -20,6 +20,7 @@ urlpatterns = [ # - accounts/keycloak/login/callback/ path("accounts/", include("allauth.urls")), path("admin/", admin.site.urls), + path("", include("servala.api.urls")), ] # Serve static and media files in development From 313e8cd09d2bfc3c64131693ab0b3ca71aa140f8 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Fri, 26 Sep 2025 10:10:29 +0200 Subject: [PATCH 018/185] Code style --- .../core/migrations/0007_controlplane_user_info_and_more.py | 5 ++++- .../frontend/templates/frontend/organizations/services.html | 5 +---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/servala/core/migrations/0007_controlplane_user_info_and_more.py b/src/servala/core/migrations/0007_controlplane_user_info_and_more.py index 5dda2ed..25c78a9 100644 --- a/src/servala/core/migrations/0007_controlplane_user_info_and_more.py +++ b/src/servala/core/migrations/0007_controlplane_user_info_and_more.py @@ -35,7 +35,10 @@ class Migration(migrations.Migration): name="external_links", field=models.JSONField( blank=True, - help_text='JSON array of link objects: {"url": "…", "title": "…", "featured": false}. Featured links will be shown on the service list page, all other links will only show on the service and offering detail pages.', + help_text=( + 'JSON array of link objects: {"url": "…", "title": "…", "featured": false}. ' + "Featured links will be shown on the service list page, all other links will only show on the service and offering detail pages." + ), null=True, verbose_name="External links", ), diff --git a/src/servala/frontend/templates/frontend/organizations/services.html b/src/servala/frontend/templates/frontend/organizations/services.html index 766b5bc..3a48ff9 100644 --- a/src/servala/frontend/templates/frontend/organizations/services.html +++ b/src/servala/frontend/templates/frontend/organizations/services.html @@ -21,10 +21,7 @@
- {% for offering in service.offerings.all %} + {% for offering in visible_offerings %}
-
+
{% for service in services %} - +
{% include "includes/service_card.html" %}
{% empty %}
@@ -60,6 +29,22 @@
{% endfor %}
+ {% if deactivated_services %} +
+
+
{% translate "You may also be interested in one of these …" %}
+

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

+
+
+
+ {% for service in deactivated_services %} +
{% include "includes/service_card.html" %}
+ {% endfor %} +
+ {% endif %} {% endblock content %} diff --git a/src/servala/frontend/templates/includes/service_card.html b/src/servala/frontend/templates/includes/service_card.html new file mode 100644 index 0000000..0dae3ae --- /dev/null +++ b/src/servala/frontend/templates/includes/service_card.html @@ -0,0 +1,31 @@ +{% load i18n %} +
+ +
+
+ {% if service.description %}

{{ service.description|urlize }}

{% endif %} +
+
+ +
diff --git a/src/servala/frontend/views/service.py b/src/servala/frontend/views/service.py index 97d1a72..a0390b8 100644 --- a/src/servala/frontend/views/service.py +++ b/src/servala/frontend/views/service.py @@ -49,6 +49,9 @@ class ServiceListView(OrganizationViewMixin, ListView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["filter_form"] = self.filter_form + context["deactivated_services"] = ( + self.request.organization.get_deactivated_services() + ) return context diff --git a/src/servala/static/css/servala.css b/src/servala/static/css/servala.css index 9a59b8f..eb4fd01 100644 --- a/src/servala/static/css/servala.css +++ b/src/servala/static/css/servala.css @@ -279,3 +279,26 @@ html[data-bs-theme="dark"] .crd-form .nav-tabs .nav-link .mandatory-indicator { .crd-form .nav-tabs .nav-link.has-mandatory { position: relative; } + +.service-deactivated .card { + opacity: 50%; + cursor: not-allowed; + img { + opacity: 75% + } + h4, small, p { + color: var(--bs-secondary-color) !important; + } + a.btn-outline-secondary { + color: var(--bs-btn-disabled-color) !important; + background-color: var(--bs-btn-disabled-bg) !important; + border-color: var(--bs-btn-disabled-border-color) !important; + opacity: var(--bs-btn-disabled-opacity); + } + a.btn-secondary { + color: white !important; + } + a.btn { + pointer-events: none; + } +} From 6443582c0ef8790825b586640f1644824b188e4a Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Tue, 7 Oct 2025 15:04:12 +0200 Subject: [PATCH 046/185] Add default billing entity from origin ref #198 --- src/servala/api/views.py | 27 +++++++++-------- .../0010_organizationorigin_billing_entity.py | 26 ++++++++++++++++ src/servala/core/models/organization.py | 10 +++++++ src/tests/conftest.py | 6 ++++ src/tests/test_api_exoscale.py | 30 +++++++++++++++++++ 5 files changed, 87 insertions(+), 12 deletions(-) create mode 100644 src/servala/core/migrations/0010_organizationorigin_billing_entity.py diff --git a/src/servala/api/views.py b/src/servala/api/views.py index 0aa73a2..5d67754 100644 --- a/src/servala/api/views.py +++ b/src/servala/api/views.py @@ -108,16 +108,19 @@ class OSBServiceInstanceView(OSBBasicAuthPermission, View): if service in organization.limit_osb_services.all(): return JsonResponse({"message": "Service already enabled"}, status=200) except Organization.DoesNotExist: - odoo_data = { - "company_name": organization_display_name, - "invoice_email": user.email, - } - with transaction.atomic(): - try: - billing_entity = BillingEntity.create_from_data( - name=f"{organization_display_name} (Exoscale)", - odoo_data=odoo_data, - ) + try: + with transaction.atomic(): + if exoscale_origin.billing_entity: + billing_entity = exoscale_origin.billing_entity + else: + odoo_data = { + "company_name": organization_display_name, + "invoice_email": user.email, + } + billing_entity = BillingEntity.create_from_data( + name=f"{organization_display_name} (Exoscale)", + odoo_data=odoo_data, + ) organization = Organization( name=organization_display_name, billing_entity=billing_entity, @@ -126,8 +129,8 @@ class OSBServiceInstanceView(OSBBasicAuthPermission, View): ) organization = Organization.create_organization(organization, user) self._send_invitation_email(request, organization, user) - except Exception: - return JsonResponse({"error": "Internal server error"}, status=500) + except Exception: + return JsonResponse({"error": "Internal server error"}, status=500) organization.limit_osb_services.add(service) self._send_service_welcome_email( diff --git a/src/servala/core/migrations/0010_organizationorigin_billing_entity.py b/src/servala/core/migrations/0010_organizationorigin_billing_entity.py new file mode 100644 index 0000000..d61a75f --- /dev/null +++ b/src/servala/core/migrations/0010_organizationorigin_billing_entity.py @@ -0,0 +1,26 @@ +# Generated by Django 5.2.7 on 2025-10-17 00:22 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0009_organization_limit_cloudproviders_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="organizationorigin", + name="billing_entity", + field=models.ForeignKey( + help_text="If set, this billing entity will be used on new organizations with this origin.", + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="origins", + to="core.billingentity", + verbose_name="Billing entity", + ), + ), + ] diff --git a/src/servala/core/models/organization.py b/src/servala/core/models/organization.py index 84453b9..7b06a41 100644 --- a/src/servala/core/models/organization.py +++ b/src/servala/core/models/organization.py @@ -355,6 +355,16 @@ class OrganizationOrigin(ServalaModelMixin, models.Model): name = models.CharField(max_length=100, verbose_name=_("Name")) description = models.TextField(blank=True, verbose_name=_("Description")) + billing_entity = models.ForeignKey( + to="BillingEntity", + on_delete=models.PROTECT, + related_name="origins", + verbose_name=_("Billing entity"), + help_text=_( + "If set, this billing entity will be used on new organizations with this origin." + ), + null=True, + ) class Meta: verbose_name = _("Organization origin") diff --git a/src/tests/conftest.py b/src/tests/conftest.py index 32499ca..09db220 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -3,6 +3,7 @@ import base64 import pytest from servala.core.models import ( + BillingEntity, Organization, OrganizationMembership, OrganizationOrigin, @@ -21,6 +22,11 @@ def origin(): return OrganizationOrigin.objects.create(name="TESTORIGIN") +@pytest.fixture +def billing_entity(): + return BillingEntity.objects.create(name="Test Entity") + + @pytest.fixture def organization(origin): return Organization.objects.create(name="Test Org", origin=origin) diff --git a/src/tests/test_api_exoscale.py b/src/tests/test_api_exoscale.py index 725eddf..b6fa4dc 100644 --- a/src/tests/test_api_exoscale.py +++ b/src/tests/test_api_exoscale.py @@ -99,6 +99,36 @@ def test_successful_onboarding_new_organization( assert "redis/offering/" in welcome_email.body +@pytest.mark.django_db +def test_new_organization_inherits_origin( + osb_client, + test_service, + test_service_offering, + valid_osb_payload, + exoscale_origin, + instance_id, + billing_entity, +): + valid_osb_payload["service_id"] = test_service.osb_service_id + valid_osb_payload["plan_id"] = test_service_offering.osb_plan_id + exoscale_origin.billing_entity = billing_entity + exoscale_origin.save() + + response = osb_client.put( + f"/api/osb/v2/service_instances/{instance_id}", + data=json.dumps(valid_osb_payload), + content_type="application/json", + ) + + assert response.status_code == 201 + response_data = json.loads(response.content) + assert response_data["message"] == "Successfully enabled service" + + org = Organization.objects.get(osb_guid="test-org-guid-123") + assert org.name == "Test Organization Display" + assert org.billing_entity == exoscale_origin.billing_entity + + @pytest.mark.django_db def test_duplicate_organization_returns_existing( osb_client, From 842b66a84d1a8e6ba571b0f5011dc4b6be05f7aa Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Tue, 7 Oct 2025 16:39:04 +0200 Subject: [PATCH 047/185] Prepare to block billing entity editing ref #198 --- src/servala/core/admin.py | 17 ++++++++++++++++- src/servala/core/models/organization.py | 4 ++++ src/servala/frontend/forms/organization.py | 7 +++++-- .../frontend/organizations/update.html | 5 +++++ 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/servala/core/admin.py b/src/servala/core/admin.py index 073d444..0a4208d 100644 --- a/src/servala/core/admin.py +++ b/src/servala/core/admin.py @@ -66,8 +66,22 @@ class OrganizationAdmin(admin.ModelAdmin): def get_readonly_fields(self, request, obj=None): readonly_fields = list(super().get_readonly_fields(request, obj) or []) readonly_fields.append("namespace") # Always read-only + + if obj and obj.has_inherited_billing_entity: + readonly_fields.append("billing_entity") + return readonly_fields + def get_form(self, request, obj=None, **kwargs): + form = super().get_form(request, obj, **kwargs) + + if obj and obj.has_inherited_billing_entity: + form.base_fields["billing_entity"].help_text = _( + "This billing entity is inherited from the organization's origin and cannot be modified." + ) + + return form + @admin.register(BillingEntity) class BillingEntityAdmin(admin.ModelAdmin): @@ -77,8 +91,9 @@ class BillingEntityAdmin(admin.ModelAdmin): @admin.register(OrganizationOrigin) class OrganizationOriginAdmin(admin.ModelAdmin): - list_display = ("name",) + list_display = ("name", "billing_entity") search_fields = ("name",) + autocomplete_fields = ("billing_entity",) @admin.register(OrganizationMembership) diff --git a/src/servala/core/models/organization.py b/src/servala/core/models/organization.py index 7b06a41..b0d58d5 100644 --- a/src/servala/core/models/organization.py +++ b/src/servala/core/models/organization.py @@ -89,6 +89,10 @@ class Organization(ServalaModelMixin, models.Model): def get_absolute_url(self): return self.urls.base + @property + def has_inherited_billing_entity(self): + return self.origin and self.billing_entity == self.origin.billing_entity + def set_owner(self, user): with scopes_disabled(): OrganizationMembership.objects.filter(user=user, organization=self).delete() diff --git a/src/servala/frontend/forms/organization.py b/src/servala/frontend/forms/organization.py index 915ad7b..6fd04b4 100644 --- a/src/servala/frontend/forms/organization.py +++ b/src/servala/frontend/forms/organization.py @@ -8,6 +8,10 @@ from servala.frontend.forms.mixins import HtmxMixin class OrganizationForm(HtmxMixin, ModelForm): + # def __init__(self, *args, **kwargs): + # super().__init__(*args, **kwargs) + # if self.instance and self.instance.has_inherited_billing_entity: + # TODO disable billing entity editing class Meta: model = Organization fields = ("name",) @@ -46,7 +50,7 @@ class OrganizationCreateForm(OrganizationForm): def __init__(self, *args, user=None, **kwargs): super().__init__(*args, **kwargs) - + self.user = user if not self.initial.get("invoice_country"): default_country_name = "Switzerland" country_choices = self.fields["invoice_country"].choices @@ -55,7 +59,6 @@ class OrganizationCreateForm(OrganizationForm): self.initial["invoice_country"] = country_id break - self.user = user self.odoo_addresses = get_invoice_addresses(self.user) if self.odoo_addresses: diff --git a/src/servala/frontend/templates/frontend/organizations/update.html b/src/servala/frontend/templates/frontend/organizations/update.html index 97d266d..2e1b9b0 100644 --- a/src/servala/frontend/templates/frontend/organizations/update.html +++ b/src/servala/frontend/templates/frontend/organizations/update.html @@ -69,6 +69,11 @@

{% translate "Billing Address" %}

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

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

+ {% endif %}
From 8b1e0f74bb6c94edb19a916a8b3e6b9858bbfb9e Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 8 Oct 2025 11:00:16 +0200 Subject: [PATCH 048/185] Add Invitation model ref #19 --- src/servala/core/admin.py | 23 ++++ .../migrations/0011_organizationinvitation.py | 107 ++++++++++++++++++ src/servala/core/models/__init__.py | 2 + src/servala/core/models/organization.py | 69 +++++++++++ 4 files changed, 201 insertions(+) create mode 100644 src/servala/core/migrations/0011_organizationinvitation.py diff --git a/src/servala/core/admin.py b/src/servala/core/admin.py index 0a4208d..f966f38 100644 --- a/src/servala/core/admin.py +++ b/src/servala/core/admin.py @@ -9,6 +9,7 @@ from servala.core.models import ( ControlPlane, ControlPlaneCRD, Organization, + OrganizationInvitation, OrganizationMembership, OrganizationOrigin, Service, @@ -105,6 +106,28 @@ class OrganizationMembershipAdmin(admin.ModelAdmin): date_hierarchy = "date_joined" +@admin.register(OrganizationInvitation) +class OrganizationInvitationAdmin(admin.ModelAdmin): + list_display = ("email", "organization", "role", "is_accepted", "created_at") + list_filter = ("role", "created_at", "accepted_at", "organization") + search_fields = ("email", "organization__name") + autocomplete_fields = ("organization", "accepted_by") + readonly_fields = ( + "secret", + "accepted_by", + "accepted_at", + "created_at", + "updated_at", + ) + date_hierarchy = "created_at" + + def is_accepted(self, obj): + return obj.is_accepted + + is_accepted.boolean = True + is_accepted.short_description = _("Accepted") + + @admin.register(ServiceCategory) class ServiceCategoryAdmin(admin.ModelAdmin): list_display = ("name", "parent") diff --git a/src/servala/core/migrations/0011_organizationinvitation.py b/src/servala/core/migrations/0011_organizationinvitation.py new file mode 100644 index 0000000..25fb4b1 --- /dev/null +++ b/src/servala/core/migrations/0011_organizationinvitation.py @@ -0,0 +1,107 @@ +# Generated by Django 5.2.7 on 2025-10-17 00:58 + +import django.db.models.deletion +import rules.contrib.models +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0010_organizationorigin_billing_entity"), + ] + + operations = [ + migrations.CreateModel( + name="OrganizationInvitation", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created"), + ), + ( + "updated_at", + models.DateTimeField(auto_now=True, verbose_name="Last updated"), + ), + ( + "email", + models.EmailField(max_length=254, verbose_name="Email address"), + ), + ( + "role", + models.CharField( + choices=[ + ("member", "Member"), + ("admin", "Administrator"), + ("owner", "Owner"), + ], + default="member", + max_length=20, + verbose_name="Role", + ), + ), + ( + "secret", + models.CharField( + editable=False, + max_length=64, + unique=True, + verbose_name="Secret token", + ), + ), + ( + "accepted_at", + models.DateTimeField( + blank=True, null=True, verbose_name="Accepted at" + ), + ), + ( + "accepted_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="accepted_invitations", + to=settings.AUTH_USER_MODEL, + verbose_name="Accepted by", + ), + ), + ( + "created_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="created_invitations", + to=settings.AUTH_USER_MODEL, + verbose_name="Created by", + ), + ), + ( + "organization", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="invitations", + to="core.organization", + verbose_name="Organization", + ), + ), + ], + options={ + "verbose_name": "Organization invitation", + "verbose_name_plural": "Organization invitations", + "unique_together": {("organization", "email")}, + }, + bases=(rules.contrib.models.RulesModelMixin, models.Model), + ), + ] diff --git a/src/servala/core/models/__init__.py b/src/servala/core/models/__init__.py index 22e8e8a..4c23f18 100644 --- a/src/servala/core/models/__init__.py +++ b/src/servala/core/models/__init__.py @@ -1,6 +1,7 @@ from .organization import ( BillingEntity, Organization, + OrganizationInvitation, OrganizationMembership, OrganizationOrigin, OrganizationRole, @@ -23,6 +24,7 @@ __all__ = [ "ControlPlane", "ControlPlaneCRD", "Organization", + "OrganizationInvitation", "OrganizationMembership", "OrganizationOrigin", "OrganizationRole", diff --git a/src/servala/core/models/organization.py b/src/servala/core/models/organization.py index b0d58d5..b6d7e9f 100644 --- a/src/servala/core/models/organization.py +++ b/src/servala/core/models/organization.py @@ -1,3 +1,5 @@ +import secrets + import rules import urlman from django.conf import settings @@ -417,3 +419,70 @@ class OrganizationMembership(ServalaModelMixin, models.Model): def __str__(self): return f"{self.user} in {self.organization} as {self.role}" + + +class OrganizationInvitation(ServalaModelMixin, models.Model): + organization = models.ForeignKey( + to=Organization, + on_delete=models.CASCADE, + related_name="invitations", + verbose_name=_("Organization"), + ) + email = models.EmailField(verbose_name=_("Email address")) + role = models.CharField( + max_length=20, + choices=OrganizationRole.choices, + default=OrganizationRole.MEMBER, + verbose_name=_("Role"), + ) + secret = models.CharField( + max_length=64, + unique=True, + editable=False, + verbose_name=_("Secret token"), + ) + created_by = models.ForeignKey( + to="core.User", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="created_invitations", + verbose_name=_("Created by"), + ) + accepted_by = models.ForeignKey( + to="core.User", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="accepted_invitations", + verbose_name=_("Accepted by"), + ) + accepted_at = models.DateTimeField( + null=True, blank=True, verbose_name=_("Accepted at") + ) + + class urls(urlman.Urls): + accept = "/invitations/{self.secret}/accept/" + + class Meta: + verbose_name = _("Organization invitation") + verbose_name_plural = _("Organization invitations") + unique_together = [["organization", "email"]] + + def __str__(self): + return f"Invitation for {self.email} to {self.organization}" + + def save(self, *args, **kwargs): + if not self.secret: + self.secret = secrets.token_urlsafe(48) + super().save(*args, **kwargs) + + @property + def is_accepted(self): + # We check both accepted_by and accepted_at to avoid a deleted user + # freeing up an invitation + return self.accepted_by or self.accepted_at + + @property + def can_be_accepted(self): + return not self.is_accepted From 09ab83d1e448519ced0058bd3622c67cb72c1dcf Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 8 Oct 2025 11:24:44 +0200 Subject: [PATCH 049/185] Allow users to accept invitations ref #19 --- .../organizations/invitation_accept.html | 42 ++++++++++++ src/servala/frontend/urls.py | 5 ++ src/servala/frontend/views/__init__.py | 2 + src/servala/frontend/views/organization.py | 64 ++++++++++++++++++- 4 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 src/servala/frontend/templates/frontend/organizations/invitation_accept.html diff --git a/src/servala/frontend/templates/frontend/organizations/invitation_accept.html b/src/servala/frontend/templates/frontend/organizations/invitation_accept.html new file mode 100644 index 0000000..fa0bfd7 --- /dev/null +++ b/src/servala/frontend/templates/frontend/organizations/invitation_accept.html @@ -0,0 +1,42 @@ +{% extends "frontend/base.html" %} +{% load i18n %} +{% block html_title %} + {% block page_title %} + {% translate "Accept Organization Invitation" %} + {% endblock page_title %} +{% endblock html_title %} +{% block content %} +
+
+
+
+
+ + {% blocktranslate with org_name=invitation.organization.name role=invitation.get_role_display %} + You have been invited to join {{ org_name }} as a {{ role }}. + {% endblocktranslate %} +
+ {% if user.email|lower != invitation.email|lower %} +
+ + {% blocktranslate with invitation_email=invitation.email user_email=user.email %} + Note: This invitation was sent to {{ invitation_email }}, + but you are currently logged in as {{ user_email }}. + {% endblocktranslate %} +
+ {% endif %} +
+ {% csrf_token %} +
+ {% translate "Cancel" %} + +
+
+
+
+
+
+{% endblock content %} diff --git a/src/servala/frontend/urls.py b/src/servala/frontend/urls.py index 7790b22..3aa9b08 100644 --- a/src/servala/frontend/urls.py +++ b/src/servala/frontend/urls.py @@ -6,6 +6,11 @@ from servala.frontend import views urlpatterns = [ path("accounts/profile/", views.ProfileView.as_view(), name="profile"), path("accounts/logout/", views.LogoutView.as_view(), name="logout"), + path( + "invitations//accept/", + views.InvitationAcceptView.as_view(), + name="invitation.accept", + ), path( "organizations/", views.OrganizationSelectionView.as_view(), diff --git a/src/servala/frontend/views/__init__.py b/src/servala/frontend/views/__init__.py index 5f11a75..6167221 100644 --- a/src/servala/frontend/views/__init__.py +++ b/src/servala/frontend/views/__init__.py @@ -8,6 +8,7 @@ from .generic import ( custom_500, ) from .organization import ( + InvitationAcceptView, OrganizationCreateView, OrganizationDashboardView, OrganizationUpdateView, @@ -25,6 +26,7 @@ from .support import SupportView __all__ = [ "IndexView", + "InvitationAcceptView", "LogoutView", "OrganizationCreateView", "OrganizationDashboardView", diff --git a/src/servala/frontend/views/organization.py b/src/servala/frontend/views/organization.py index 2f35f76..e56416d 100644 --- a/src/servala/frontend/views/organization.py +++ b/src/servala/frontend/views/organization.py @@ -1,11 +1,17 @@ -from django.shortcuts import redirect +from django.contrib import messages +from django.shortcuts import get_object_or_404, redirect +from django.urls import reverse +from django.utils import timezone +from django.utils.decorators import method_decorator from django.utils.translation import gettext_lazy as _ -from django.views.generic import CreateView, DetailView +from django.views.generic import CreateView, DetailView, TemplateView +from django_scopes import scopes_disabled from rules.contrib.views import AutoPermissionRequiredMixin from servala.core.models import ( BillingEntity, Organization, + OrganizationInvitation, OrganizationMembership, ServiceInstance, ) @@ -103,3 +109,57 @@ class OrganizationUpdateView(OrganizationViewMixin, HtmxUpdateView): def get_success_url(self): return self.request.path + + +@method_decorator(scopes_disabled(), name="dispatch") +class InvitationAcceptView(TemplateView): + template_name = "frontend/organizations/invitation_accept.html" + + def get_invitation(self): + secret = self.kwargs.get("secret") + return get_object_or_404(OrganizationInvitation, secret=secret) + + def dispatch(self, request, *args, **kwargs): + invitation = self.get_invitation() + + if invitation.is_accepted: + messages.warning( + request, + _("This invitation has already been accepted."), + ) + return redirect("frontend:organization.selection") + if not request.user.is_authenticated: + request.session["invitation_next"] = request.path + messages.info( + request, + _("Please log in or sign up to accept this invitation."), + ) + return redirect(f"{reverse('account_login')}?next={request.path}") + return super().dispatch(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["invitation"] = self.get_invitation() + return context + + def post(self, request, *args, **kwargs): + invitation = self.get_invitation() + invitation.accepted_by = request.user + invitation.accepted_at = timezone.now() + invitation.save() + + OrganizationMembership.objects.get_or_create( + user=request.user, + organization=invitation.organization, + defaults={"role": invitation.role}, + ) + + messages.success( + request, + _("You have successfully joined {organization}!").format( + organization=invitation.organization.name + ), + ) + + request.session.pop("invitation_next", None) + return redirect(invitation.organization.urls.base) From 21c26f9e5d846fe366a844056df2f606178bec78 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 8 Oct 2025 15:04:46 +0200 Subject: [PATCH 050/185] Allow users to invite other users ref #19 --- src/servala/frontend/forms/organization.py | 68 +++++++++++- .../frontend/organizations/update.html | 102 ++++++++++++++++++ src/servala/frontend/views/organization.py | 89 ++++++++++++++- 3 files changed, 256 insertions(+), 3 deletions(-) diff --git a/src/servala/frontend/forms/organization.py b/src/servala/frontend/forms/organization.py index 6fd04b4..27e6a09 100644 --- a/src/servala/frontend/forms/organization.py +++ b/src/servala/frontend/forms/organization.py @@ -1,8 +1,9 @@ from django import forms +from django.core.exceptions import ValidationError from django.forms import ModelForm from django.utils.translation import gettext_lazy as _ -from servala.core.models import Organization +from servala.core.models import Organization, OrganizationInvitation, OrganizationRole from servala.core.odoo import get_invoice_addresses, get_odoo_countries from servala.frontend.forms.mixins import HtmxMixin @@ -111,3 +112,68 @@ class OrganizationCreateForm(OrganizationForm): "existing_odoo_address_id", _("Please select an invoice address.") ) return cleaned_data + + +class OrganizationInvitationForm(forms.ModelForm): + + def __init__(self, *args, organization=None, user_role=None, **kwargs): + super().__init__(*args, **kwargs) + self.organization = organization + self.user_role = user_role + + if user_role: + allowed_roles = self._get_allowed_roles(user_role) + self.fields["role"].choices = [ + (value, label) + for value, label in OrganizationRole.choices + if value in allowed_roles + ] + + def _get_allowed_roles(self, user_role): + role_hierarchy = { + OrganizationRole.OWNER: [ + OrganizationRole.OWNER, + OrganizationRole.ADMIN, + OrganizationRole.MEMBER, + ], + OrganizationRole.ADMIN: [ + OrganizationRole.ADMIN, + OrganizationRole.MEMBER, + ], + OrganizationRole.MEMBER: [], + } + return role_hierarchy.get(user_role, []) + + def clean_email(self): + email = self.cleaned_data["email"].lower() + + if self.organization.members.filter(email__iexact=email).exists(): + raise ValidationError( + _("A user with this email is already a member of this organization.") + ) + + if OrganizationInvitation.objects.filter( + organization=self.organization, + email__iexact=email, + accepted_by__isnull=True, + ).exists(): + raise ValidationError( + _("An invitation has already been sent to this email address.") + ) + + return email + + def save(self, commit=True): + invitation = super().save(commit=False) + invitation.organization = self.organization + if commit: + invitation.save() + return invitation + + class Meta: + model = OrganizationInvitation + fields = ("email", "role") + widgets = { + "email": forms.EmailInput(attrs={"placeholder": _("user@example.com")}), + "role": forms.RadioSelect(), + } diff --git a/src/servala/frontend/templates/frontend/organizations/update.html b/src/servala/frontend/templates/frontend/organizations/update.html index 2e1b9b0..d55dc56 100644 --- a/src/servala/frontend/templates/frontend/organizations/update.html +++ b/src/servala/frontend/templates/frontend/organizations/update.html @@ -36,6 +36,74 @@ {% endpartialdef org-name-edit %} +{% partialdef members-list %} +
+ + + + + + + + + + + {% for membership in memberships %} + + + + + + + {% empty %} + + + + {% endfor %} + +
{% translate "Name" %}{% translate "Email" %}{% translate "Role" %}{% translate "Joined" %}
{{ membership.user }}{{ membership.user.email }} + + {{ membership.get_role_display }} + + {{ membership.date_joined|date:"Y-m-d" }}
{% translate "No members yet" %}
+
+{% if pending_invitations %} +
+ {% translate "Pending Invitations" %} +
+
+ + + + + + + + + + + {% for invitation in pending_invitations %} + + + + + + + {% endfor %} + +
{% translate "Email" %}{% translate "Role" %}{% translate "Sent" %}{% translate "Link" %}
{{ invitation.email }} + + {{ invitation.get_role_display }} + + {{ invitation.created_at|date:"Y-m-d H:i" }} + +
+
+{% endif %} +{% endpartialdef members-list %} {% block content %}
@@ -135,5 +203,39 @@
{% endif %} + {% if can_manage_members %} +
+
+

+ {% translate "Members" %} +

+
+
+
{% partial members-list %}
+
+
+
+
+

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

+
+
+
+
+ {% csrf_token %} +
{{ invitation_form }}
+
+
+ +
+
+
+
+
+
+ {% endif %} {% endblock content %} diff --git a/src/servala/frontend/views/organization.py b/src/servala/frontend/views/organization.py index e56416d..651b18c 100644 --- a/src/servala/frontend/views/organization.py +++ b/src/servala/frontend/views/organization.py @@ -3,6 +3,7 @@ from django.shortcuts import get_object_or_404, redirect from django.urls import reverse from django.utils import timezone from django.utils.decorators import method_decorator +from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ from django.views.generic import CreateView, DetailView, TemplateView from django_scopes import scopes_disabled @@ -13,9 +14,14 @@ from servala.core.models import ( Organization, OrganizationInvitation, OrganizationMembership, + OrganizationRole, ServiceInstance, ) -from servala.frontend.forms.organization import OrganizationCreateForm, OrganizationForm +from servala.frontend.forms.organization import ( + OrganizationCreateForm, + OrganizationForm, + OrganizationInvitationForm, +) from servala.frontend.views.mixins import HtmxUpdateView, OrganizationViewMixin @@ -105,7 +111,86 @@ class OrganizationDashboardView( class OrganizationUpdateView(OrganizationViewMixin, HtmxUpdateView): template_name = "frontend/organizations/update.html" form_class = OrganizationForm - fragments = ("org-name", "org-name-edit") + fragments = ("org-name", "org-name-edit", "members-list") + + @cached_property + def user_role(self): + membership = ( + OrganizationMembership.objects.filter( + user=self.request.user, organization=self.get_object() + ) + .order_by("role") + .first() + ) + return membership.role if membership else None + + @cached_property + def can_manage_members(self): + return self.user_role in [ + OrganizationRole.ADMIN, + OrganizationRole.OWNER, + ] + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + organization = self.get_object() + + if self.can_manage_members: + memberships = ( + OrganizationMembership.objects.filter(organization=organization) + .select_related("user") + .order_by("role", "user__email") + ) + pending_invitations = OrganizationInvitation.objects.filter( + organization=organization, accepted_by__isnull=True + ).order_by("-created_at") + invitation_form = OrganizationInvitationForm( + organization=organization, user_role=self.user_role + ) + context.update( + { + "memberships": memberships, + "pending_invitations": pending_invitations, + "invitation_form": invitation_form, + "can_manage_members": self.can_manage_members, + "user_role": self.user_role, + } + ) + + return context + + def post(self, request, *args, **kwargs): + if "invite_email" in request.POST: + return self.handle_invitation(request) + return super().post(request, *args, **kwargs) + + def handle_invitation(self, request): + organization = self.get_object() + if not self.can_manage_members: + messages.error(request, _("You do not have permission to invite members.")) + return redirect(self.get_success_url()) + + form = OrganizationInvitationForm( + request.POST, organization=organization, user_role=self.user_role + ) + + if form.is_valid(): + invitation = form.save(commit=False) + invitation.created_by = request.user + invitation.save() + + messages.success( + request, + _("Invitation sent to {email}. Share this link: {url}").format( + email=invitation.email, + url=request.build_absolute_uri(invitation.urls.accept), + ), + ) + else: + for error in form.errors.values(): + messages.error(request, error.as_text()) + + return redirect(self.get_success_url()) def get_success_url(self): return self.request.path From b9ff0e61daac5a55e84e512902ef3bdf6d29b57b Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 8 Oct 2025 17:53:27 +0200 Subject: [PATCH 051/185] Send invitation emails ref #19 --- src/servala/api/views.py | 39 ++++++---------- src/servala/core/admin.py | 30 ++++++++++++ src/servala/core/models/organization.py | 54 +++++++++++++++++++++- src/servala/frontend/views/organization.py | 25 +++++++--- src/tests/test_api_exoscale.py | 11 ++--- 5 files changed, 119 insertions(+), 40 deletions(-) diff --git a/src/servala/api/views.py b/src/servala/api/views.py index 5d67754..456f4b2 100644 --- a/src/servala/api/views.py +++ b/src/servala/api/views.py @@ -12,7 +12,13 @@ from django.views.decorators.csrf import csrf_exempt from servala.api.permissions import OSBBasicAuthPermission from servala.core.exoscale import get_exoscale_origin -from servala.core.models import BillingEntity, Organization, User +from servala.core.models import ( + BillingEntity, + Organization, + OrganizationInvitation, + OrganizationRole, + User, +) from servala.core.models.service import Service, ServiceOffering logger = logging.getLogger(__name__) @@ -127,8 +133,13 @@ class OSBServiceInstanceView(OSBBasicAuthPermission, View): origin=exoscale_origin, osb_guid=organization_guid, ) - organization = Organization.create_organization(organization, user) - self._send_invitation_email(request, organization, user) + organization = Organization.create_organization(organization) + invitation = OrganizationInvitation.objects.create( + organization=organization, + email=user.email.lower(), + role=OrganizationRole.OWNER, + ) + invitation.send_invitation_email(request) except Exception: return JsonResponse({"error": "Internal server error"}, status=500) @@ -138,28 +149,6 @@ class OSBServiceInstanceView(OSBBasicAuthPermission, View): ) return JsonResponse({"message": "Successfully enabled service"}, status=201) - def _send_invitation_email(self, request, organization, user): - subject = f"Welcome to Servala - {organization.name}" - url = request.build_absolute_uri(organization.urls.base) - message = f"""Hello {user.first_name or user.email}, - -You have been invited to join the organization "{organization.name}" on Servala Portal. - -You can access your organization at: {url} - -Please use this email address ({user.email}) when prompted to log in. - -Best regards, -The Servala Team""" - - send_mail( - subject=subject, - message=message, - from_email=settings.EMAIL_DEFAULT_FROM, - recipient_list=[user.email], - fail_silently=False, - ) - def _send_service_welcome_email( self, request, organization, user, service, service_offering ): diff --git a/src/servala/core/admin.py b/src/servala/core/admin.py index f966f38..6e3aff6 100644 --- a/src/servala/core/admin.py +++ b/src/servala/core/admin.py @@ -120,6 +120,7 @@ class OrganizationInvitationAdmin(admin.ModelAdmin): "updated_at", ) date_hierarchy = "created_at" + actions = ["send_invitation_emails"] def is_accepted(self, obj): return obj.is_accepted @@ -127,6 +128,35 @@ class OrganizationInvitationAdmin(admin.ModelAdmin): is_accepted.boolean = True is_accepted.short_description = _("Accepted") + def send_invitation_emails(self, request, queryset): + pending_invitations = queryset.filter(accepted_by__isnull=True) + sent_count = 0 + failed_count = 0 + + for invitation in pending_invitations: + try: + invitation.send_invitation_email(request) + sent_count += 1 + except Exception as e: + failed_count += 1 + messages.error( + request, + _(f"Failed to send invitation to {invitation.email}: {str(e)}"), + ) + + if sent_count > 0: + messages.success( + request, + _(f"Successfully sent {sent_count} invitation email(s)."), + ) + + if failed_count > 0: + messages.warning( + request, _(f"Failed to send {failed_count} invitation email(s).") + ) + + send_invitation_emails.short_description = _("Send invitation emails") + @admin.register(ServiceCategory) class ServiceCategoryAdmin(admin.ModelAdmin): diff --git a/src/servala/core/models/organization.py b/src/servala/core/models/organization.py index b6d7e9f..d308bfa 100644 --- a/src/servala/core/models/organization.py +++ b/src/servala/core/models/organization.py @@ -3,7 +3,10 @@ import secrets import rules import urlman from django.conf import settings +from django.contrib.sites.shortcuts import get_current_site +from django.core.mail import send_mail from django.db import models, transaction +from django.http import HttpRequest from django.utils.functional import cached_property from django.utils.safestring import mark_safe from django.utils.text import slugify @@ -112,7 +115,7 @@ class Organization(ServalaModelMixin, models.Model): @classmethod @transaction.atomic - def create_organization(cls, instance, owner): + def create_organization(cls, instance, owner=None): try: instance.origin except Exception: @@ -120,7 +123,8 @@ class Organization(ServalaModelMixin, models.Model): pk=settings.SERVALA_DEFAULT_ORIGIN ) instance.save() - instance.set_owner(owner) + if owner: + instance.set_owner(owner) if ( instance.billing_entity.odoo_company_id @@ -486,3 +490,49 @@ class OrganizationInvitation(ServalaModelMixin, models.Model): @property def can_be_accepted(self): return not self.is_accepted + + def send_invitation_email(self, request=None): + subject = _("You're invited to join {organization} on Servala").format( + organization=self.organization.name + ) + + if request: + invitation_url = request.build_absolute_uri(self.urls.accept) + organization_url = request.build_absolute_uri(self.organization.urls.base) + else: + fake_request = HttpRequest() + fake_request.META["SERVER_NAME"] = get_current_site(None).domain + fake_request.META["SERVER_PORT"] = "443" + fake_request.META["wsgi.url_scheme"] = "https" + invitation_url = fake_request.build_absolute_uri(self.urls.accept) + organization_url = fake_request.build_absolute_uri( + self.organization.urls.base + ) + + message = _( + """Hello, + +You have been invited to join the organization "{organization}" on Servala Portal as a {role}. + +To accept this invitation, please click the link below: +{invitation_url} + +Once you accept, you'll be able to access the organization at: +{organization_url} + +Best regards, +The Servala Team""" + ).format( + organization=self.organization.name, + role=self.get_role_display(), + invitation_url=invitation_url, + organization_url=organization_url, + ) + + send_mail( + subject=subject, + message=message, + from_email=settings.EMAIL_DEFAULT_FROM, + recipient_list=[self.email], + fail_silently=False, + ) diff --git a/src/servala/frontend/views/organization.py b/src/servala/frontend/views/organization.py index 651b18c..7013a6c 100644 --- a/src/servala/frontend/views/organization.py +++ b/src/servala/frontend/views/organization.py @@ -179,13 +179,24 @@ class OrganizationUpdateView(OrganizationViewMixin, HtmxUpdateView): invitation.created_by = request.user invitation.save() - messages.success( - request, - _("Invitation sent to {email}. Share this link: {url}").format( - email=invitation.email, - url=request.build_absolute_uri(invitation.urls.accept), - ), - ) + try: + invitation.send_invitation_email(request) + messages.success( + request, + _( + "Invitation sent to {email}. They will receive an email with the invitation link." + ).format(email=invitation.email), + ) + except Exception: + messages.warning( + request, + _( + "Invitation created for {email}, but email failed to send. Share this link manually: {url}" + ).format( + email=invitation.email, + url=request.build_absolute_uri(invitation.urls.accept), + ), + ) else: for error in form.errors.values(): messages.error(request, error.as_text()) diff --git a/src/tests/test_api_exoscale.py b/src/tests/test_api_exoscale.py index b6fa4dc..6d10deb 100644 --- a/src/tests/test_api_exoscale.py +++ b/src/tests/test_api_exoscale.py @@ -73,12 +73,8 @@ def test_successful_onboarding_new_organization( assert org.origin == exoscale_origin assert org.namespace.startswith("org-") - user = User.objects.get(email="test@example.com") - assert user.first_name == "Test" - assert user.last_name == "User" with scopes_disabled(): - membership = org.memberships.get(user=user) - assert membership.role == "owner" + assert org.invitations.all().filter(email="test@example.com").exists() billing_entity = org.billing_entity assert billing_entity.name == "Test Organization Display (Exoscale)" @@ -91,7 +87,10 @@ def test_successful_onboarding_new_organization( assert len(mail.outbox) == 2 invitation_email = mail.outbox[0] - assert invitation_email.subject == "Welcome to Servala - Test Organization Display" + assert ( + invitation_email.subject + == "You're invited to join Test Organization Display on Servala" + ) assert "test@example.com" in invitation_email.to welcome_email = mail.outbox[1] From 4bf35260ad6419ec0c392751131baf776ad2a4ac Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Mon, 13 Oct 2025 11:24:40 +0200 Subject: [PATCH 052/185] Add external links to service offerings ref #197 --- src/servala/core/admin.py | 22 ++++++++++++++++-- .../0012_serviceoffering_external_links.py | 23 +++++++++++++++++++ src/servala/core/models/service.py | 6 +++++ .../organizations/service_detail.html | 8 +------ .../service_offering_detail.html | 13 ++++------- .../templates/includes/external_link.html | 7 ++++++ 6 files changed, 62 insertions(+), 17 deletions(-) create mode 100644 src/servala/core/migrations/0012_serviceoffering_external_links.py create mode 100644 src/servala/frontend/templates/includes/external_link.html diff --git a/src/servala/core/admin.py b/src/servala/core/admin.py index 6e3aff6..51af3fe 100644 --- a/src/servala/core/admin.py +++ b/src/servala/core/admin.py @@ -176,7 +176,6 @@ class ServiceAdmin(admin.ModelAdmin): def get_form(self, request, obj=None, **kwargs): form = super().get_form(request, obj, **kwargs) - # JSON schema for external_links field external_links_schema = { "type": "array", "title": "External Links", @@ -209,7 +208,6 @@ class CloudProviderAdmin(admin.ModelAdmin): def get_form(self, request, obj=None, **kwargs): form = super().get_form(request, obj, **kwargs) - # JSON schema for external_links field external_links_schema = { "type": "array", "title": "External Links", @@ -372,3 +370,23 @@ class ServiceOfferingAdmin(admin.ModelAdmin): search_fields = ("description",) autocomplete_fields = ("service", "provider") inlines = (ControlPlaneCRDInline,) + + def get_form(self, request, obj=None, **kwargs): + form = super().get_form(request, obj, **kwargs) + external_links_schema = { + "type": "array", + "title": "External Links", + "items": { + "type": "object", + "title": "Link", + "properties": { + "url": {"type": "string", "format": "uri", "title": "URL"}, + "title": {"type": "string", "title": "Title"}, + }, + "required": ["url", "title"], + }, + } + form.base_fields["external_links"].widget = JSONFormWidget( + schema=external_links_schema + ) + return form diff --git a/src/servala/core/migrations/0012_serviceoffering_external_links.py b/src/servala/core/migrations/0012_serviceoffering_external_links.py new file mode 100644 index 0000000..d8c2ac7 --- /dev/null +++ b/src/servala/core/migrations/0012_serviceoffering_external_links.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.7 on 2025-10-17 02:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0011_organizationinvitation"), + ] + + operations = [ + migrations.AddField( + model_name="serviceoffering", + name="external_links", + field=models.JSONField( + blank=True, + help_text='JSON array of link objects: {"url": "…", "title": "…"}. ', + null=True, + verbose_name="External links", + ), + ), + ] diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index 846b24e..37b3d18 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -511,6 +511,12 @@ class ServiceOffering(ServalaModelMixin, models.Model): verbose_name=_("Provider"), ) description = models.TextField(blank=True, verbose_name=_("Description")) + external_links = models.JSONField( + null=True, + blank=True, + verbose_name=_("External links"), + help_text=('JSON array of link objects: {"url": "…", "title": "…"}. '), + ) osb_plan_id = models.CharField( max_length=100, null=True, diff --git a/src/servala/frontend/templates/frontend/organizations/service_detail.html b/src/servala/frontend/templates/frontend/organizations/service_detail.html index c72fbef..a101ed1 100644 --- a/src/servala/frontend/templates/frontend/organizations/service_detail.html +++ b/src/servala/frontend/templates/frontend/organizations/service_detail.html @@ -32,13 +32,7 @@
{% translate "External Links" %}
{% for link in service.external_links %} - - {{ link.title }} - - + {% include "includes/external_link.html" %} {% endfor %}
diff --git a/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html b/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html index 7f3863e..c2049c7 100644 --- a/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html +++ b/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html @@ -64,19 +64,16 @@ {{ select_form }} {% endif %} - {% if service.external_links %} + {% if service.external_links or offering.external_links %}
{% translate "External Links" %}
{% for link in service.external_links %} - - {{ link.title }} - - + {% include "includes/external_link.html" %} + {% endfor %} + {% for link in offering.external_links %} + {% include "includes/external_link.html" %} {% endfor %}
diff --git a/src/servala/frontend/templates/includes/external_link.html b/src/servala/frontend/templates/includes/external_link.html new file mode 100644 index 0000000..e8319bf --- /dev/null +++ b/src/servala/frontend/templates/includes/external_link.html @@ -0,0 +1,7 @@ + + {{ link.title }} + + From 27b9133ad4f876f0f4fa49616e76fee4e91d5dd4 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Mon, 13 Oct 2025 12:56:06 +0200 Subject: [PATCH 053/185] Do not display _HOST keys in connection details ref #200 --- src/servala/core/models/service.py | 3 ++ .../service_instance_detail.html | 54 ++++++++++--------- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index 37b3d18..cecd7dd 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -911,6 +911,9 @@ class ServiceInstance(ServalaModelMixin, models.Model): import base64 for key, value in secret.data.items(): + # Skip keys ending with _HOST as they're only useful for dedicated OpenShift clusters + if key.endswith("_HOST"): + continue try: credentials[key] = base64.b64decode(value).decode("utf-8") except Exception: diff --git a/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html b/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html index c17dca0..d375344 100644 --- a/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html +++ b/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html @@ -173,34 +173,36 @@
{% endif %} {% if instance.connection_credentials %} -
-
-

{% translate "Connection Credentials" %}

-
-
-
- - - - - - - - - {% for key, value in instance.connection_credentials.items %} +
+
+
+

{% translate "Connection Credentials" %}

+
+
+
+
{% translate "Name" %}{% translate "Value" %}
+ - - + + - {% endfor %} - -
{{ key }} - {% if key == "error" %} - {{ value }} - {% else %} - {{ value }} - {% endif %} - {% translate "Name" %}{% translate "Value" %}
+ + + {% for key, value in instance.connection_credentials.items %} + + {{ key }} + + {% if key == "error" %} + {{ value }} + {% else %} + {{ value }} + {% endif %} + + + {% endfor %} + + +
From 72fedefb7f7c00af12988d47387f3fa131d3d96f Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Tue, 14 Oct 2025 11:16:03 +0200 Subject: [PATCH 054/185] Roll back service creation on error ref #202 --- src/servala/core/models/service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index cecd7dd..7be6003 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -663,6 +663,7 @@ class ServiceInstance(ServalaModelMixin, models.Model): return mark_safe(f"
    {error_items}
") @classmethod + @transaction.atomic def create_instance(cls, name, organization, context, created_by, spec_data): # Ensure the namespace exists context.control_plane.get_or_create_namespace(organization) @@ -710,7 +711,7 @@ class ServiceInstance(ServalaModelMixin, models.Model): body=create_data, ) except Exception as e: - instance.delete() + # Transaction will automatically roll back the instance creation if isinstance(e, ApiException): try: error_body = json.loads(e.body) From 573b7a5eb5c42f9675908bd70b3653325ddb3fb4 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Tue, 14 Oct 2025 14:34:27 +0200 Subject: [PATCH 055/185] Add wildcard_dns field ref #203 --- src/servala/core/admin.py | 10 +++++++- .../0013_controlplane_wildcard_dns.py | 24 +++++++++++++++++++ src/servala/core/models/service.py | 9 +++++++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 src/servala/core/migrations/0013_controlplane_wildcard_dns.py diff --git a/src/servala/core/admin.py b/src/servala/core/admin.py index 51af3fe..efe935c 100644 --- a/src/servala/core/admin.py +++ b/src/servala/core/admin.py @@ -240,7 +240,15 @@ class ControlPlaneAdmin(admin.ModelAdmin): fieldsets = ( ( None, - {"fields": ("name", "description", "cloud_provider", "required_label")}, + { + "fields": ( + "name", + "description", + "cloud_provider", + "required_label", + "wildcard_dns", + ) + }, ), ( _("API Credentials"), diff --git a/src/servala/core/migrations/0013_controlplane_wildcard_dns.py b/src/servala/core/migrations/0013_controlplane_wildcard_dns.py new file mode 100644 index 0000000..26dbaaf --- /dev/null +++ b/src/servala/core/migrations/0013_controlplane_wildcard_dns.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2.7 on 2025-10-17 02:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0012_serviceoffering_external_links"), + ] + + operations = [ + migrations.AddField( + model_name="controlplane", + name="wildcard_dns", + field=models.CharField( + blank=True, + help_text="Wildcard DNS domain for auto-generating FQDNs (e.g., apps.exoscale-ch-gva-2-prod2.services.servala.com)", + max_length=255, + null=True, + verbose_name="Wildcard DNS", + ), + ), + ] diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index 7be6003..f67f454 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -159,6 +159,15 @@ class ControlPlane(ServalaModelMixin, models.Model): "Key-value information displayed to users when selecting this control plane" ), ) + wildcard_dns = models.CharField( + max_length=255, + blank=True, + null=True, + verbose_name=_("Wildcard DNS"), + help_text=_( + "Wildcard DNS domain for auto-generating FQDNs (e.g., apps.exoscale-ch-gva-2-prod2.services.servala.com)" + ), + ) class Meta: verbose_name = _("Control plane") From 3375a1c8f3e181b483ab96d1cef56ced8f190bfd Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Tue, 14 Oct 2025 17:05:22 +0200 Subject: [PATCH 056/185] Generate wildcard DNS in frontend ref #203 --- .../frontend/templates/frontend/base.html | 2 + .../service_offering_detail.html | 11 ++++++ src/servala/frontend/views/service.py | 27 ++++++++++--- src/servala/static/js/fqdn.js | 38 +++++++++++++++++++ 4 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 src/servala/static/js/fqdn.js diff --git a/src/servala/frontend/templates/frontend/base.html b/src/servala/frontend/templates/frontend/base.html index 1301bec..7c6bc54 100644 --- a/src/servala/frontend/templates/frontend/base.html +++ b/src/servala/frontend/templates/frontend/base.html @@ -93,5 +93,7 @@ })(); + {% block extra_js %} + {% endblock extra_js %} diff --git a/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html b/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html index c2049c7..842e610 100644 --- a/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html +++ b/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html @@ -93,3 +93,14 @@
{% endblock content %} +{% block extra_js %} + {% if wildcard_dns and organization_namespace %} + + + {% endif %} +{% endblock extra_js %} diff --git a/src/servala/frontend/views/service.py b/src/servala/frontend/views/service.py index a0390b8..ba4f0a4 100644 --- a/src/servala/frontend/views/service.py +++ b/src/servala/frontend/views/service.py @@ -132,12 +132,25 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView def get_instance_form(self): if not self.context_object or not self.context_object.model_form_class: return None - return self.context_object.model_form_class( + + initial = { + "organization": self.request.organization, + "context": self.context_object, + } + + # Pre-populate FQDN field if it exists and control plane has wildcard DNS + form_class = self.context_object.model_form_class + if ( + "spec.parameters.service.fqdn" in form_class.base_fields + and self.context_object.control_plane.wildcard_dns + ): + # Generate initial FQDN: instancename-namespace.wildcard_dns + # We'll set a placeholder that JavaScript will replace dynamically + initial["spec.parameters.service.fqdn"] = "" + + return form_class( data=self.request.POST if self.request.method == "POST" else None, - initial={ - "organization": self.request.organization, - "context": self.context_object, - }, + initial=initial, ) def get_context_data(self, **kwargs): @@ -146,6 +159,10 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView context["has_control_planes"] = self.planes.exists() context["selected_plane"] = self.selected_plane context["service_form"] = self.get_instance_form() + # Pass data for dynamic FQDN generation + if self.selected_plane and self.selected_plane.wildcard_dns: + context["wildcard_dns"] = self.selected_plane.wildcard_dns + context["organization_namespace"] = self.request.organization.namespace return context def post(self, request, *args, **kwargs): diff --git a/src/servala/static/js/fqdn.js b/src/servala/static/js/fqdn.js new file mode 100644 index 0000000..7b61c9a --- /dev/null +++ b/src/servala/static/js/fqdn.js @@ -0,0 +1,38 @@ + +const initializeFqdnGeneration = () => { + const nameField = document.querySelector('input[name="name"]'); + const fqdnField = document.querySelector('label[for="id_spec.parameters.service.fqdn"] + div input.array-item-input'); + + if (nameField && fqdnField) { + const generateFqdn = (instanceName) => { + if (!instanceName) return ''; + return `${instanceName}-${fqdnConfig.namespace}.${fqdnConfig.wildcardDns}`; + } + + const newNameField = nameField.cloneNode(true); + nameField.parentNode.replaceChild(newNameField, nameField); + const newFqdnField = fqdnField.cloneNode(true); + fqdnField.parentNode.replaceChild(newFqdnField, fqdnField); + + newNameField.addEventListener('input', function() { + if (!newFqdnField.dataset.manuallyEdited) { + newFqdnField.value = generateFqdn(this.value); + } + }); + + newFqdnField.addEventListener('input', function() { + this.dataset.manuallyEdited = 'true'; + }); + + if (newNameField.value && !newFqdnField.value) { + newFqdnField.value = generateFqdn(newNameField.value); + } + } +} + +document.addEventListener('DOMContentLoaded', initializeFqdnGeneration); +document.body.addEventListener('htmx:afterSwap', function(event) { + if (event.detail.target.id === 'service-form') { + initializeFqdnGeneration(); + } +}); From 4124add14608b3b44b80e3e1db704104bc3899c5 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 15 Oct 2025 11:58:02 +0200 Subject: [PATCH 057/185] Fix common abbreviations ref #204 --- src/servala/core/crd.py | 113 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 110 insertions(+), 3 deletions(-) diff --git a/src/servala/core/crd.py b/src/servala/core/crd.py index 44c809b..5d5c34e 100644 --- a/src/servala/core/crd.py +++ b/src/servala/core/crd.py @@ -86,10 +86,117 @@ def build_object_fields(schema, name, verbose_name_prefix=None, parent_required= def deslugify(title): + """ + Convert camelCase, PascalCase, or snake_case to human-readable title. + Handles known acronyms (e.g., postgreSQLParameters -> PostgreSQL Parameters). + """ + ACRONYMS = { + # Database systems + "SQL": "SQL", + "MYSQL": "MySQL", + "POSTGRESQL": "PostgreSQL", + "MARIADB": "MariaDB", + "MSSQL": "MSSQL", + "MONGODB": "MongoDB", + "REDIS": "Redis", + # Protocols + "HTTP": "HTTP", + "HTTPS": "HTTPS", + "FTP": "FTP", + "SFTP": "SFTP", + "SSH": "SSH", + "TLS": "TLS", + "SSL": "SSL", + # APIs + "API": "API", + "REST": "REST", + "GRPC": "gRPC", + "GRAPHQL": "GraphQL", + # Networking + "URL": "URL", + "URI": "URI", + "FQDN": "FQDN", + "DNS": "DNS", + "IP": "IP", + "TCP": "TCP", + "UDP": "UDP", + # Data formats + "JSON": "JSON", + "XML": "XML", + "YAML": "YAML", + "CSV": "CSV", + "HTML": "HTML", + "CSS": "CSS", + # Hardware + "CPU": "CPU", + "RAM": "RAM", + "GPU": "GPU", + "SSD": "SSD", + "HDD": "HDD", + # Identifiers + "ID": "ID", + "UUID": "UUID", + "GUID": "GUID", + "ARN": "ARN", + # Cloud providers + "AWS": "AWS", + "GCP": "GCP", + "AZURE": "Azure", + "IBM": "IBM", + # Kubernetes/Cloud + "DB": "DB", + "PVC": "PVC", + "PV": "PV", + "VPN": "VPN", + # Auth + "OS": "OS", + "LDAP": "LDAP", + "SAML": "SAML", + "OAUTH": "OAuth", + "JWT": "JWT", + # AWS Services + "S3": "S3", + "EC2": "EC2", + "RDS": "RDS", + "EBS": "EBS", + "IAM": "IAM", + } + if "_" in title: - title.replace("_", " ") - return title.title() - return re.sub(r"(? SQL Parameters) + words = re.findall(r"[A-Z]+(?=[A-Z][a-z]|\b)|[A-Z][a-z]+|[a-z]+|[0-9]+", title) + + # Merge adjacent words if they form a known compound acronym (e.g., postgre + SQL = PostgreSQL) + merged_words = [] + i = 0 + while i < len(words): + if i < len(words) - 1: + # Check if current word + next word form a known acronym + combined = (words[i] + words[i + 1]).upper() + if combined in ACRONYMS: + merged_words.append(combined) + i += 2 + continue + merged_words.append(words[i]) + i += 1 + + # Capitalize each word, using proper casing for known acronyms + result = [] + for word in merged_words: + word_upper = word.upper() + if word_upper in ACRONYMS: + result.append(ACRONYMS[word_upper]) + else: + result.append(word.capitalize()) + + return " ".join(result) def get_django_field( From cd886df05b7e3959cdcca7a0fa00f9d6837f887a Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 15 Oct 2025 14:34:38 +0200 Subject: [PATCH 058/185] Add ServiceDefinition.advanced_fields ref #204 --- src/servala/core/admin.py | 27 +++++++++++++++++++ .../0014_servicedefinition_advanced_fields.py | 27 +++++++++++++++++++ src/servala/core/models/service.py | 15 ++++++++++- 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 src/servala/core/migrations/0014_servicedefinition_advanced_fields.py diff --git a/src/servala/core/admin.py b/src/servala/core/admin.py index efe935c..1aec22a 100644 --- a/src/servala/core/admin.py +++ b/src/servala/core/admin.py @@ -318,8 +318,35 @@ class ServiceDefinitionAdmin(admin.ModelAdmin): "description": _("API definition for the Kubernetes Custom Resource"), }, ), + ( + _("Form Configuration"), + { + "fields": ("advanced_fields",), + "description": _( + "Configure which fields should be hidden behind an 'Advanced' toggle in the form" + ), + }, + ), ) + def get_form(self, request, obj=None, **kwargs): + form = super().get_form(request, obj, **kwargs) + # JSON schema for advanced_fields field + advanced_fields_schema = { + "type": "array", + "title": "Advanced Fields", + "items": { + "type": "string", + "title": "Field Name", + "description": "Field name in dot notation (e.g., spec.parameters.monitoring.enabled)", + }, + } + if "advanced_fields" in form.base_fields: + form.base_fields["advanced_fields"].widget = JSONFormWidget( + schema=advanced_fields_schema + ) + return form + def get_exclude(self, request, obj=None): # Exclude the original api_definition field as we're using our custom fields return ["api_definition"] diff --git a/src/servala/core/migrations/0014_servicedefinition_advanced_fields.py b/src/servala/core/migrations/0014_servicedefinition_advanced_fields.py new file mode 100644 index 0000000..20632b9 --- /dev/null +++ b/src/servala/core/migrations/0014_servicedefinition_advanced_fields.py @@ -0,0 +1,27 @@ +# Generated by Django 5.2.7 on 2025-10-17 03:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0013_controlplane_wildcard_dns"), + ] + + operations = [ + migrations.AddField( + model_name="servicedefinition", + name="advanced_fields", + field=models.JSONField( + blank=True, + default=list, + help_text=( + "Array of field names that should be hidden behind an 'Advanced' toggle. " + "Use dot notation (e.g., ['spec.parameters.monitoring.enabled', 'spec.parameters.backup.schedule'])" + ), + null=True, + verbose_name="Advanced fields", + ), + ), + ] diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index f67f454..42fc500 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -359,6 +359,16 @@ class ServiceDefinition(ServalaModelMixin, models.Model): null=True, blank=True, ) + advanced_fields = models.JSONField( + verbose_name=_("Advanced fields"), + help_text=_( + "Array of field names that should be hidden behind an 'Advanced' toggle. " + "Use dot notation (e.g., ['spec.parameters.monitoring.enabled', 'spec.parameters.backup.schedule'])" + ), + null=True, + blank=True, + default=list, + ) service = models.ForeignKey( to="Service", on_delete=models.CASCADE, @@ -499,7 +509,10 @@ class ControlPlaneCRD(ServalaModelMixin, models.Model): if not self.django_model: return - return generate_model_form_class(self.django_model) + advanced_fields = self.service_definition.advanced_fields or [] + return generate_model_form_class( + self.django_model, advanced_fields=advanced_fields + ) class ServiceOffering(ServalaModelMixin, models.Model): From 31018298852e28af7f16324d3790002dffb60ace Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 15 Oct 2025 16:42:57 +0200 Subject: [PATCH 059/185] Allow users to toggle advanced fields ref #204 --- src/servala/core/crd.py | 16 +++- .../includes/tabbed_fieldset_form.html | 14 ++++ src/servala/static/css/servala.css | 30 +++++++ src/servala/static/js/advanced-fields.js | 83 +++++++++++++++++++ 4 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 src/servala/static/js/advanced-fields.js diff --git a/src/servala/core/crd.py b/src/servala/core/crd.py index 5d5c34e..276e9c2 100644 --- a/src/servala/core/crd.py +++ b/src/servala/core/crd.py @@ -327,6 +327,19 @@ class CrdModelFormMixin: field.widget = forms.HiddenInput() field.required = False + # Mark advanced fields with a CSS class and data attribute + advanced_fields = getattr(self, "ADVANCED_FIELDS", []) + for name, field in self.fields.items(): + if name in advanced_fields: + field.widget.attrs.update( + { + "class": ( + field.widget.attrs.get("class", "") + " advanced-field" + ).strip(), + "data-advanced": "true", + } + ) + if self.instance and self.instance.pk: self.fields["name"].disabled = True self.fields["name"].help_text = _("Name cannot be changed after creation.") @@ -513,7 +526,7 @@ class CrdModelFormMixin: pass -def generate_model_form_class(model): +def generate_model_form_class(model, advanced_fields=None): meta_attrs = { "model": model, "fields": "__all__", @@ -521,6 +534,7 @@ def generate_model_form_class(model): fields = { "Meta": type("Meta", (object,), meta_attrs), "__module__": "crd_models", + "ADVANCED_FIELDS": advanced_fields or [], } class_name = f"{model.__name__}ModelForm" return ModelFormMetaclass(class_name, (CrdModelFormMixin, ModelForm), fields) diff --git a/src/servala/frontend/templates/includes/tabbed_fieldset_form.html b/src/servala/frontend/templates/includes/tabbed_fieldset_form.html index c9d947a..5857bdf 100644 --- a/src/servala/frontend/templates/includes/tabbed_fieldset_form.html +++ b/src/servala/frontend/templates/includes/tabbed_fieldset_form.html @@ -1,10 +1,23 @@ {% load i18n %} {% load get_field %} +{% load static %}
{% csrf_token %} {% include "frontend/forms/errors.html" %} + {% if form.ADVANCED_FIELDS %} +
+ +
+ {% endif %}
+ diff --git a/src/servala/static/css/servala.css b/src/servala/static/css/servala.css index eb4fd01..0ea8b28 100644 --- a/src/servala/static/css/servala.css +++ b/src/servala/static/css/servala.css @@ -302,3 +302,33 @@ html[data-bs-theme="dark"] .crd-form .nav-tabs .nav-link .mandatory-indicator { pointer-events: none; } } +.ml-auto { + margin-left: auto !important +} + +/* Advanced fields tab flash animation */ +@keyframes tab-pulse { + 0%, 100% { + background-color: transparent; + box-shadow: none; + } + 50% { + background-color: var(--brand-light); + box-shadow: 0 0 10px rgba(154, 99, 236, 0.3); + } +} + +html[data-bs-theme="dark"] @keyframes tab-pulse { + 0%, 100% { + background-color: transparent; + box-shadow: none; + } + 50% { + background-color: rgba(154, 99, 236, 0.2); + box-shadow: 0 0 10px rgba(154, 99, 236, 0.4); + } +} + +.nav-tabs .nav-link.tab-flash { + animation: tab-pulse 1s ease-in-out 2; +} diff --git a/src/servala/static/js/advanced-fields.js b/src/servala/static/js/advanced-fields.js new file mode 100644 index 0000000..989e61a --- /dev/null +++ b/src/servala/static/js/advanced-fields.js @@ -0,0 +1,83 @@ +/** + * Advanced Fields Toggle + * Handles showing/hiding advanced fields in CRD forms + */ +(function() { + 'use strict'; + + function flashTabsWithAdvancedFields() { + const advancedGroups = document.querySelectorAll('.advanced-field-group'); + const tabsToFlash = new Set(); + advancedGroups.forEach(function(group) { + const tabPane = group.closest('.tab-pane'); + if (tabPane) { + const tabId = tabPane.getAttribute('id'); + if (tabId) { + const tabButton = document.querySelector(`[data-bs-target="#${tabId}"]`); + if (tabButton && !tabButton.classList.contains('active')) { + tabsToFlash.add(tabButton); + } + } + } + }); + + tabsToFlash.forEach(function(tab) { + tab.classList.add('tab-flash'); + setTimeout(function() { + tab.classList.remove('tab-flash'); + }, 2000); + }); + } + + function initializeAdvancedFields() { + const advancedInputs = document.querySelectorAll('[data-advanced="true"]'); + + if (advancedInputs.length === 0) { + return; + } + + advancedInputs.forEach(function(input) { + const formGroup = input.closest('.form-group, .mb-3, .col-12, .col-md-6'); + if (formGroup) { + formGroup.classList.add('advanced-field-group', 'collapse'); + } + }); + + const toggleButton = document.getElementById('advanced-toggle'); + if (toggleButton) { + let isExpanded = false; + + document.querySelectorAll('.advanced-field-group').forEach(function(group) { + group.addEventListener('shown.bs.collapse', function() { + toggleButton.innerHTML = ' Hide Advanced Options'; + if (!isExpanded) { + isExpanded = true; + setTimeout(flashTabsWithAdvancedFields, 100); + } + }); + + group.addEventListener('hidden.bs.collapse', function() { + const anyVisible = Array.from(document.querySelectorAll('.advanced-field-group')).some( + g => g.classList.contains('show') + ); + if (!anyVisible) { + toggleButton.innerHTML = ' Show Advanced Options'; + isExpanded = false; + } + }); + }); + } + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initializeAdvancedFields); + } else { + initializeAdvancedFields(); + } + + document.body.addEventListener('htmx:afterSwap', function(event) { + if (event.detail.target.id === 'service-form' || event.detail.target.closest('.crd-form')) { + setTimeout(initializeAdvancedFields, 100); + } + }); +})(); From a370f67932efeb987bd748ef5a307ecec9e8285d Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 16 Oct 2025 03:01:43 +0000 Subject: [PATCH 060/185] Update dependency cryptography to >=46.0.3 --- pyproject.toml | 2 +- uv.lock | 96 +++++++++++++++++++++++++------------------------- 2 files changed, 49 insertions(+), 49 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index eadb2f2..b457676 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ readme = "README.md" requires-python = ">=3.13" dependencies = [ "argon2-cffi>=25.1.0", - "cryptography>=46.0.1", + "cryptography>=46.0.3", "django==5.2.7", "django-allauth>=65.11.2", "django-auditlog>=3.2.1", diff --git a/uv.lock b/uv.lock index 81fa302..ea6a797 100644 --- a/uv.lock +++ b/uv.lock @@ -337,58 +337,58 @@ wheels = [ [[package]] name = "cryptography" -version = "46.0.1" +version = "46.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a9/62/e3664e6ffd7743e1694b244dde70b43a394f6f7fbcacf7014a8ff5197c73/cryptography-46.0.1.tar.gz", hash = "sha256:ed570874e88f213437f5cf758f9ef26cbfc3f336d889b1e592ee11283bb8d1c7", size = 749198, upload-time = "2025-09-17T00:10:35.797Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/8c/44ee01267ec01e26e43ebfdae3f120ec2312aa72fa4c0507ebe41a26739f/cryptography-46.0.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:1cd6d50c1a8b79af1a6f703709d8973845f677c8e97b1268f5ff323d38ce8475", size = 7285044, upload-time = "2025-09-17T00:08:36.807Z" }, - { url = "https://files.pythonhosted.org/packages/22/59/9ae689a25047e0601adfcb159ec4f83c0b4149fdb5c3030cc94cd218141d/cryptography-46.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0ff483716be32690c14636e54a1f6e2e1b7bf8e22ca50b989f88fa1b2d287080", size = 4308182, upload-time = "2025-09-17T00:08:39.388Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ee/ca6cc9df7118f2fcd142c76b1da0f14340d77518c05b1ebfbbabca6b9e7d/cryptography-46.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9873bf7c1f2a6330bdfe8621e7ce64b725784f9f0c3a6a55c3047af5849f920e", size = 4572393, upload-time = "2025-09-17T00:08:41.663Z" }, - { url = "https://files.pythonhosted.org/packages/7f/a3/0f5296f63815d8e985922b05c31f77ce44787b3127a67c0b7f70f115c45f/cryptography-46.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0dfb7c88d4462a0cfdd0d87a3c245a7bc3feb59de101f6ff88194f740f72eda6", size = 4308400, upload-time = "2025-09-17T00:08:43.559Z" }, - { url = "https://files.pythonhosted.org/packages/5d/8c/74fcda3e4e01be1d32775d5b4dd841acaac3c1b8fa4d0774c7ac8d52463d/cryptography-46.0.1-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e22801b61613ebdebf7deb18b507919e107547a1d39a3b57f5f855032dd7cfb8", size = 4015786, upload-time = "2025-09-17T00:08:45.758Z" }, - { url = "https://files.pythonhosted.org/packages/dc/b8/85d23287baeef273b0834481a3dd55bbed3a53587e3b8d9f0898235b8f91/cryptography-46.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:757af4f6341ce7a1e47c326ca2a81f41d236070217e5fbbad61bbfe299d55d28", size = 4982606, upload-time = "2025-09-17T00:08:47.602Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d3/de61ad5b52433b389afca0bc70f02a7a1f074651221f599ce368da0fe437/cryptography-46.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f7a24ea78de345cfa7f6a8d3bde8b242c7fac27f2bd78fa23474ca38dfaeeab9", size = 4604234, upload-time = "2025-09-17T00:08:49.879Z" }, - { url = "https://files.pythonhosted.org/packages/dc/1f/dbd4d6570d84748439237a7478d124ee0134bf166ad129267b7ed8ea6d22/cryptography-46.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e8776dac9e660c22241b6587fae51a67b4b0147daa4d176b172c3ff768ad736", size = 4307669, upload-time = "2025-09-17T00:08:52.321Z" }, - { url = "https://files.pythonhosted.org/packages/ec/fd/ca0a14ce7f0bfe92fa727aacaf2217eb25eb7e4ed513b14d8e03b26e63ed/cryptography-46.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9f40642a140c0c8649987027867242b801486865277cbabc8c6059ddef16dc8b", size = 4947579, upload-time = "2025-09-17T00:08:54.697Z" }, - { url = "https://files.pythonhosted.org/packages/89/6b/09c30543bb93401f6f88fce556b3bdbb21e55ae14912c04b7bf355f5f96c/cryptography-46.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:449ef2b321bec7d97ef2c944173275ebdab78f3abdd005400cc409e27cd159ab", size = 4603669, upload-time = "2025-09-17T00:08:57.16Z" }, - { url = "https://files.pythonhosted.org/packages/23/9a/38cb01cb09ce0adceda9fc627c9cf98eb890fc8d50cacbe79b011df20f8a/cryptography-46.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2dd339ba3345b908fa3141ddba4025568fa6fd398eabce3ef72a29ac2d73ad75", size = 4435828, upload-time = "2025-09-17T00:08:59.606Z" }, - { url = "https://files.pythonhosted.org/packages/0f/53/435b5c36a78d06ae0bef96d666209b0ecd8f8181bfe4dda46536705df59e/cryptography-46.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7411c910fb2a412053cf33cfad0153ee20d27e256c6c3f14d7d7d1d9fec59fd5", size = 4709553, upload-time = "2025-09-17T00:09:01.832Z" }, - { url = "https://files.pythonhosted.org/packages/f5/c4/0da6e55595d9b9cd3b6eb5dc22f3a07ded7f116a3ea72629cab595abb804/cryptography-46.0.1-cp311-abi3-win32.whl", hash = "sha256:cbb8e769d4cac884bb28e3ff620ef1001b75588a5c83c9c9f1fdc9afbe7f29b0", size = 3058327, upload-time = "2025-09-17T00:09:03.726Z" }, - { url = "https://files.pythonhosted.org/packages/95/0f/cd29a35e0d6e78a0ee61793564c8cff0929c38391cb0de27627bdc7525aa/cryptography-46.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:92e8cfe8bd7dd86eac0a677499894862cd5cc2fd74de917daa881d00871ac8e7", size = 3523893, upload-time = "2025-09-17T00:09:06.272Z" }, - { url = "https://files.pythonhosted.org/packages/f2/dd/eea390f3e78432bc3d2f53952375f8b37cb4d37783e626faa6a51e751719/cryptography-46.0.1-cp311-abi3-win_arm64.whl", hash = "sha256:db5597a4c7353b2e5fb05a8e6cb74b56a4658a2b7bf3cb6b1821ae7e7fd6eaa0", size = 2932145, upload-time = "2025-09-17T00:09:08.568Z" }, - { url = "https://files.pythonhosted.org/packages/0a/fb/c73588561afcd5e24b089952bd210b14676c0c5bf1213376350ae111945c/cryptography-46.0.1-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:4c49eda9a23019e11d32a0eb51a27b3e7ddedde91e099c0ac6373e3aacc0d2ee", size = 7193928, upload-time = "2025-09-17T00:09:10.595Z" }, - { url = "https://files.pythonhosted.org/packages/26/34/0ff0bb2d2c79f25a2a63109f3b76b9108a906dd2a2eb5c1d460b9938adbb/cryptography-46.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9babb7818fdd71394e576cf26c5452df77a355eac1a27ddfa24096665a27f8fd", size = 4293515, upload-time = "2025-09-17T00:09:12.861Z" }, - { url = "https://files.pythonhosted.org/packages/df/b7/d4f848aee24ecd1be01db6c42c4a270069a4f02a105d9c57e143daf6cf0f/cryptography-46.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9f2c4cc63be3ef43c0221861177cee5d14b505cd4d4599a89e2cd273c4d3542a", size = 4545619, upload-time = "2025-09-17T00:09:15.397Z" }, - { url = "https://files.pythonhosted.org/packages/44/a5/42fedefc754fd1901e2d95a69815ea4ec8a9eed31f4c4361fcab80288661/cryptography-46.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:41c281a74df173876da1dc9a9b6953d387f06e3d3ed9284e3baae3ab3f40883a", size = 4299160, upload-time = "2025-09-17T00:09:17.155Z" }, - { url = "https://files.pythonhosted.org/packages/86/a1/cd21174f56e769c831fbbd6399a1b7519b0ff6280acec1b826d7b072640c/cryptography-46.0.1-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0a17377fa52563d730248ba1f68185461fff36e8bc75d8787a7dd2e20a802b7a", size = 3994491, upload-time = "2025-09-17T00:09:18.971Z" }, - { url = "https://files.pythonhosted.org/packages/8d/2f/a8cbfa1c029987ddc746fd966711d4fa71efc891d37fbe9f030fe5ab4eec/cryptography-46.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:0d1922d9280e08cde90b518a10cd66831f632960a8d08cb3418922d83fce6f12", size = 4960157, upload-time = "2025-09-17T00:09:20.923Z" }, - { url = "https://files.pythonhosted.org/packages/67/ae/63a84e6789e0d5a2502edf06b552bcb0fa9ff16147265d5c44a211942abe/cryptography-46.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:af84e8e99f1a82cea149e253014ea9dc89f75b82c87bb6c7242203186f465129", size = 4577263, upload-time = "2025-09-17T00:09:23.356Z" }, - { url = "https://files.pythonhosted.org/packages/ef/8f/1b9fa8e92bd9cbcb3b7e1e593a5232f2c1e6f9bd72b919c1a6b37d315f92/cryptography-46.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ef648d2c690703501714588b2ba640facd50fd16548133b11b2859e8655a69da", size = 4298703, upload-time = "2025-09-17T00:09:25.566Z" }, - { url = "https://files.pythonhosted.org/packages/c3/af/bb95db070e73fea3fae31d8a69ac1463d89d1c084220f549b00dd01094a8/cryptography-46.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:e94eb5fa32a8a9f9bf991f424f002913e3dd7c699ef552db9b14ba6a76a6313b", size = 4926363, upload-time = "2025-09-17T00:09:27.451Z" }, - { url = "https://files.pythonhosted.org/packages/f5/3b/d8fb17ffeb3a83157a1cc0aa5c60691d062aceecba09c2e5e77ebfc1870c/cryptography-46.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:534b96c0831855e29fc3b069b085fd185aa5353033631a585d5cd4dd5d40d657", size = 4576958, upload-time = "2025-09-17T00:09:29.924Z" }, - { url = "https://files.pythonhosted.org/packages/d9/46/86bc3a05c10c8aa88c8ae7e953a8b4e407c57823ed201dbcba55c4d655f4/cryptography-46.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9b55038b5c6c47559aa33626d8ecd092f354e23de3c6975e4bb205df128a2a0", size = 4422507, upload-time = "2025-09-17T00:09:32.222Z" }, - { url = "https://files.pythonhosted.org/packages/a8/4e/387e5a21dfd2b4198e74968a541cfd6128f66f8ec94ed971776e15091ac3/cryptography-46.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ec13b7105117dbc9afd023300fb9954d72ca855c274fe563e72428ece10191c0", size = 4683964, upload-time = "2025-09-17T00:09:34.118Z" }, - { url = "https://files.pythonhosted.org/packages/25/a3/f9f5907b166adb8f26762071474b38bbfcf89858a5282f032899075a38a1/cryptography-46.0.1-cp314-cp314t-win32.whl", hash = "sha256:504e464944f2c003a0785b81668fe23c06f3b037e9cb9f68a7c672246319f277", size = 3029705, upload-time = "2025-09-17T00:09:36.381Z" }, - { url = "https://files.pythonhosted.org/packages/12/66/4d3a4f1850db2e71c2b1628d14b70b5e4c1684a1bd462f7fffb93c041c38/cryptography-46.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c52fded6383f7e20eaf70a60aeddd796b3677c3ad2922c801be330db62778e05", size = 3502175, upload-time = "2025-09-17T00:09:38.261Z" }, - { url = "https://files.pythonhosted.org/packages/52/c7/9f10ad91435ef7d0d99a0b93c4360bea3df18050ff5b9038c489c31ac2f5/cryptography-46.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:9495d78f52c804b5ec8878b5b8c7873aa8e63db9cd9ee387ff2db3fffe4df784", size = 2912354, upload-time = "2025-09-17T00:09:40.078Z" }, - { url = "https://files.pythonhosted.org/packages/98/e5/fbd632385542a3311915976f88e0dfcf09e62a3fc0aff86fb6762162a24d/cryptography-46.0.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:d84c40bdb8674c29fa192373498b6cb1e84f882889d21a471b45d1f868d8d44b", size = 7255677, upload-time = "2025-09-17T00:09:42.407Z" }, - { url = "https://files.pythonhosted.org/packages/56/3e/13ce6eab9ad6eba1b15a7bd476f005a4c1b3f299f4c2f32b22408b0edccf/cryptography-46.0.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9ed64e5083fa806709e74fc5ea067dfef9090e5b7a2320a49be3c9df3583a2d8", size = 4301110, upload-time = "2025-09-17T00:09:45.614Z" }, - { url = "https://files.pythonhosted.org/packages/a2/67/65dc233c1ddd688073cf7b136b06ff4b84bf517ba5529607c9d79720fc67/cryptography-46.0.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:341fb7a26bc9d6093c1b124b9f13acc283d2d51da440b98b55ab3f79f2522ead", size = 4562369, upload-time = "2025-09-17T00:09:47.601Z" }, - { url = "https://files.pythonhosted.org/packages/17/db/d64ae4c6f4e98c3dac5bf35dd4d103f4c7c345703e43560113e5e8e31b2b/cryptography-46.0.1-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6ef1488967e729948d424d09c94753d0167ce59afba8d0f6c07a22b629c557b2", size = 4302126, upload-time = "2025-09-17T00:09:49.335Z" }, - { url = "https://files.pythonhosted.org/packages/3d/19/5f1eea17d4805ebdc2e685b7b02800c4f63f3dd46cfa8d4c18373fea46c8/cryptography-46.0.1-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7823bc7cdf0b747ecfb096d004cc41573c2f5c7e3a29861603a2871b43d3ef32", size = 4009431, upload-time = "2025-09-17T00:09:51.239Z" }, - { url = "https://files.pythonhosted.org/packages/81/b5/229ba6088fe7abccbfe4c5edb96c7a5ad547fac5fdd0d40aa6ea540b2985/cryptography-46.0.1-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:f736ab8036796f5a119ff8211deda416f8c15ce03776db704a7a4e17381cb2ef", size = 4980739, upload-time = "2025-09-17T00:09:54.181Z" }, - { url = "https://files.pythonhosted.org/packages/3a/9c/50aa38907b201e74bc43c572f9603fa82b58e831bd13c245613a23cff736/cryptography-46.0.1-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:e46710a240a41d594953012213ea8ca398cd2448fbc5d0f1be8160b5511104a0", size = 4592289, upload-time = "2025-09-17T00:09:56.731Z" }, - { url = "https://files.pythonhosted.org/packages/5a/33/229858f8a5bb22f82468bb285e9f4c44a31978d5f5830bb4ea1cf8a4e454/cryptography-46.0.1-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:84ef1f145de5aee82ea2447224dc23f065ff4cc5791bb3b506615957a6ba8128", size = 4301815, upload-time = "2025-09-17T00:09:58.548Z" }, - { url = "https://files.pythonhosted.org/packages/52/cb/b76b2c87fbd6ed4a231884bea3ce073406ba8e2dae9defad910d33cbf408/cryptography-46.0.1-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9394c7d5a7565ac5f7d9ba38b2617448eba384d7b107b262d63890079fad77ca", size = 4943251, upload-time = "2025-09-17T00:10:00.475Z" }, - { url = "https://files.pythonhosted.org/packages/94/0f/f66125ecf88e4cb5b8017ff43f3a87ede2d064cb54a1c5893f9da9d65093/cryptography-46.0.1-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ed957044e368ed295257ae3d212b95456bd9756df490e1ac4538857f67531fcc", size = 4591247, upload-time = "2025-09-17T00:10:02.874Z" }, - { url = "https://files.pythonhosted.org/packages/f6/22/9f3134ae436b63b463cfdf0ff506a0570da6873adb4bf8c19b8a5b4bac64/cryptography-46.0.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f7de12fa0eee6234de9a9ce0ffcfa6ce97361db7a50b09b65c63ac58e5f22fc7", size = 4428534, upload-time = "2025-09-17T00:10:04.994Z" }, - { url = "https://files.pythonhosted.org/packages/89/39/e6042bcb2638650b0005c752c38ea830cbfbcbb1830e4d64d530000aa8dc/cryptography-46.0.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7fab1187b6c6b2f11a326f33b036f7168f5b996aedd0c059f9738915e4e8f53a", size = 4699541, upload-time = "2025-09-17T00:10:06.925Z" }, - { url = "https://files.pythonhosted.org/packages/68/46/753d457492d15458c7b5a653fc9a84a1c9c7a83af6ebdc94c3fc373ca6e8/cryptography-46.0.1-cp38-abi3-win32.whl", hash = "sha256:45f790934ac1018adeba46a0f7289b2b8fe76ba774a88c7f1922213a56c98bc1", size = 3043779, upload-time = "2025-09-17T00:10:08.951Z" }, - { url = "https://files.pythonhosted.org/packages/2f/50/b6f3b540c2f6ee712feeb5fa780bb11fad76634e71334718568e7695cb55/cryptography-46.0.1-cp38-abi3-win_amd64.whl", hash = "sha256:7176a5ab56fac98d706921f6416a05e5aff7df0e4b91516f450f8627cda22af3", size = 3517226, upload-time = "2025-09-17T00:10:10.769Z" }, - { url = "https://files.pythonhosted.org/packages/ff/e8/77d17d00981cdd27cc493e81e1749a0b8bbfb843780dbd841e30d7f50743/cryptography-46.0.1-cp38-abi3-win_arm64.whl", hash = "sha256:efc9e51c3e595267ff84adf56e9b357db89ab2279d7e375ffcaf8f678606f3d9", size = 2923149, upload-time = "2025-09-17T00:10:13.236Z" }, + { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, + { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, + { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, + { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, + { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, + { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, + { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, + { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, + { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, + { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, ] [[package]] @@ -1252,7 +1252,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "argon2-cffi", specifier = ">=25.1.0" }, - { name = "cryptography", specifier = ">=46.0.1" }, + { name = "cryptography", specifier = ">=46.0.3" }, { name = "django", specifier = "==5.2.7" }, { name = "django-allauth", specifier = ">=65.11.2" }, { name = "django-auditlog", specifier = ">=3.2.1" }, From 8ba9787d4bc68b360d6bc789d38f447b68f18189 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Fri, 17 Oct 2025 06:11:27 +0200 Subject: [PATCH 061/185] Fix "Add item" button adding multiple items ref #224 --- src/servala/static/js/dynamic-array.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/servala/static/js/dynamic-array.js b/src/servala/static/js/dynamic-array.js index c198ddf..b6fa4e5 100644 --- a/src/servala/static/js/dynamic-array.js +++ b/src/servala/static/js/dynamic-array.js @@ -7,6 +7,10 @@ const initDynamicArrayWidget = () => { const containers = document.querySelectorAll('.dynamic-array-widget') containers.forEach(container => { + if (container.dataset.initialized === 'true') { + return + } + const itemsContainer = container.querySelector('.array-items') const addButton = container.querySelector('.add-array-item') const hiddenInput = container.querySelector('input[type="hidden"]') @@ -22,6 +26,7 @@ const initDynamicArrayWidget = () => { // Ensure hidden input is synced with visible inputs on initialization updateHiddenInput(container) + container.dataset.initialized = 'true' }) } From 014e88aa24f9e1d2c40f8cf6129fd7a18d193d0e Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 17 Oct 2025 10:35:47 +0200 Subject: [PATCH 062/185] remove the help text about billing entity being read only The issue is that when obj.has_inherited_billing_entity is True, the code adds "billing_entity" to the readonly fields. When a field is marked as readonly, Django may exclude it from form.base_fields, which causes a KeyError when trying to access it. --- src/servala/core/admin.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/servala/core/admin.py b/src/servala/core/admin.py index 1aec22a..c0beb9e 100644 --- a/src/servala/core/admin.py +++ b/src/servala/core/admin.py @@ -73,16 +73,6 @@ class OrganizationAdmin(admin.ModelAdmin): return readonly_fields - def get_form(self, request, obj=None, **kwargs): - form = super().get_form(request, obj, **kwargs) - - if obj and obj.has_inherited_billing_entity: - form.base_fields["billing_entity"].help_text = _( - "This billing entity is inherited from the organization's origin and cannot be modified." - ) - - return form - @admin.register(BillingEntity) class BillingEntityAdmin(admin.ModelAdmin): From 54998ab9d0f6265297afb17ba6330380cc9eef26 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 17 Oct 2025 10:40:00 +0200 Subject: [PATCH 063/185] explicitely convert is_accepted to boolean The is_accepted property returns self.accepted_by or self.accepted_at. When accepted_by is a User object (not None), it returns the User object instead of a boolean. The Django admin's boolean field renderer expects a boolean value (True, False, or None), not a User object. --- src/servala/core/models/organization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/servala/core/models/organization.py b/src/servala/core/models/organization.py index d308bfa..1669f39 100644 --- a/src/servala/core/models/organization.py +++ b/src/servala/core/models/organization.py @@ -485,7 +485,7 @@ class OrganizationInvitation(ServalaModelMixin, models.Model): def is_accepted(self): # We check both accepted_by and accepted_at to avoid a deleted user # freeing up an invitation - return self.accepted_by or self.accepted_at + return bool(self.accepted_by or self.accepted_at) @property def can_be_accepted(self): From ce34afa10a8dd18aebaa13666bc3eff4180c76e4 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 17 Oct 2025 10:54:43 +0200 Subject: [PATCH 064/185] service enabling / disabling is not specific to a csp --- .../frontend/templates/frontend/organizations/services.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/servala/frontend/templates/frontend/organizations/services.html b/src/servala/frontend/templates/frontend/organizations/services.html index 4b8c95a..461d37d 100644 --- a/src/servala/frontend/templates/frontend/organizations/services.html +++ b/src/servala/frontend/templates/frontend/organizations/services.html @@ -35,7 +35,7 @@
{% translate "You may also be interested in one of these …" %}

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

From ed4fbb3c1db7450b382716778ce50fe670900ab4 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 18 Oct 2025 03:01:48 +0000 Subject: [PATCH 065/185] Update dependency psycopg2-binary to >=2.9.11 --- pyproject.toml | 2 +- uv.lock | 35 +++++++++++++++++++++-------------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2860f5d..2d4f8e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ dependencies = [ "jsonschema>=4.25.1", "kubernetes>=33.1.0", "pillow>=11.3.0", - "psycopg2-binary>=2.9.10", + "psycopg2-binary>=2.9.11", "pyjwt>=2.10.1", "requests>=2.32.5", "rules>=3.5", diff --git a/uv.lock b/uv.lock index 82eee80..93a4b2e 100644 --- a/uv.lock +++ b/uv.lock @@ -838,21 +838,28 @@ wheels = [ [[package]] name = "psycopg2-binary" -version = "2.9.10" +version = "2.9.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/bdc8274dc0585090b4e3432267d7be4dfbfd8971c0fa59167c711105a6bf/psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", size = 385764, upload-time = "2024-10-16T11:24:58.126Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/30/d41d3ba765609c0763505d565c4d12d8f3c79793f0d0f044ff5a28bf395b/psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d", size = 3044699, upload-time = "2024-10-16T11:21:42.841Z" }, - { url = "https://files.pythonhosted.org/packages/35/44/257ddadec7ef04536ba71af6bc6a75ec05c5343004a7ec93006bee66c0bc/psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb", size = 3275245, upload-time = "2024-10-16T11:21:51.989Z" }, - { url = "https://files.pythonhosted.org/packages/1b/11/48ea1cd11de67f9efd7262085588790a95d9dfcd9b8a687d46caf7305c1a/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7", size = 2851631, upload-time = "2024-10-16T11:21:57.584Z" }, - { url = "https://files.pythonhosted.org/packages/62/e0/62ce5ee650e6c86719d621a761fe4bc846ab9eff8c1f12b1ed5741bf1c9b/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d", size = 3082140, upload-time = "2024-10-16T11:22:02.005Z" }, - { url = "https://files.pythonhosted.org/packages/27/ce/63f946c098611f7be234c0dd7cb1ad68b0b5744d34f68062bb3c5aa510c8/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73", size = 3264762, upload-time = "2024-10-16T11:22:06.412Z" }, - { url = "https://files.pythonhosted.org/packages/43/25/c603cd81402e69edf7daa59b1602bd41eb9859e2824b8c0855d748366ac9/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673", size = 3020967, upload-time = "2024-10-16T11:22:11.583Z" }, - { url = "https://files.pythonhosted.org/packages/5f/d6/8708d8c6fca531057fa170cdde8df870e8b6a9b136e82b361c65e42b841e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f", size = 2872326, upload-time = "2024-10-16T11:22:16.406Z" }, - { url = "https://files.pythonhosted.org/packages/ce/ac/5b1ea50fc08a9df82de7e1771537557f07c2632231bbab652c7e22597908/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", size = 2822712, upload-time = "2024-10-16T11:22:21.366Z" }, - { url = "https://files.pythonhosted.org/packages/c4/fc/504d4503b2abc4570fac3ca56eb8fed5e437bf9c9ef13f36b6621db8ef00/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", size = 2920155, upload-time = "2024-10-16T11:22:25.684Z" }, - { url = "https://files.pythonhosted.org/packages/b2/d1/323581e9273ad2c0dbd1902f3fb50c441da86e894b6e25a73c3fda32c57e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", size = 2959356, upload-time = "2024-10-16T11:22:30.562Z" }, - { url = "https://files.pythonhosted.org/packages/08/50/d13ea0a054189ae1bc21af1d85b6f8bb9bbc5572991055d70ad9006fe2d6/psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", size = 2569224, upload-time = "2025-01-04T20:09:19.234Z" }, + { url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" }, + { url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" }, + { url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" }, + { url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" }, + { url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" }, + { url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" }, + { url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" }, + { url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" }, + { url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" }, + { url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" }, + { url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" }, + { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" }, ] [[package]] @@ -1264,7 +1271,7 @@ requires-dist = [ { name = "jsonschema", specifier = ">=4.25.1" }, { name = "kubernetes", specifier = ">=33.1.0" }, { name = "pillow", specifier = ">=11.3.0" }, - { name = "psycopg2-binary", specifier = ">=2.9.10" }, + { name = "psycopg2-binary", specifier = ">=2.9.11" }, { name = "pyjwt", specifier = ">=2.10.1" }, { name = "requests", specifier = ">=2.32.5" }, { name = "rules", specifier = ">=3.5" }, From 5790e157bc1b0d8919edd4c4366a857cf5a590aa Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 18 Oct 2025 03:01:53 +0000 Subject: [PATCH 066/185] Update https://github.com/renovatebot/github-action action to v43.0.17 --- .forgejo/workflows/renovate.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/renovate.yaml b/.forgejo/workflows/renovate.yaml index 97f1c2b..9821f8f 100644 --- a/.forgejo/workflows/renovate.yaml +++ b/.forgejo/workflows/renovate.yaml @@ -19,7 +19,7 @@ jobs: node-version: "22" - name: Renovate - uses: https://github.com/renovatebot/github-action@v43.0.14 + uses: https://github.com/renovatebot/github-action@v43.0.17 with: token: ${{ secrets.RENOVATE_TOKEN }} env: From 0ef1678c3c9477f14712b2b0f8eb95371b2a57f5 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 18 Oct 2025 03:01:58 +0000 Subject: [PATCH 067/185] Update dependency django-allauth to >=65.12.1 --- pyproject.toml | 2 +- uv.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2860f5d..bfab895 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ dependencies = [ "argon2-cffi>=25.1.0", "cryptography>=46.0.3", "django==5.2.7", - "django-allauth>=65.11.2", + "django-allauth>=65.12.1", "django-auditlog>=3.3.0", "django-fernet-encrypted-fields>=0.3.0", "django-jsonform>=2.23.2", diff --git a/uv.lock b/uv.lock index 82eee80..1b524b1 100644 --- a/uv.lock +++ b/uv.lock @@ -421,13 +421,13 @@ wheels = [ [[package]] name = "django-allauth" -version = "65.11.2" +version = "65.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref" }, { name = "django" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b7/19/3671e67b5fcc744c0d9380b0bb6120b7226bc9944bd9affb029b2d510d53/django_allauth-65.11.2.tar.gz", hash = "sha256:7b7e771d3384d0e247d0d6aef31b0cb589f92305b7e975e70056a513525906e7", size = 1916225, upload-time = "2025-09-09T18:37:19.55Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/94/75d7f8c59e061d1b66a6d917b287817fe02d2671c9e6376a4ddfb3954989/django_allauth-65.12.1.tar.gz", hash = "sha256:662666ff2d5c71766f66b1629ac7345c30796813221184e13e11ed7460940c6a", size = 1967971, upload-time = "2025-10-16T16:39:58.342Z" } [[package]] name = "django-auditlog" @@ -1254,7 +1254,7 @@ requires-dist = [ { name = "argon2-cffi", specifier = ">=25.1.0" }, { name = "cryptography", specifier = ">=46.0.3" }, { name = "django", specifier = "==5.2.7" }, - { name = "django-allauth", specifier = ">=65.11.2" }, + { name = "django-allauth", specifier = ">=65.12.1" }, { name = "django-auditlog", specifier = ">=3.3.0" }, { name = "django-fernet-encrypted-fields", specifier = ">=0.3.0" }, { name = "django-jsonform", specifier = ">=2.23.2" }, From 71cf20ac1a707975f8c8faf34e262b170783af6b Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sun, 19 Oct 2025 03:01:43 +0000 Subject: [PATCH 068/185] Update dependency coverage to >=7.11.0 --- pyproject.toml | 2 +- uv.lock | 112 ++++++++++++++++++++++++------------------------- 2 files changed, 57 insertions(+), 57 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2860f5d..0bf6a31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ dependencies = [ dev = [ "black>=25.9.0", "bumpver>=2025.1131", - "coverage>=7.10.7", + "coverage>=7.11.0", "djlint>=1.36.4", "flake8>=7.3.0", "flake8-bugbear>=24.12.12", diff --git a/uv.lock b/uv.lock index 82eee80..ced1dfb 100644 --- a/uv.lock +++ b/uv.lock @@ -276,63 +276,63 @@ wheels = [ [[package]] name = "coverage" -version = "7.10.7" +version = "7.11.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/38/ee22495420457259d2f3390309505ea98f98a5eed40901cf62196abad006/coverage-7.11.0.tar.gz", hash = "sha256:167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050", size = 811905, upload-time = "2025-10-15T15:15:08.542Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, - { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, - { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, - { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, - { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, - { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, - { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, - { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, - { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, - { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, - { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, - { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, - { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, - { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, - { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, - { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, - { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, - { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, - { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, - { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, - { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, - { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, - { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, - { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, - { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, - { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, - { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, - { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, - { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, - { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, - { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, - { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, - { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, - { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, - { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, - { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, - { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, - { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, - { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, - { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, - { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, - { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, - { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, - { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, - { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, - { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, - { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, - { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, - { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, - { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, - { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, + { url = "https://files.pythonhosted.org/packages/60/7f/85e4dfe65e400645464b25c036a26ac226cf3a69d4a50c3934c532491cdd/coverage-7.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cc3f49e65ea6e0d5d9bd60368684fe52a704d46f9e7fc413918f18d046ec40e1", size = 216129, upload-time = "2025-10-15T15:13:25.371Z" }, + { url = "https://files.pythonhosted.org/packages/96/5d/dc5fa98fea3c175caf9d360649cb1aa3715e391ab00dc78c4c66fabd7356/coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f39ae2f63f37472c17b4990f794035c9890418b1b8cca75c01193f3c8d3e01be", size = 216380, upload-time = "2025-10-15T15:13:26.976Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f5/3da9cc9596708273385189289c0e4d8197d37a386bdf17619013554b3447/coverage-7.11.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7db53b5cdd2917b6eaadd0b1251cf4e7d96f4a8d24e174bdbdf2f65b5ea7994d", size = 247375, upload-time = "2025-10-15T15:13:28.923Z" }, + { url = "https://files.pythonhosted.org/packages/65/6c/f7f59c342359a235559d2bc76b0c73cfc4bac7d61bb0df210965cb1ecffd/coverage-7.11.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10ad04ac3a122048688387828b4537bc9cf60c0bf4869c1e9989c46e45690b82", size = 249978, upload-time = "2025-10-15T15:13:30.525Z" }, + { url = "https://files.pythonhosted.org/packages/e7/8c/042dede2e23525e863bf1ccd2b92689692a148d8b5fd37c37899ba882645/coverage-7.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4036cc9c7983a2b1f2556d574d2eb2154ac6ed55114761685657e38782b23f52", size = 251253, upload-time = "2025-10-15T15:13:32.174Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a9/3c58df67bfa809a7bddd786356d9c5283e45d693edb5f3f55d0986dd905a/coverage-7.11.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ab934dd13b1c5e94b692b1e01bd87e4488cb746e3a50f798cb9464fd128374b", size = 247591, upload-time = "2025-10-15T15:13:34.147Z" }, + { url = "https://files.pythonhosted.org/packages/26/5b/c7f32efd862ee0477a18c41e4761305de6ddd2d49cdeda0c1116227570fd/coverage-7.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59a6e5a265f7cfc05f76e3bb53eca2e0dfe90f05e07e849930fecd6abb8f40b4", size = 249411, upload-time = "2025-10-15T15:13:38.425Z" }, + { url = "https://files.pythonhosted.org/packages/76/b5/78cb4f1e86c1611431c990423ec0768122905b03837e1b4c6a6f388a858b/coverage-7.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:df01d6c4c81e15a7c88337b795bb7595a8596e92310266b5072c7e301168efbd", size = 247303, upload-time = "2025-10-15T15:13:40.464Z" }, + { url = "https://files.pythonhosted.org/packages/87/c9/23c753a8641a330f45f221286e707c427e46d0ffd1719b080cedc984ec40/coverage-7.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8c934bd088eed6174210942761e38ee81d28c46de0132ebb1801dbe36a390dcc", size = 247157, upload-time = "2025-10-15T15:13:42.087Z" }, + { url = "https://files.pythonhosted.org/packages/c5/42/6e0cc71dc8a464486e944a4fa0d85bdec031cc2969e98ed41532a98336b9/coverage-7.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a03eaf7ec24078ad64a07f02e30060aaf22b91dedf31a6b24d0d98d2bba7f48", size = 248921, upload-time = "2025-10-15T15:13:43.715Z" }, + { url = "https://files.pythonhosted.org/packages/e8/1c/743c2ef665e6858cccb0f84377dfe3a4c25add51e8c7ef19249be92465b6/coverage-7.11.0-cp313-cp313-win32.whl", hash = "sha256:695340f698a5f56f795b2836abe6fb576e7c53d48cd155ad2f80fd24bc63a040", size = 218526, upload-time = "2025-10-15T15:13:45.336Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d5/226daadfd1bf8ddbccefbd3aa3547d7b960fb48e1bdac124e2dd13a2b71a/coverage-7.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:2727d47fce3ee2bac648528e41455d1b0c46395a087a229deac75e9f88ba5a05", size = 219317, upload-time = "2025-10-15T15:13:47.401Z" }, + { url = "https://files.pythonhosted.org/packages/97/54/47db81dcbe571a48a298f206183ba8a7ba79200a37cd0d9f4788fcd2af4a/coverage-7.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:0efa742f431529699712b92ecdf22de8ff198df41e43aeaaadf69973eb93f17a", size = 217948, upload-time = "2025-10-15T15:13:49.096Z" }, + { url = "https://files.pythonhosted.org/packages/e5/8b/cb68425420154e7e2a82fd779a8cc01549b6fa83c2ad3679cd6c088ebd07/coverage-7.11.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:587c38849b853b157706407e9ebdca8fd12f45869edb56defbef2daa5fb0812b", size = 216837, upload-time = "2025-10-15T15:13:51.09Z" }, + { url = "https://files.pythonhosted.org/packages/33/55/9d61b5765a025685e14659c8d07037247de6383c0385757544ffe4606475/coverage-7.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b971bdefdd75096163dd4261c74be813c4508477e39ff7b92191dea19f24cd37", size = 217061, upload-time = "2025-10-15T15:13:52.747Z" }, + { url = "https://files.pythonhosted.org/packages/52/85/292459c9186d70dcec6538f06ea251bc968046922497377bf4a1dc9a71de/coverage-7.11.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:269bfe913b7d5be12ab13a95f3a76da23cf147be7fa043933320ba5625f0a8de", size = 258398, upload-time = "2025-10-15T15:13:54.45Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e2/46edd73fb8bf51446c41148d81944c54ed224854812b6ca549be25113ee0/coverage-7.11.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dadbcce51a10c07b7c72b0ce4a25e4b6dcb0c0372846afb8e5b6307a121eb99f", size = 260574, upload-time = "2025-10-15T15:13:56.145Z" }, + { url = "https://files.pythonhosted.org/packages/07/5e/1df469a19007ff82e2ca8fe509822820a31e251f80ee7344c34f6cd2ec43/coverage-7.11.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ed43fa22c6436f7957df036331f8fe4efa7af132054e1844918866cd228af6c", size = 262797, upload-time = "2025-10-15T15:13:58.635Z" }, + { url = "https://files.pythonhosted.org/packages/f9/50/de216b31a1434b94d9b34a964c09943c6be45069ec704bfc379d8d89a649/coverage-7.11.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9516add7256b6713ec08359b7b05aeff8850c98d357784c7205b2e60aa2513fa", size = 257361, upload-time = "2025-10-15T15:14:00.409Z" }, + { url = "https://files.pythonhosted.org/packages/82/1e/3f9f8344a48111e152e0fd495b6fff13cc743e771a6050abf1627a7ba918/coverage-7.11.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb92e47c92fcbcdc692f428da67db33337fa213756f7adb6a011f7b5a7a20740", size = 260349, upload-time = "2025-10-15T15:14:02.188Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/3f52741f9e7d82124272f3070bbe316006a7de1bad1093f88d59bfc6c548/coverage-7.11.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d06f4fc7acf3cabd6d74941d53329e06bab00a8fe10e4df2714f0b134bfc64ef", size = 258114, upload-time = "2025-10-15T15:14:03.907Z" }, + { url = "https://files.pythonhosted.org/packages/0b/8b/918f0e15f0365d50d3986bbd3338ca01178717ac5678301f3f547b6619e6/coverage-7.11.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:6fbcee1a8f056af07ecd344482f711f563a9eb1c2cad192e87df00338ec3cdb0", size = 256723, upload-time = "2025-10-15T15:14:06.324Z" }, + { url = "https://files.pythonhosted.org/packages/44/9e/7776829f82d3cf630878a7965a7d70cc6ca94f22c7d20ec4944f7148cb46/coverage-7.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dbbf012be5f32533a490709ad597ad8a8ff80c582a95adc8d62af664e532f9ca", size = 259238, upload-time = "2025-10-15T15:14:08.002Z" }, + { url = "https://files.pythonhosted.org/packages/9a/b8/49cf253e1e7a3bedb85199b201862dd7ca4859f75b6cf25ffa7298aa0760/coverage-7.11.0-cp313-cp313t-win32.whl", hash = "sha256:cee6291bb4fed184f1c2b663606a115c743df98a537c969c3c64b49989da96c2", size = 219180, upload-time = "2025-10-15T15:14:09.786Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e1/1a541703826be7ae2125a0fb7f821af5729d56bb71e946e7b933cc7a89a4/coverage-7.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a386c1061bf98e7ea4758e4313c0ab5ecf57af341ef0f43a0bf26c2477b5c268", size = 220241, upload-time = "2025-10-15T15:14:11.471Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d1/5ee0e0a08621140fd418ec4020f595b4d52d7eb429ae6a0c6542b4ba6f14/coverage-7.11.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f9ea02ef40bb83823b2b04964459d281688fe173e20643870bb5d2edf68bc836", size = 218510, upload-time = "2025-10-15T15:14:13.46Z" }, + { url = "https://files.pythonhosted.org/packages/f4/06/e923830c1985ce808e40a3fa3eb46c13350b3224b7da59757d37b6ce12b8/coverage-7.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c770885b28fb399aaf2a65bbd1c12bf6f307ffd112d6a76c5231a94276f0c497", size = 216110, upload-time = "2025-10-15T15:14:15.157Z" }, + { url = "https://files.pythonhosted.org/packages/42/82/cdeed03bfead45203fb651ed756dfb5266028f5f939e7f06efac4041dad5/coverage-7.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3d0e2087dba64c86a6b254f43e12d264b636a39e88c5cc0a01a7c71bcfdab7e", size = 216395, upload-time = "2025-10-15T15:14:16.863Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ba/e1c80caffc3199aa699813f73ff097bc2df7b31642bdbc7493600a8f1de5/coverage-7.11.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73feb83bb41c32811973b8565f3705caf01d928d972b72042b44e97c71fd70d1", size = 247433, upload-time = "2025-10-15T15:14:18.589Z" }, + { url = "https://files.pythonhosted.org/packages/80/c0/5b259b029694ce0a5bbc1548834c7ba3db41d3efd3474489d7efce4ceb18/coverage-7.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c6f31f281012235ad08f9a560976cc2fc9c95c17604ff3ab20120fe480169bca", size = 249970, upload-time = "2025-10-15T15:14:20.307Z" }, + { url = "https://files.pythonhosted.org/packages/8c/86/171b2b5e1aac7e2fd9b43f7158b987dbeb95f06d1fbecad54ad8163ae3e8/coverage-7.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9570ad567f880ef675673992222746a124b9595506826b210fbe0ce3f0499cd", size = 251324, upload-time = "2025-10-15T15:14:22.419Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/7e10414d343385b92024af3932a27a1caf75c6e27ee88ba211221ff1a145/coverage-7.11.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8badf70446042553a773547a61fecaa734b55dc738cacf20c56ab04b77425e43", size = 247445, upload-time = "2025-10-15T15:14:24.205Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3b/e4f966b21f5be8c4bf86ad75ae94efa0de4c99c7bbb8114476323102e345/coverage-7.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a09c1211959903a479e389685b7feb8a17f59ec5a4ef9afde7650bd5eabc2777", size = 249324, upload-time = "2025-10-15T15:14:26.234Z" }, + { url = "https://files.pythonhosted.org/packages/00/a2/8479325576dfcd909244d0df215f077f47437ab852ab778cfa2f8bf4d954/coverage-7.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:5ef83b107f50db3f9ae40f69e34b3bd9337456c5a7fe3461c7abf8b75dd666a2", size = 247261, upload-time = "2025-10-15T15:14:28.42Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d8/3a9e2db19d94d65771d0f2e21a9ea587d11b831332a73622f901157cc24b/coverage-7.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f91f927a3215b8907e214af77200250bb6aae36eca3f760f89780d13e495388d", size = 247092, upload-time = "2025-10-15T15:14:30.784Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b1/bbca3c472544f9e2ad2d5116b2379732957048be4b93a9c543fcd0207e5f/coverage-7.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbcd376716d6b7fbfeedd687a6c4be019c5a5671b35f804ba76a4c0a778cba4", size = 248755, upload-time = "2025-10-15T15:14:32.585Z" }, + { url = "https://files.pythonhosted.org/packages/89/49/638d5a45a6a0f00af53d6b637c87007eb2297042186334e9923a61aa8854/coverage-7.11.0-cp314-cp314-win32.whl", hash = "sha256:bab7ec4bb501743edc63609320aaec8cd9188b396354f482f4de4d40a9d10721", size = 218793, upload-time = "2025-10-15T15:14:34.972Z" }, + { url = "https://files.pythonhosted.org/packages/30/cc/b675a51f2d068adb3cdf3799212c662239b0ca27f4691d1fff81b92ea850/coverage-7.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d4ba9a449e9364a936a27322b20d32d8b166553bfe63059bd21527e681e2fad", size = 219587, upload-time = "2025-10-15T15:14:37.047Z" }, + { url = "https://files.pythonhosted.org/packages/93/98/5ac886876026de04f00820e5094fe22166b98dcb8b426bf6827aaf67048c/coverage-7.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:ce37f215223af94ef0f75ac68ea096f9f8e8c8ec7d6e8c346ee45c0d363f0479", size = 218168, upload-time = "2025-10-15T15:14:38.861Z" }, + { url = "https://files.pythonhosted.org/packages/14/d1/b4145d35b3e3ecf4d917e97fc8895bcf027d854879ba401d9ff0f533f997/coverage-7.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f413ce6e07e0d0dc9c433228727b619871532674b45165abafe201f200cc215f", size = 216850, upload-time = "2025-10-15T15:14:40.651Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d1/7f645fc2eccd318369a8a9948acc447bb7c1ade2911e31d3c5620544c22b/coverage-7.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05791e528a18f7072bf5998ba772fe29db4da1234c45c2087866b5ba4dea710e", size = 217071, upload-time = "2025-10-15T15:14:42.755Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/64d124649db2737ceced1dfcbdcb79898d5868d311730f622f8ecae84250/coverage-7.11.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cacb29f420cfeb9283b803263c3b9a068924474ff19ca126ba9103e1278dfa44", size = 258570, upload-time = "2025-10-15T15:14:44.542Z" }, + { url = "https://files.pythonhosted.org/packages/6c/3f/6f5922f80dc6f2d8b2c6f974835c43f53eb4257a7797727e6ca5b7b2ec1f/coverage-7.11.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314c24e700d7027ae3ab0d95fbf8d53544fca1f20345fd30cd219b737c6e58d3", size = 260738, upload-time = "2025-10-15T15:14:46.436Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5f/9e883523c4647c860b3812b417a2017e361eca5b635ee658387dc11b13c1/coverage-7.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:630d0bd7a293ad2fc8b4b94e5758c8b2536fdf36c05f1681270203e463cbfa9b", size = 262994, upload-time = "2025-10-15T15:14:48.3Z" }, + { url = "https://files.pythonhosted.org/packages/07/bb/43b5a8e94c09c8bf51743ffc65c4c841a4ca5d3ed191d0a6919c379a1b83/coverage-7.11.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e89641f5175d65e2dbb44db15fe4ea48fade5d5bbb9868fdc2b4fce22f4a469d", size = 257282, upload-time = "2025-10-15T15:14:50.236Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e5/0ead8af411411330b928733e1d201384b39251a5f043c1612970310e8283/coverage-7.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c9f08ea03114a637dab06cedb2e914da9dc67fa52c6015c018ff43fdde25b9c2", size = 260430, upload-time = "2025-10-15T15:14:52.413Z" }, + { url = "https://files.pythonhosted.org/packages/ae/66/03dd8bb0ba5b971620dcaac145461950f6d8204953e535d2b20c6b65d729/coverage-7.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce9f3bde4e9b031eaf1eb61df95c1401427029ea1bfddb8621c1161dcb0fa02e", size = 258190, upload-time = "2025-10-15T15:14:54.268Z" }, + { url = "https://files.pythonhosted.org/packages/45/ae/28a9cce40bf3174426cb2f7e71ee172d98e7f6446dff936a7ccecee34b14/coverage-7.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:e4dc07e95495923d6fd4d6c27bf70769425b71c89053083843fd78f378558996", size = 256658, upload-time = "2025-10-15T15:14:56.436Z" }, + { url = "https://files.pythonhosted.org/packages/5c/7c/3a44234a8599513684bfc8684878fd7b126c2760f79712bb78c56f19efc4/coverage-7.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:424538266794db2861db4922b05d729ade0940ee69dcf0591ce8f69784db0e11", size = 259342, upload-time = "2025-10-15T15:14:58.538Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e6/0108519cba871af0351725ebdb8660fd7a0fe2ba3850d56d32490c7d9b4b/coverage-7.11.0-cp314-cp314t-win32.whl", hash = "sha256:4c1eeb3fb8eb9e0190bebafd0462936f75717687117339f708f395fe455acc73", size = 219568, upload-time = "2025-10-15T15:15:00.382Z" }, + { url = "https://files.pythonhosted.org/packages/c9/76/44ba876e0942b4e62fdde23ccb029ddb16d19ba1bef081edd00857ba0b16/coverage-7.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b56efee146c98dbf2cf5cffc61b9829d1e94442df4d7398b26892a53992d3547", size = 220687, upload-time = "2025-10-15T15:15:02.322Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0c/0df55ecb20d0d0ed5c322e10a441775e1a3a5d78c60f0c4e1abfe6fcf949/coverage-7.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b5c2705afa83f49bd91962a4094b6b082f94aef7626365ab3f8f4bd159c5acf3", size = 218711, upload-time = "2025-10-15T15:15:04.575Z" }, + { url = "https://files.pythonhosted.org/packages/5f/04/642c1d8a448ae5ea1369eac8495740a79eb4e581a9fb0cbdce56bbf56da1/coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68", size = 207761, upload-time = "2025-10-15T15:15:06.439Z" }, ] [[package]] @@ -1276,7 +1276,7 @@ requires-dist = [ dev = [ { name = "black", specifier = ">=25.9.0" }, { name = "bumpver", specifier = ">=2025.1131" }, - { name = "coverage", specifier = ">=7.10.7" }, + { name = "coverage", specifier = ">=7.11.0" }, { name = "djlint", specifier = ">=1.36.4" }, { name = "flake8", specifier = ">=7.3.0" }, { name = "flake8-bugbear", specifier = ">=24.12.12" }, From 61c1aabeb0cdf15253e1754617d42484eaeef5b8 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 20 Oct 2025 03:01:57 +0000 Subject: [PATCH 069/185] Update dependency isort to v7 --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2860f5d..0adbab5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ dev = [ "flake8>=7.3.0", "flake8-bugbear>=24.12.12", "flake8-pyproject>=1.2.3", - "isort>=6.1.0", + "isort>=7.0.0", "pytest>=8.4.2", "pytest-cov>=7.0.0", "pytest-django>=4.11.1", diff --git a/uv.lock b/uv.lock index 82eee80..136213e 100644 --- a/uv.lock +++ b/uv.lock @@ -622,11 +622,11 @@ wheels = [ [[package]] name = "isort" -version = "6.1.0" +version = "7.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1e/82/fa43935523efdfcce6abbae9da7f372b627b27142c3419fcf13bf5b0c397/isort-6.1.0.tar.gz", hash = "sha256:9b8f96a14cfee0677e78e941ff62f03769a06d412aabb9e2a90487b3b7e8d481", size = 824325, upload-time = "2025-10-01T16:26:45.027Z" } +sdist = { url = "https://files.pythonhosted.org/packages/63/53/4f3c058e3bace40282876f9b553343376ee687f3c35a525dc79dbd450f88/isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187", size = 805049, upload-time = "2025-10-11T13:30:59.107Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/cc/9b681a170efab4868a032631dea1e8446d8ec718a7f657b94d49d1a12643/isort-6.1.0-py3-none-any.whl", hash = "sha256:58d8927ecce74e5087aef019f778d4081a3b6c98f15a80ba35782ca8a2097784", size = 94329, upload-time = "2025-10-01T16:26:43.291Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ed/e3705d6d02b4f7aea715a353c8ce193efd0b5db13e204df895d38734c244/isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1", size = 94672, upload-time = "2025-10-11T13:30:57.665Z" }, ] [[package]] @@ -1281,7 +1281,7 @@ dev = [ { name = "flake8", specifier = ">=7.3.0" }, { name = "flake8-bugbear", specifier = ">=24.12.12" }, { name = "flake8-pyproject", specifier = ">=1.2.3" }, - { name = "isort", specifier = ">=6.1.0" }, + { name = "isort", specifier = ">=7.0.0" }, { name = "pytest", specifier = ">=8.4.2" }, { name = "pytest-cov", specifier = ">=7.0.0" }, { name = "pytest-django", specifier = ">=4.11.1" }, From 359bc587496d915a7aa0bf8fdf294ebf50b48cfc Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Mon, 20 Oct 2025 11:56:35 +0200 Subject: [PATCH 070/185] Fix advanced fields not working with array fields ref #204 --- .../frontend/templates/frontend/forms/dynamic_array.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/servala/frontend/templates/frontend/forms/dynamic_array.html b/src/servala/frontend/templates/frontend/forms/dynamic_array.html index 4b7e68c..9d61825 100644 --- a/src/servala/frontend/templates/frontend/forms/dynamic_array.html +++ b/src/servala/frontend/templates/frontend/forms/dynamic_array.html @@ -1,6 +1,9 @@
+ data-name="{{ widget.name }}" + {% for name, value in widget.attrs.items %}{% if value is not False and name != "id" and name != "class" %} {{ name }}{% if value is not True %}="{{ value|stringformat:'s' }}"{% endif %} + {% endif %} + {% endfor %}>
{% for item in value_list %}
From 864c0ffc06c7c597a55d6048cea775736df14a61 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Mon, 20 Oct 2025 13:54:33 +0200 Subject: [PATCH 071/185] Fix advanced fields not working with categories ref #204 --- src/servala/core/crd.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/servala/core/crd.py b/src/servala/core/crd.py index 276e9c2..35d9240 100644 --- a/src/servala/core/crd.py +++ b/src/servala/core/crd.py @@ -330,7 +330,9 @@ class CrdModelFormMixin: # Mark advanced fields with a CSS class and data attribute advanced_fields = getattr(self, "ADVANCED_FIELDS", []) for name, field in self.fields.items(): - if name in advanced_fields: + if name in advanced_fields or any( + name.startswith(f"{af}.") for af in advanced_fields + ): field.widget.attrs.update( { "class": ( From 850a79185117bccf0c762b9c51f2385be5169658 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Mon, 20 Oct 2025 15:02:41 +0200 Subject: [PATCH 072/185] Handle whole form sections being advanced ref #204 --- src/servala/core/crd.py | 23 +++++++++++++++---- .../includes/tabbed_fieldset_form.html | 13 +++++++---- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/servala/core/crd.py b/src/servala/core/crd.py index 35d9240..fe8edbb 100644 --- a/src/servala/core/crd.py +++ b/src/servala/core/crd.py @@ -328,11 +328,8 @@ class CrdModelFormMixin: field.required = False # Mark advanced fields with a CSS class and data attribute - advanced_fields = getattr(self, "ADVANCED_FIELDS", []) for name, field in self.fields.items(): - if name in advanced_fields or any( - name.startswith(f"{af}.") for af in advanced_fields - ): + if self.is_field_advanced(name): field.widget.attrs.update( { "class": ( @@ -358,6 +355,17 @@ class CrdModelFormMixin: return True return False + def is_field_advanced(self, field_name): + advanced_fields = getattr(self, "ADVANCED_FIELDS", []) + return field_name in advanced_fields or any( + field_name.startswith(f"{af}.") for af in advanced_fields + ) + + def are_all_fields_advanced(self, field_list): + if not field_list: + return False + return all(self.is_field_advanced(field_name) for field_name in field_list) + def get_fieldsets(self): fieldsets = [] @@ -373,6 +381,7 @@ class CrdModelFormMixin: "fields": general_fields, "fieldsets": [], "has_mandatory": self.has_mandatory_fields(general_fields), + "is_advanced": self.are_all_fields_advanced(general_fields), } if all( [ @@ -439,6 +448,9 @@ class CrdModelFormMixin: title = f"{fieldset['title']}: {sub_fieldset['title']}: " for field in sub_fieldset["fields"]: self.strip_title(field, title) + sub_fieldset["is_advanced"] = self.are_all_fields_advanced( + sub_fieldset["fields"] + ) nested_fieldsets_list.append(sub_fieldset) fieldset["fieldsets"] = nested_fieldsets_list @@ -455,6 +467,8 @@ class CrdModelFormMixin: all_fields.extend(sub_fieldset["fields"]) fieldset["has_mandatory"] = self.has_mandatory_fields(all_fields) + fieldset["is_advanced"] = self.are_all_fields_advanced(all_fields) + fieldsets.append(fieldset) # Add 'others' tab if there are any fields @@ -465,6 +479,7 @@ class CrdModelFormMixin: "fields": others, "fieldsets": [], "has_mandatory": self.has_mandatory_fields(others), + "is_advanced": self.are_all_fields_advanced(others), } ) diff --git a/src/servala/frontend/templates/includes/tabbed_fieldset_form.html b/src/servala/frontend/templates/includes/tabbed_fieldset_form.html index 5857bdf..74fa22a 100644 --- a/src/servala/frontend/templates/includes/tabbed_fieldset_form.html +++ b/src/servala/frontend/templates/includes/tabbed_fieldset_form.html @@ -21,7 +21,8 @@
From de0ac39901cc77086389f7e872f95a54653974e6 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 21 Oct 2025 03:01:51 +0000 Subject: [PATCH 073/185] Update dependency sentry-sdk to >=2.42.1 --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2860f5d..222eea8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ "pyjwt>=2.10.1", "requests>=2.32.5", "rules>=3.5", - "sentry-sdk[django]>=2.39.0", + "sentry-sdk[django]>=2.42.1", "urlman>=2.0.2", ] diff --git a/uv.lock b/uv.lock index 82eee80..a68868d 100644 --- a/uv.lock +++ b/uv.lock @@ -1191,15 +1191,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.39.0" +version = "2.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4c/72/43294fa4bdd75c51610b5104a3ff834459ba653abb415150aa7826a249dd/sentry_sdk-2.39.0.tar.gz", hash = "sha256:8c185854d111f47f329ab6bc35993f28f7a6b7114db64aa426b326998cfa14e9", size = 348556, upload-time = "2025-09-25T09:15:39.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/31/04/ec8c1dd9250847303d98516e917978cb1c7083024770d86d657d2ccb5a70/sentry_sdk-2.42.1.tar.gz", hash = "sha256:8598cc6edcfe74cb8074ba6a7c15338cdee93d63d3eb9b9943b4b568354ad5b6", size = 354839, upload-time = "2025-10-20T12:38:40.45Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/44/4356cc64246ba7b2b920f7c97a85c3c52748e213e250b512ee8152eb559d/sentry_sdk-2.39.0-py2.py3-none-any.whl", hash = "sha256:ba655ca5e57b41569b18e2a5552cb3375209760a5d332cdd87c6c3f28f729602", size = 370851, upload-time = "2025-09-25T09:15:36.35Z" }, + { url = "https://files.pythonhosted.org/packages/0f/cb/c21b96ff379923310b4fb2c06e8d560d801e24aeb300faa72a04776868fc/sentry_sdk-2.42.1-py2.py3-none-any.whl", hash = "sha256:f8716b50c927d3beb41bc88439dc6bcd872237b596df5b14613e2ade104aee02", size = 380952, upload-time = "2025-10-20T12:38:38.88Z" }, ] [package.optional-dependencies] @@ -1268,7 +1268,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.39.0" }, + { name = "sentry-sdk", extras = ["django"], specifier = ">=2.42.1" }, { name = "urlman", specifier = ">=2.0.2" }, ] From 45b2b93aba8dc6f743dac21354768e30193b6160 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Tue, 21 Oct 2025 10:03:34 +0200 Subject: [PATCH 074/185] Add Invites to auditlog ref #19 --- src/servala/core/models/organization.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/servala/core/models/organization.py b/src/servala/core/models/organization.py index 1669f39..26435f3 100644 --- a/src/servala/core/models/organization.py +++ b/src/servala/core/models/organization.py @@ -2,6 +2,7 @@ import secrets import rules import urlman +from auditlog.registry import auditlog from django.conf import settings from django.contrib.sites.shortcuts import get_current_site from django.core.mail import send_mail @@ -536,3 +537,7 @@ The Servala Team""" recipient_list=[self.email], fail_silently=False, ) + + +auditlog.register(OrganizationInvitation, serialize_data=True) +auditlog.register(OrganizationMembership, serialize_data=True) From 7c6464330ddeea5daea2039f0d09f35009baec24 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Tue, 21 Oct 2025 13:29:07 +0200 Subject: [PATCH 075/185] Remove star in front of error message ref #19 --- src/servala/frontend/views/organization.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/servala/frontend/views/organization.py b/src/servala/frontend/views/organization.py index 7013a6c..63c84bb 100644 --- a/src/servala/frontend/views/organization.py +++ b/src/servala/frontend/views/organization.py @@ -199,7 +199,8 @@ class OrganizationUpdateView(OrganizationViewMixin, HtmxUpdateView): ) else: for error in form.errors.values(): - messages.error(request, error.as_text()) + for error_msg in error: + messages.error(request, error_msg) return redirect(self.get_success_url()) From 892a19bbcc2a2d4f228a812f4850d6aa1447a5e8 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Tue, 21 Oct 2025 14:54:12 +0200 Subject: [PATCH 076/185] Explain user roles ref #19 --- .../frontend/organizations/update.html | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/servala/frontend/templates/frontend/organizations/update.html b/src/servala/frontend/templates/frontend/organizations/update.html index d55dc56..2785e9a 100644 --- a/src/servala/frontend/templates/frontend/organizations/update.html +++ b/src/servala/frontend/templates/frontend/organizations/update.html @@ -222,12 +222,34 @@
-
+
+
+ {% translate "Role Permissions" %} +
+
    +
  • + {% translate "Owner" %}: {% translate "Can manage all organization settings, members, services, and can appoint administrators." %} +
  • +
  • + {% translate "Administrator" %}: {% translate "Can manage members, invite users, and manage all services and instances." %} +
  • +
  • + {% translate "Member" %}: {% translate "Can view organization details, create and manage their own service instances." %} +
  • +
+
+ {% csrf_token %} + +
{{ invitation_form }}
-
From 714cd9be5452adf635b4e602c19c83b07e63074b Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Tue, 21 Oct 2025 15:10:57 +0200 Subject: [PATCH 077/185] Implement organization delete, fix style, use rules ref #19 --- src/servala/core/models/organization.py | 1 + src/servala/core/rules.py | 10 +- .../frontend/organizations/update.html | 102 ++++++++++-------- src/servala/frontend/urls.py | 5 + src/servala/frontend/views/__init__.py | 2 + src/servala/frontend/views/organization.py | 93 ++++++++++++++-- 6 files changed, 159 insertions(+), 54 deletions(-) diff --git a/src/servala/core/models/organization.py b/src/servala/core/models/organization.py index 26435f3..bbcc16f 100644 --- a/src/servala/core/models/organization.py +++ b/src/servala/core/models/organization.py @@ -468,6 +468,7 @@ class OrganizationInvitation(ServalaModelMixin, models.Model): class urls(urlman.Urls): accept = "/invitations/{self.secret}/accept/" + delete = "{self.organization.urls.details}invitations/{self.pk}/delete/" class Meta: verbose_name = _("Organization invitation") diff --git a/src/servala/core/rules.py b/src/servala/core/rules.py index cf4dc1c..e1a0992 100644 --- a/src/servala/core/rules.py +++ b/src/servala/core/rules.py @@ -14,20 +14,26 @@ def has_organization_role(user, org, roles): @rules.predicate def is_organization_owner(user, obj): + from servala.core.models.organization import OrganizationRole + if hasattr(obj, "organization"): org = obj.organization else: org = obj - return has_organization_role(user, org, ["owner"]) + return has_organization_role(user, org, [OrganizationRole.OWNER]) @rules.predicate def is_organization_admin(user, obj): + from servala.core.models.organization import OrganizationRole + if hasattr(obj, "organization"): org = obj.organization else: org = obj - return has_organization_role(user, org, ["owner", "admin"]) + return has_organization_role( + user, org, [OrganizationRole.OWNER, OrganizationRole.ADMIN] + ) @rules.predicate diff --git a/src/servala/frontend/templates/frontend/organizations/update.html b/src/servala/frontend/templates/frontend/organizations/update.html index 2785e9a..73c2c69 100644 --- a/src/servala/frontend/templates/frontend/organizations/update.html +++ b/src/servala/frontend/templates/frontend/organizations/update.html @@ -67,43 +67,66 @@
+{% endpartialdef members-list %} +{% partialdef pending-invitations-card %} {% if pending_invitations %} -
- {% translate "Pending Invitations" %} -
-
- - - - - - - - - - - {% for invitation in pending_invitations %} - - - - - - - {% endfor %} - -
{% translate "Email" %}{% translate "Role" %}{% translate "Sent" %}{% translate "Link" %}
{{ invitation.email }} - - {{ invitation.get_role_display }} - - {{ invitation.created_at|date:"Y-m-d H:i" }} - -
+
+
+

+ {% translate "Pending Invitations" %} +

+
+
+
+
+ + + + + + + + + + + {% for invitation in pending_invitations %} + + + + + + + {% endfor %} + +
{% translate "Email" %}{% translate "Role" %}{% translate "Sent" %}{% translate "Actions" %}
{{ invitation.email }} + + {{ invitation.get_role_display }} + + {{ invitation.created_at|date:"Y-m-d H:i" }} + + + {% csrf_token %} + + + +
+
+
+
{% endif %} -{% endpartialdef members-list %} +{% endpartialdef pending-invitations-card %} {% block content %}
@@ -214,6 +237,7 @@
{% partial members-list %}
+
{% partial pending-invitations-card %}

@@ -238,18 +262,12 @@

-
+ {% csrf_token %} - -
{{ invitation_form }}
-
diff --git a/src/servala/frontend/urls.py b/src/servala/frontend/urls.py index 3aa9b08..73d0759 100644 --- a/src/servala/frontend/urls.py +++ b/src/servala/frontend/urls.py @@ -30,6 +30,11 @@ urlpatterns = [ views.OrganizationUpdateView.as_view(), name="organization.details", ), + path( + "details/invitations//delete/", + views.InvitationDeleteView.as_view(), + name="invitation.delete", + ), path( "services/", views.ServiceListView.as_view(), diff --git a/src/servala/frontend/views/__init__.py b/src/servala/frontend/views/__init__.py index 6167221..33b0560 100644 --- a/src/servala/frontend/views/__init__.py +++ b/src/servala/frontend/views/__init__.py @@ -9,6 +9,7 @@ from .generic import ( ) from .organization import ( InvitationAcceptView, + InvitationDeleteView, OrganizationCreateView, OrganizationDashboardView, OrganizationUpdateView, @@ -27,6 +28,7 @@ from .support import SupportView __all__ = [ "IndexView", "InvitationAcceptView", + "InvitationDeleteView", "LogoutView", "OrganizationCreateView", "OrganizationDashboardView", diff --git a/src/servala/frontend/views/organization.py b/src/servala/frontend/views/organization.py index 63c84bb..c4c1336 100644 --- a/src/servala/frontend/views/organization.py +++ b/src/servala/frontend/views/organization.py @@ -5,7 +5,7 @@ from django.utils import timezone from django.utils.decorators import method_decorator from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ -from django.views.generic import CreateView, DetailView, TemplateView +from django.views.generic import CreateView, DeleteView, DetailView, TemplateView from django_scopes import scopes_disabled from rules.contrib.views import AutoPermissionRequiredMixin @@ -14,7 +14,6 @@ from servala.core.models import ( Organization, OrganizationInvitation, OrganizationMembership, - OrganizationRole, ServiceInstance, ) from servala.frontend.forms.organization import ( @@ -22,7 +21,11 @@ from servala.frontend.forms.organization import ( OrganizationForm, OrganizationInvitationForm, ) -from servala.frontend.views.mixins import HtmxUpdateView, OrganizationViewMixin +from servala.frontend.views.mixins import ( + HtmxUpdateView, + HtmxViewMixin, + OrganizationViewMixin, +) class OrganizationCreateView(AutoPermissionRequiredMixin, CreateView): @@ -108,10 +111,8 @@ class OrganizationDashboardView( return context -class OrganizationUpdateView(OrganizationViewMixin, HtmxUpdateView): +class OrganizationMembershipMixin: template_name = "frontend/organizations/update.html" - form_class = OrganizationForm - fragments = ("org-name", "org-name-edit", "members-list") @cached_property def user_role(self): @@ -126,10 +127,9 @@ class OrganizationUpdateView(OrganizationViewMixin, HtmxUpdateView): @cached_property def can_manage_members(self): - return self.user_role in [ - OrganizationRole.ADMIN, - OrganizationRole.OWNER, - ] + return self.request.user.has_perm( + "core.change_organization", self.request.organization + ) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -159,6 +159,18 @@ class OrganizationUpdateView(OrganizationViewMixin, HtmxUpdateView): return context + +class OrganizationUpdateView( + OrganizationViewMixin, OrganizationMembershipMixin, HtmxUpdateView +): + form_class = OrganizationForm + fragments = ( + "org-name", + "org-name-edit", + "members-list", + "pending-invitations-card", + ) + def post(self, request, *args, **kwargs): if "invite_email" in request.POST: return self.handle_invitation(request) @@ -202,6 +214,9 @@ class OrganizationUpdateView(OrganizationViewMixin, HtmxUpdateView): for error_msg in error: messages.error(request, error_msg) + if self.is_htmx and self._get_fragment(): + return self.get(request, *self.args, **self.kwargs) + return redirect(self.get_success_url()) def get_success_url(self): @@ -260,3 +275,61 @@ class InvitationAcceptView(TemplateView): request.session.pop("invitation_next", None) return redirect(invitation.organization.urls.base) + + +class InvitationDeleteView(HtmxViewMixin, OrganizationMembershipMixin, DeleteView): + model = OrganizationInvitation + http_method_names = ["get", "post"] + fragments = ("pending-invitations-card",) + + def get_queryset(self): + return OrganizationInvitation.objects.filter(accepted_by__isnull=True) + + def get_success_url(self): + return self.object.organization.urls.details + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + organization = self.request.organization + context["pending_invitations"] = OrganizationInvitation.objects.filter( + organization=organization, accepted_by__isnull=True + ).order_by("-created_at") + return context + + def _check_permission(self): + return self.request.user.has_perm( + "core.change_organization", self.request.organization + ) + + def get_object(self): + if self.request.method == "POST" and self.is_htmx: + try: + return super().get_object() + except Exception: + return + return super().get_object() + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + organization = self.object.organization + + if not self._check_permission(): + if not self.is_htmx: + messages.error( + request, + _("You do not have permission to delete this invitation."), + ) + return redirect(organization.urls.details) + + email = self.object.email + self.object.delete() + if not self.is_htmx: + messages.success( + request, + _("Invitation for {email} has been deleted.").format(email=email), + ) + + if self.is_htmx and self._get_fragment(): + return self.get(request, *args, **kwargs) + + return redirect(self.get_success_url()) From b4d239a1a6aba0fe8ca8307d490047ac50b1f990 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 22 Oct 2025 11:12:18 +0200 Subject: [PATCH 078/185] Limit cloud providers per organization origin ref #38 --- src/servala/core/admin.py | 2 ++ ...anization_limit_cloudproviders_and_more.py | 10 ------- ...organizationorigin_limit_cloudproviders.py | 24 +++++++++++++++ src/servala/core/models/organization.py | 29 ++++++++++++------- src/servala/frontend/forms/service.py | 9 ++++++ src/servala/frontend/views/service.py | 4 ++- 6 files changed, 57 insertions(+), 21 deletions(-) create mode 100644 src/servala/core/migrations/0015_organizationorigin_limit_cloudproviders.py diff --git a/src/servala/core/admin.py b/src/servala/core/admin.py index c0beb9e..b54ba65 100644 --- a/src/servala/core/admin.py +++ b/src/servala/core/admin.py @@ -63,6 +63,7 @@ class OrganizationAdmin(admin.ModelAdmin): search_fields = ("name", "namespace") autocomplete_fields = ("billing_entity", "origin") inlines = (OrganizationMembershipInline,) + filter_horizontal = ("limit_osb_services",) def get_readonly_fields(self, request, obj=None): readonly_fields = list(super().get_readonly_fields(request, obj) or []) @@ -85,6 +86,7 @@ class OrganizationOriginAdmin(admin.ModelAdmin): list_display = ("name", "billing_entity") search_fields = ("name",) autocomplete_fields = ("billing_entity",) + filter_horizontal = ("limit_cloudproviders",) @admin.register(OrganizationMembership) diff --git a/src/servala/core/migrations/0009_organization_limit_cloudproviders_and_more.py b/src/servala/core/migrations/0009_organization_limit_cloudproviders_and_more.py index 3ec1032..1558d07 100644 --- a/src/servala/core/migrations/0009_organization_limit_cloudproviders_and_more.py +++ b/src/servala/core/migrations/0009_organization_limit_cloudproviders_and_more.py @@ -10,16 +10,6 @@ class Migration(migrations.Migration): ] operations = [ - migrations.AddField( - model_name="organization", - name="limit_cloudproviders", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="core.cloudprovider", - verbose_name="Limit to these Cloud providers", - ), - ), migrations.AddField( model_name="organization", name="limit_osb_services", diff --git a/src/servala/core/migrations/0015_organizationorigin_limit_cloudproviders.py b/src/servala/core/migrations/0015_organizationorigin_limit_cloudproviders.py new file mode 100644 index 0000000..f119b99 --- /dev/null +++ b/src/servala/core/migrations/0015_organizationorigin_limit_cloudproviders.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2.7 on 2025-10-21 16:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0014_servicedefinition_advanced_fields"), + ] + + operations = [ + migrations.AddField( + model_name="organizationorigin", + name="limit_cloudproviders", + field=models.ManyToManyField( + blank=True, + help_text="If set, all organizations with this origin will be limited to these cloud providers.", + related_name="+", + to="core.cloudprovider", + verbose_name="Limit to these Cloud providers", + ), + ), + ] diff --git a/src/servala/core/models/organization.py b/src/servala/core/models/organization.py index bbcc16f..1137b1c 100644 --- a/src/servala/core/models/organization.py +++ b/src/servala/core/models/organization.py @@ -52,12 +52,6 @@ class Organization(ServalaModelMixin, models.Model): related_name="organizations", verbose_name=_("Members"), ) - limit_cloudproviders = models.ManyToManyField( - to="CloudProvider", - related_name="+", - verbose_name=_("Limit to these Cloud providers"), - blank=True, - ) limit_osb_services = models.ManyToManyField( to="Service", related_name="+", @@ -99,6 +93,14 @@ class Organization(ServalaModelMixin, models.Model): def has_inherited_billing_entity(self): return self.origin and self.billing_entity == self.origin.billing_entity + @property + def limit_cloudproviders(self): + if self.origin: + return self.origin.limit_cloudproviders.all() + from servala.core.models import CloudProvider + + return CloudProvider.objects.none() + def set_owner(self, user): with scopes_disabled(): OrganizationMembership.objects.filter(user=user, organization=self).delete() @@ -161,9 +163,8 @@ class Organization(ServalaModelMixin, models.Model): if self.limit_osb_services.exists(): queryset = self.limit_osb_services.all() if self.limit_cloudproviders.exists(): - allowed_providers = self.limit_cloudproviders.all() queryset = queryset.filter( - offerings__provider__in=allowed_providers + offerings__provider__in=self.limit_cloudproviders ).distinct() return queryset.prefetch_related( "offerings", "offerings__provider" @@ -177,9 +178,8 @@ class Organization(ServalaModelMixin, models.Model): queryset = Service.objects.select_related("category") if self.limit_cloudproviders.exists(): - allowed_providers = self.limit_cloudproviders.all() queryset = queryset.filter( - offerings__provider__in=allowed_providers + offerings__provider__in=self.limit_cloudproviders ).distinct() queryset = queryset.exclude(id__in=self.limit_osb_services.all()) return queryset.prefetch_related("offerings", "offerings__provider") @@ -376,6 +376,15 @@ class OrganizationOrigin(ServalaModelMixin, models.Model): ), null=True, ) + limit_cloudproviders = models.ManyToManyField( + to="CloudProvider", + related_name="+", + verbose_name=_("Limit to these Cloud providers"), + blank=True, + help_text=_( + "If set, all organizations with this origin will be limited to these cloud providers." + ), + ) class Meta: verbose_name = _("Organization origin") diff --git a/src/servala/frontend/forms/service.py b/src/servala/frontend/forms/service.py index 5dd78a7..23325f3 100644 --- a/src/servala/frontend/forms/service.py +++ b/src/servala/frontend/forms/service.py @@ -21,6 +21,15 @@ class ServiceFilterForm(forms.Form): ) q = forms.CharField(label=_("Search"), required=False) + def __init__(self, *args, organization=None, **kwargs): + super().__init__(*args, **kwargs) + if organization and organization.limit_cloudproviders.exists(): + allowed_providers = organization.limit_cloudproviders + if allowed_providers.count() <= 1: + self.fields.pop("cloud_provider", None) + else: + self.fields["cloud_provider"].queryset = allowed_providers + def filter_queryset(self, queryset): if category := self.cleaned_data.get("category"): queryset = queryset.filter(category=category) diff --git a/src/servala/frontend/views/service.py b/src/servala/frontend/views/service.py index ba4f0a4..689f381 100644 --- a/src/servala/frontend/views/service.py +++ b/src/servala/frontend/views/service.py @@ -44,7 +44,9 @@ class ServiceListView(OrganizationViewMixin, ListView): @cached_property def filter_form(self): - return ServiceFilterForm(data=self.request.GET or None) + return ServiceFilterForm( + data=self.request.GET or None, organization=self.request.organization + ) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) From d8ceaf4b1b0078db930464ffa64b97e3c1692cb6 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 22 Oct 2025 11:12:49 +0200 Subject: [PATCH 079/185] Fix display of empty service list --- .../templates/frontend/organizations/services.html | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/servala/frontend/templates/frontend/organizations/services.html b/src/servala/frontend/templates/frontend/organizations/services.html index 461d37d..3250c52 100644 --- a/src/servala/frontend/templates/frontend/organizations/services.html +++ b/src/servala/frontend/templates/frontend/organizations/services.html @@ -20,10 +20,12 @@ {% for service in services %}
{% include "includes/service_card.html" %}
{% empty %} -
-
-
-

{% translate "No services found." %}

+
+
+
+
+

{% translate "No services found." %}

+
From 3b49b173603752bd56c6c16913c5985cd286c743 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 22 Oct 2025 11:37:07 +0200 Subject: [PATCH 080/185] Implement per-origin default odoo sales orders ref #227 --- src/servala/core/admin.py | 2 +- ...zationorigin_default_odoo_sale_order_id.py | 23 +++++++++++++++++++ src/servala/core/models/organization.py | 23 ++++++++++++++++++- 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 src/servala/core/migrations/0016_organizationorigin_default_odoo_sale_order_id.py diff --git a/src/servala/core/admin.py b/src/servala/core/admin.py index b54ba65..87da376 100644 --- a/src/servala/core/admin.py +++ b/src/servala/core/admin.py @@ -83,7 +83,7 @@ class BillingEntityAdmin(admin.ModelAdmin): @admin.register(OrganizationOrigin) class OrganizationOriginAdmin(admin.ModelAdmin): - list_display = ("name", "billing_entity") + list_display = ("name", "billing_entity", "default_odoo_sale_order_id") search_fields = ("name",) autocomplete_fields = ("billing_entity",) filter_horizontal = ("limit_cloudproviders",) diff --git a/src/servala/core/migrations/0016_organizationorigin_default_odoo_sale_order_id.py b/src/servala/core/migrations/0016_organizationorigin_default_odoo_sale_order_id.py new file mode 100644 index 0000000..1432324 --- /dev/null +++ b/src/servala/core/migrations/0016_organizationorigin_default_odoo_sale_order_id.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.7 on 2025-10-22 09:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0015_organizationorigin_limit_cloudproviders"), + ] + + operations = [ + migrations.AddField( + model_name="organizationorigin", + name="default_odoo_sale_order_id", + field=models.IntegerField( + blank=True, + help_text="If set, this sale order will be used for new organizations with this origin.", + null=True, + verbose_name="Default Odoo Sale Order ID", + ), + ), + ] diff --git a/src/servala/core/models/organization.py b/src/servala/core/models/organization.py index 1137b1c..2bc76ff 100644 --- a/src/servala/core/models/organization.py +++ b/src/servala/core/models/organization.py @@ -129,7 +129,20 @@ class Organization(ServalaModelMixin, models.Model): if owner: instance.set_owner(owner) - if ( + if instance.origin and instance.origin.default_odoo_sale_order_id: + sale_order_id = instance.origin.default_odoo_sale_order_id + sale_order_data = CLIENT.search_read( + model="sale.order", + domain=[["id", "=", sale_order_id]], + fields=["name"], + limit=1, + ) + + instance.odoo_sale_order_id = sale_order_id + if sale_order_data: + instance.odoo_sale_order_name = sale_order_data[0]["name"] + instance.save(update_fields=["odoo_sale_order_id", "odoo_sale_order_name"]) + elif ( instance.billing_entity.odoo_company_id and instance.billing_entity.odoo_invoice_id ): @@ -385,6 +398,14 @@ class OrganizationOrigin(ServalaModelMixin, models.Model): "If set, all organizations with this origin will be limited to these cloud providers." ), ) + default_odoo_sale_order_id = models.IntegerField( + null=True, + blank=True, + verbose_name=_("Default Odoo Sale Order ID"), + help_text=_( + "If set, this sale order will be used for new organizations with this origin." + ), + ) class Meta: verbose_name = _("Organization origin") From 090827bbbf2ed21c611ae26cc9c20be17e27fb6e Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 22 Oct 2025 11:39:19 +0200 Subject: [PATCH 081/185] Squash new migrations --- ...009_controlplane_wildcard_dns_and_more.py} | 82 ++++++++++++++++++- ...anization_limit_cloudproviders_and_more.py | 23 ------ .../0010_organizationorigin_billing_entity.py | 26 ------ .../0012_serviceoffering_external_links.py | 23 ------ .../0013_controlplane_wildcard_dns.py | 24 ------ .../0014_servicedefinition_advanced_fields.py | 27 ------ ...organizationorigin_limit_cloudproviders.py | 24 ------ ...zationorigin_default_odoo_sale_order_id.py | 23 ------ 8 files changed, 80 insertions(+), 172 deletions(-) rename src/servala/core/migrations/{0011_organizationinvitation.py => 0009_controlplane_wildcard_dns_and_more.py} (53%) delete mode 100644 src/servala/core/migrations/0009_organization_limit_cloudproviders_and_more.py delete mode 100644 src/servala/core/migrations/0010_organizationorigin_billing_entity.py delete mode 100644 src/servala/core/migrations/0012_serviceoffering_external_links.py delete mode 100644 src/servala/core/migrations/0013_controlplane_wildcard_dns.py delete mode 100644 src/servala/core/migrations/0014_servicedefinition_advanced_fields.py delete mode 100644 src/servala/core/migrations/0015_organizationorigin_limit_cloudproviders.py delete mode 100644 src/servala/core/migrations/0016_organizationorigin_default_odoo_sale_order_id.py diff --git a/src/servala/core/migrations/0011_organizationinvitation.py b/src/servala/core/migrations/0009_controlplane_wildcard_dns_and_more.py similarity index 53% rename from src/servala/core/migrations/0011_organizationinvitation.py rename to src/servala/core/migrations/0009_controlplane_wildcard_dns_and_more.py index 25fb4b1..811c843 100644 --- a/src/servala/core/migrations/0011_organizationinvitation.py +++ b/src/servala/core/migrations/0009_controlplane_wildcard_dns_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.7 on 2025-10-17 00:58 +# Generated by Django 5.2.7 on 2025-10-22 09:38 import django.db.models.deletion import rules.contrib.models @@ -9,10 +9,88 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ("core", "0010_organizationorigin_billing_entity"), + ("core", "0008_organization_osb_guid_service_osb_service_id_and_more"), ] operations = [ + migrations.AddField( + model_name="controlplane", + name="wildcard_dns", + field=models.CharField( + blank=True, + help_text="Wildcard DNS domain for auto-generating FQDNs (e.g., apps.exoscale-ch-gva-2-prod2.services.servala.com)", + max_length=255, + null=True, + verbose_name="Wildcard DNS", + ), + ), + migrations.AddField( + model_name="organization", + name="limit_osb_services", + field=models.ManyToManyField( + blank=True, + related_name="+", + to="core.service", + verbose_name="Services activated from OSB", + ), + ), + migrations.AddField( + model_name="organizationorigin", + name="billing_entity", + field=models.ForeignKey( + help_text="If set, this billing entity will be used on new organizations with this origin.", + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="origins", + to="core.billingentity", + verbose_name="Billing entity", + ), + ), + migrations.AddField( + model_name="organizationorigin", + name="default_odoo_sale_order_id", + field=models.IntegerField( + blank=True, + help_text="If set, this sale order will be used for new organizations with this origin.", + null=True, + verbose_name="Default Odoo Sale Order ID", + ), + ), + migrations.AddField( + model_name="organizationorigin", + name="limit_cloudproviders", + field=models.ManyToManyField( + blank=True, + help_text="If set, all organizations with this origin will be limited to these cloud providers.", + related_name="+", + to="core.cloudprovider", + verbose_name="Limit to these Cloud providers", + ), + ), + migrations.AddField( + model_name="servicedefinition", + name="advanced_fields", + field=models.JSONField( + blank=True, + default=list, + help_text=( + "Array of field names that should be hidden behind an 'Advanced' toggle." + "Use dot notation (e.g., ['spec.parameters.monitoring.enabled', 'spec.parameters.backup.schedule'])" + ), + null=True, + verbose_name="Advanced fields", + ), + ), + migrations.AddField( + model_name="serviceoffering", + name="external_links", + field=models.JSONField( + blank=True, + help_text='JSON array of link objects: {"url": "…", "title": "…"}. ', + null=True, + verbose_name="External links", + ), + ), migrations.CreateModel( name="OrganizationInvitation", fields=[ diff --git a/src/servala/core/migrations/0009_organization_limit_cloudproviders_and_more.py b/src/servala/core/migrations/0009_organization_limit_cloudproviders_and_more.py deleted file mode 100644 index 1558d07..0000000 --- a/src/servala/core/migrations/0009_organization_limit_cloudproviders_and_more.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-16 22:52 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("core", "0008_organization_osb_guid_service_osb_service_id_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="organization", - name="limit_osb_services", - field=models.ManyToManyField( - blank=True, - related_name="+", - to="core.service", - verbose_name="Services activated from OSB", - ), - ), - ] diff --git a/src/servala/core/migrations/0010_organizationorigin_billing_entity.py b/src/servala/core/migrations/0010_organizationorigin_billing_entity.py deleted file mode 100644 index d61a75f..0000000 --- a/src/servala/core/migrations/0010_organizationorigin_billing_entity.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-17 00:22 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("core", "0009_organization_limit_cloudproviders_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="organizationorigin", - name="billing_entity", - field=models.ForeignKey( - help_text="If set, this billing entity will be used on new organizations with this origin.", - null=True, - on_delete=django.db.models.deletion.PROTECT, - related_name="origins", - to="core.billingentity", - verbose_name="Billing entity", - ), - ), - ] diff --git a/src/servala/core/migrations/0012_serviceoffering_external_links.py b/src/servala/core/migrations/0012_serviceoffering_external_links.py deleted file mode 100644 index d8c2ac7..0000000 --- a/src/servala/core/migrations/0012_serviceoffering_external_links.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-17 02:23 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("core", "0011_organizationinvitation"), - ] - - operations = [ - migrations.AddField( - model_name="serviceoffering", - name="external_links", - field=models.JSONField( - blank=True, - help_text='JSON array of link objects: {"url": "…", "title": "…"}. ', - null=True, - verbose_name="External links", - ), - ), - ] diff --git a/src/servala/core/migrations/0013_controlplane_wildcard_dns.py b/src/servala/core/migrations/0013_controlplane_wildcard_dns.py deleted file mode 100644 index 26dbaaf..0000000 --- a/src/servala/core/migrations/0013_controlplane_wildcard_dns.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-17 02:51 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("core", "0012_serviceoffering_external_links"), - ] - - operations = [ - migrations.AddField( - model_name="controlplane", - name="wildcard_dns", - field=models.CharField( - blank=True, - help_text="Wildcard DNS domain for auto-generating FQDNs (e.g., apps.exoscale-ch-gva-2-prod2.services.servala.com)", - max_length=255, - null=True, - verbose_name="Wildcard DNS", - ), - ), - ] diff --git a/src/servala/core/migrations/0014_servicedefinition_advanced_fields.py b/src/servala/core/migrations/0014_servicedefinition_advanced_fields.py deleted file mode 100644 index 20632b9..0000000 --- a/src/servala/core/migrations/0014_servicedefinition_advanced_fields.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-17 03:23 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("core", "0013_controlplane_wildcard_dns"), - ] - - operations = [ - migrations.AddField( - model_name="servicedefinition", - name="advanced_fields", - field=models.JSONField( - blank=True, - default=list, - help_text=( - "Array of field names that should be hidden behind an 'Advanced' toggle. " - "Use dot notation (e.g., ['spec.parameters.monitoring.enabled', 'spec.parameters.backup.schedule'])" - ), - null=True, - verbose_name="Advanced fields", - ), - ), - ] diff --git a/src/servala/core/migrations/0015_organizationorigin_limit_cloudproviders.py b/src/servala/core/migrations/0015_organizationorigin_limit_cloudproviders.py deleted file mode 100644 index f119b99..0000000 --- a/src/servala/core/migrations/0015_organizationorigin_limit_cloudproviders.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-21 16:04 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("core", "0014_servicedefinition_advanced_fields"), - ] - - operations = [ - migrations.AddField( - model_name="organizationorigin", - name="limit_cloudproviders", - field=models.ManyToManyField( - blank=True, - help_text="If set, all organizations with this origin will be limited to these cloud providers.", - related_name="+", - to="core.cloudprovider", - verbose_name="Limit to these Cloud providers", - ), - ), - ] diff --git a/src/servala/core/migrations/0016_organizationorigin_default_odoo_sale_order_id.py b/src/servala/core/migrations/0016_organizationorigin_default_odoo_sale_order_id.py deleted file mode 100644 index 1432324..0000000 --- a/src/servala/core/migrations/0016_organizationorigin_default_odoo_sale_order_id.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-22 09:32 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("core", "0015_organizationorigin_limit_cloudproviders"), - ] - - operations = [ - migrations.AddField( - model_name="organizationorigin", - name="default_odoo_sale_order_id", - field=models.IntegerField( - blank=True, - help_text="If set, this sale order will be used for new organizations with this origin.", - null=True, - verbose_name="Default Odoo Sale Order ID", - ), - ), - ] From cb3464d9b5d794c81f13d5f7820fc06ee3985779 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 22 Oct 2025 15:37:25 +0200 Subject: [PATCH 082/185] Remove invite uniqueness constraint --- ...010_remove_invitation_unique_constraint.py | 31 +++++++++++++++++++ src/servala/core/models/organization.py | 1 - 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 src/servala/core/migrations/0010_remove_invitation_unique_constraint.py diff --git a/src/servala/core/migrations/0010_remove_invitation_unique_constraint.py b/src/servala/core/migrations/0010_remove_invitation_unique_constraint.py new file mode 100644 index 0000000..78c2c45 --- /dev/null +++ b/src/servala/core/migrations/0010_remove_invitation_unique_constraint.py @@ -0,0 +1,31 @@ +# Generated by Django 5.2.7 on 2025-10-22 13:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0009_controlplane_wildcard_dns_and_more"), + ] + + operations = [ + migrations.AlterUniqueTogether( + name="organizationinvitation", + unique_together=set(), + ), + migrations.AlterField( + model_name="servicedefinition", + name="advanced_fields", + field=models.JSONField( + blank=True, + default=list, + help_text=( + "Array of field names that should be hidden behind an 'Advanced' toggle. " + "Use dot notation (e.g., ['spec.parameters.monitoring.enabled', 'spec.parameters.backup.schedule'])" + ), + null=True, + verbose_name="Advanced fields", + ), + ), + ] diff --git a/src/servala/core/models/organization.py b/src/servala/core/models/organization.py index 2bc76ff..553fa12 100644 --- a/src/servala/core/models/organization.py +++ b/src/servala/core/models/organization.py @@ -503,7 +503,6 @@ class OrganizationInvitation(ServalaModelMixin, models.Model): class Meta: verbose_name = _("Organization invitation") verbose_name_plural = _("Organization invitations") - unique_together = [["organization", "email"]] def __str__(self): return f"Invitation for {self.email} to {self.organization}" From 534b2e8d72115e41cf9f485527f72af6a4ca4477 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 22 Oct 2025 15:40:34 +0200 Subject: [PATCH 083/185] Make sure origin billing entity is not mandatory ref #38 --- ...alter_organizationorigin_billing_entity.py | 27 +++++++++++++++++++ src/servala/core/models/organization.py | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 src/servala/core/migrations/0011_alter_organizationorigin_billing_entity.py diff --git a/src/servala/core/migrations/0011_alter_organizationorigin_billing_entity.py b/src/servala/core/migrations/0011_alter_organizationorigin_billing_entity.py new file mode 100644 index 0000000..b122d68 --- /dev/null +++ b/src/servala/core/migrations/0011_alter_organizationorigin_billing_entity.py @@ -0,0 +1,27 @@ +# Generated by Django 5.2.7 on 2025-10-22 13:40 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0010_remove_invitation_unique_constraint"), + ] + + operations = [ + migrations.AlterField( + model_name="organizationorigin", + name="billing_entity", + field=models.ForeignKey( + blank=True, + help_text="If set, this billing entity will be used on new organizations with this origin.", + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="origins", + to="core.billingentity", + verbose_name="Billing entity", + ), + ), + ] diff --git a/src/servala/core/models/organization.py b/src/servala/core/models/organization.py index 553fa12..78605f6 100644 --- a/src/servala/core/models/organization.py +++ b/src/servala/core/models/organization.py @@ -387,7 +387,7 @@ class OrganizationOrigin(ServalaModelMixin, models.Model): help_text=_( "If set, this billing entity will be used on new organizations with this origin." ), - null=True, + null=True, blank=True, ) limit_cloudproviders = models.ManyToManyField( to="CloudProvider", From b6938f9204e75e0226cbbad7730c63a69a680a5e Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 22 Oct 2025 15:54:52 +0200 Subject: [PATCH 084/185] Fix misaligned dashboard table closes #228 --- .../frontend/templates/frontend/organizations/dashboard.html | 1 - 1 file changed, 1 deletion(-) diff --git a/src/servala/frontend/templates/frontend/organizations/dashboard.html b/src/servala/frontend/templates/frontend/organizations/dashboard.html index 167c806..441d0be 100644 --- a/src/servala/frontend/templates/frontend/organizations/dashboard.html +++ b/src/servala/frontend/templates/frontend/organizations/dashboard.html @@ -87,7 +87,6 @@ {% translate "Name" %} {% translate "Service" %} - {% translate "Status" %} {% translate "Created" %} {% translate "Actions" %} From 0be3739ce6feee2c16efc553c4f50a16e0b108c9 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 22 Oct 2025 16:16:23 +0200 Subject: [PATCH 085/185] Add beta banner closes #228 --- .env.example | 4 ++ src/servala/core/models/organization.py | 3 +- src/servala/frontend/context_processors.py | 7 +++ .../frontend/templates/frontend/base.html | 1 + .../templates/includes/beta_banner.html | 13 +++++ src/servala/settings.py | 2 + src/servala/static/css/servala.css | 56 +++++++++++++++++++ 7 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 src/servala/frontend/templates/includes/beta_banner.html diff --git a/.env.example b/.env.example index 998150c..63df700 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,10 @@ # When the environment is "development", DEBUG is set to True. SERVALA_ENVIRONMENT='development' +# Set to "False" to disable the beta testing banner at the top of every page. +# Defaults to "True". +SERVALA_SHOW_BETA_BANNER='True' + # Set SERVALA_PREVIOUS_SECRET_KEY when rotating to a new secret key in order to not expire all sessions and to remain able to read encrypted fields! # In order to retire the previous key, run the ``reencrypt_fields`` command. Once you drop the previous secret key from # the rotation, all sessions that still rely on that key will be invalidated (i.e., users will have to log in again). diff --git a/src/servala/core/models/organization.py b/src/servala/core/models/organization.py index 78605f6..646849f 100644 --- a/src/servala/core/models/organization.py +++ b/src/servala/core/models/organization.py @@ -387,7 +387,8 @@ class OrganizationOrigin(ServalaModelMixin, models.Model): help_text=_( "If set, this billing entity will be used on new organizations with this origin." ), - null=True, blank=True, + null=True, + blank=True, ) limit_cloudproviders = models.ManyToManyField( to="CloudProvider", diff --git a/src/servala/frontend/context_processors.py b/src/servala/frontend/context_processors.py index 1ff2a14..78dc0a9 100644 --- a/src/servala/frontend/context_processors.py +++ b/src/servala/frontend/context_processors.py @@ -1,5 +1,12 @@ +from django.conf import settings + + def add_organizations(request): if not request.user.is_authenticated: return {"user_organizations": []} return {"user_organizations": request.user.organizations.all().order_by("name")} + + +def add_beta_banner(request): + return {"show_beta_banner": settings.SERVALA_SHOW_BETA_BANNER} diff --git a/src/servala/frontend/templates/frontend/base.html b/src/servala/frontend/templates/frontend/base.html index 7c6bc54..620cb6d 100644 --- a/src/servala/frontend/templates/frontend/base.html +++ b/src/servala/frontend/templates/frontend/base.html @@ -22,6 +22,7 @@
+ {% include 'includes/beta_banner.html' %} {% include 'includes/header.html' %}
diff --git a/src/servala/frontend/templates/includes/beta_banner.html b/src/servala/frontend/templates/includes/beta_banner.html new file mode 100644 index 0000000..04bd91d --- /dev/null +++ b/src/servala/frontend/templates/includes/beta_banner.html @@ -0,0 +1,13 @@ +{% if show_beta_banner %} +
+
+
+ BETA + The Servala Portal is currently in beta testing. Your feedback helps us improve! + +
+
+
+{% endif %} diff --git a/src/servala/settings.py b/src/servala/settings.py index eef44f2..f57ac0d 100644 --- a/src/servala/settings.py +++ b/src/servala/settings.py @@ -20,6 +20,7 @@ from servala.__about__ import __version__ as version SERVALA_ENVIRONMENT = os.environ.get("SERVALA_ENVIRONMENT", "development") DEBUG = SERVALA_ENVIRONMENT == "development" +SERVALA_SHOW_BETA_BANNER = os.environ.get("SERVALA_SHOW_BETA_BANNER", "True") == "True" SECRET_KEY = os.environ.get("SERVALA_SECRET_KEY") if previous_secret_key := os.environ.get("SERVALA_PREVIOUS_SECRET_KEY"): @@ -219,6 +220,7 @@ TEMPLATES = [ "django.contrib.messages.context_processors.messages", "django.template.context_processors.static", "servala.frontend.context_processors.add_organizations", + "servala.frontend.context_processors.add_beta_banner", ], "loaders": template_loaders, }, diff --git a/src/servala/static/css/servala.css b/src/servala/static/css/servala.css index 0ea8b28..cb7061a 100644 --- a/src/servala/static/css/servala.css +++ b/src/servala/static/css/servala.css @@ -332,3 +332,59 @@ html[data-bs-theme="dark"] @keyframes tab-pulse { .nav-tabs .nav-link.tab-flash { animation: tab-pulse 1s ease-in-out 2; } + +.beta-banner { + background: linear-gradient(135deg, var(--bs-primary) 0%, var(--brand-mid) 100%); + color: white; + padding: 0.75rem 0; + text-align: center; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} +.beta-banner-content { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + gap: 0.75rem; +} +.beta-banner-badge { + background-color: white; + color: var(--bs-primary); + padding: 0.25rem 0.75rem; + border-radius: 1rem; + font-weight: bold; + font-size: 0.875rem; + letter-spacing: 0.5px; +} +.beta-banner-text { + font-size: 0.95rem; +} +.beta-banner-button { + background-color: white; + color: var(--bs-primary); + border: none; + font-weight: 600; + padding: 0.375rem 1rem; + transition: all 0.2s ease; +} +.beta-banner-button:hover { + background-color: var(--brand-light); + color: var(--bs-primary); + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); +} + +html[data-bs-theme="dark"] .beta-banner { + background: linear-gradient(135deg, var(--bs-primary) 0%, #7a4fc4 100%); +} +html[data-bs-theme="dark"] .beta-banner-badge { + background-color: rgba(255, 255, 255, 0.95); +} +html[data-bs-theme="dark"] .beta-banner-button { + background-color: rgba(255, 255, 255, 0.95); + color: var(--bs-primary); +} +html[data-bs-theme="dark"] .beta-banner-button:hover { + background-color: white; + color: var(--bs-primary); +} From b913e544a4e7240c57fcba1e597aae2668f28967 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Wed, 22 Oct 2025 16:29:59 +0200 Subject: [PATCH 086/185] bump version 2025.10.03-1 -> 2025.10.22-0 --- README.md | 2 +- pyproject.toml | 2 +- src/servala/__about__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7f21192..4231d69 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ The Servala Self-Service Portal -Latest release: 2025.10.03-1 +Latest release: 2025.10.22-0 ## Documentation diff --git a/pyproject.toml b/pyproject.toml index 670e876..1f0516a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ testpaths = "src/tests" pythonpath = "src" [tool.bumpver] -current_version = "2025.10.03-1" +current_version = "2025.10.22-0" version_pattern = "YYYY.0M.0D-INC0" commit_message = "bump version {old_version} -> {new_version}" tag_message = "{new_version}" diff --git a/src/servala/__about__.py b/src/servala/__about__.py index 90f10c1..fb0c6b7 100644 --- a/src/servala/__about__.py +++ b/src/servala/__about__.py @@ -1 +1 @@ -__version__ = "2025.10.03-1" +__version__ = "2025.10.22-0" From e5745ebb50ec446cab53ec09d12d78860dbd2a41 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Wed, 22 Oct 2025 16:30:28 +0200 Subject: [PATCH 087/185] bump version 2025.10.22-0 -> 2025.10.22-1 --- README.md | 2 +- pyproject.toml | 2 +- src/servala/__about__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4231d69..3f503dd 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ The Servala Self-Service Portal -Latest release: 2025.10.22-0 +Latest release: 2025.10.22-1 ## Documentation diff --git a/pyproject.toml b/pyproject.toml index 1f0516a..61fa9e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ testpaths = "src/tests" pythonpath = "src" [tool.bumpver] -current_version = "2025.10.22-0" +current_version = "2025.10.22-1" version_pattern = "YYYY.0M.0D-INC0" commit_message = "bump version {old_version} -> {new_version}" tag_message = "{new_version}" diff --git a/src/servala/__about__.py b/src/servala/__about__.py index fb0c6b7..888e9bd 100644 --- a/src/servala/__about__.py +++ b/src/servala/__about__.py @@ -1 +1 @@ -__version__ = "2025.10.22-0" +__version__ = "2025.10.22-1" From 63c4b806ec4cdeedfb35956fb234c055aab3760a Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 23 Oct 2025 03:01:55 +0000 Subject: [PATCH 088/185] Update actions/setup-node action to v6 --- .forgejo/workflows/renovate.yaml | 2 +- .forgejo/workflows/tests.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.forgejo/workflows/renovate.yaml b/.forgejo/workflows/renovate.yaml index 9821f8f..0428714 100644 --- a/.forgejo/workflows/renovate.yaml +++ b/.forgejo/workflows/renovate.yaml @@ -14,7 +14,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: "22" diff --git a/.forgejo/workflows/tests.yaml b/.forgejo/workflows/tests.yaml index 767e526..3d637f3 100644 --- a/.forgejo/workflows/tests.yaml +++ b/.forgejo/workflows/tests.yaml @@ -21,7 +21,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: "22" From 62faaf2ed94fcc0b38891f596af37dc19ca710ff Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 23 Oct 2025 03:02:02 +0000 Subject: [PATCH 089/185] Update dependency pillow to v12 --- pyproject.toml | 2 +- uv.lock | 103 +++++++++++++++++++++++++------------------------ 2 files changed, 54 insertions(+), 51 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 61fa9e5..2c15e56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ dependencies = [ "django-template-partials>=25.2", "jsonschema>=4.25.1", "kubernetes>=34.1.0", - "pillow>=11.3.0", + "pillow>=12.0.0", "psycopg2-binary>=2.9.11", "pyjwt>=2.10.1", "requests>=2.32.5", diff --git a/uv.lock b/uv.lock index 01b9943..a916da4 100644 --- a/uv.lock +++ b/uv.lock @@ -764,57 +764,60 @@ wheels = [ [[package]] name = "pillow" -version = "11.3.0" +version = "12.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" }, - { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" }, - { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" }, - { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" }, - { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" }, - { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" }, - { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" }, - { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" }, - { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" }, - { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" }, - { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" }, - { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" }, - { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" }, - { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" }, - { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" }, - { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" }, - { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" }, - { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" }, - { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" }, - { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" }, - { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" }, - { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" }, - { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" }, - { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" }, - { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" }, - { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" }, - { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" }, - { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" }, - { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" }, - { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" }, - { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" }, - { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" }, - { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" }, - { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" }, - { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" }, - { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" }, - { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" }, - { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" }, - { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" }, - { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" }, - { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" }, - { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/de993bb2d21b33a98d031ecf6a978e4b61da207bef02f7b43093774c480d/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", size = 4045493, upload-time = "2025-10-15T18:22:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b6/bc8d0c4c9f6f111a783d045310945deb769b806d7574764234ffd50bc5ea/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", size = 4120461, upload-time = "2025-10-15T18:22:27.286Z" }, + { url = "https://files.pythonhosted.org/packages/5d/57/d60d343709366a353dc56adb4ee1e7d8a2cc34e3fbc22905f4167cfec119/pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", size = 3576912, upload-time = "2025-10-15T18:22:28.751Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a4/a0a31467e3f83b94d37568294b01d22b43ae3c5d85f2811769b9c66389dd/pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", size = 5249132, upload-time = "2025-10-15T18:22:30.641Z" }, + { url = "https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", size = 4650099, upload-time = "2025-10-15T18:22:32.73Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bd/69ed99fd46a8dba7c1887156d3572fe4484e3f031405fcc5a92e31c04035/pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", size = 6230808, upload-time = "2025-10-15T18:22:34.337Z" }, + { url = "https://files.pythonhosted.org/packages/ea/94/8fad659bcdbf86ed70099cb60ae40be6acca434bbc8c4c0d4ef356d7e0de/pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07", size = 8037804, upload-time = "2025-10-15T18:22:36.402Z" }, + { url = "https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e", size = 6345553, upload-time = "2025-10-15T18:22:38.066Z" }, + { url = "https://files.pythonhosted.org/packages/38/57/755dbd06530a27a5ed74f8cb0a7a44a21722ebf318edbe67ddbd7fb28f88/pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", size = 7037729, upload-time = "2025-10-15T18:22:39.769Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/7e94f4c41d238615674d06ed677c14883103dce1c52e4af16f000338cfd7/pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", size = 6459789, upload-time = "2025-10-15T18:22:41.437Z" }, + { url = "https://files.pythonhosted.org/packages/9c/14/4448bb0b5e0f22dd865290536d20ec8a23b64e2d04280b89139f09a36bb6/pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", size = 7130917, upload-time = "2025-10-15T18:22:43.152Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ca/16c6926cc1c015845745d5c16c9358e24282f1e588237a4c36d2b30f182f/pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", size = 6302391, upload-time = "2025-10-15T18:22:44.753Z" }, + { url = "https://files.pythonhosted.org/packages/6d/2a/dd43dcfd6dae9b6a49ee28a8eedb98c7d5ff2de94a5d834565164667b97b/pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", size = 7007477, upload-time = "2025-10-15T18:22:46.838Z" }, + { url = "https://files.pythonhosted.org/packages/77/f0/72ea067f4b5ae5ead653053212af05ce3705807906ba3f3e8f58ddf617e6/pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", size = 2435918, upload-time = "2025-10-15T18:22:48.399Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5e/9046b423735c21f0487ea6cb5b10f89ea8f8dfbe32576fe052b5ba9d4e5b/pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", size = 5251406, upload-time = "2025-10-15T18:22:49.905Z" }, + { url = "https://files.pythonhosted.org/packages/12/66/982ceebcdb13c97270ef7a56c3969635b4ee7cd45227fa707c94719229c5/pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", size = 4653218, upload-time = "2025-10-15T18:22:51.587Z" }, + { url = "https://files.pythonhosted.org/packages/16/b3/81e625524688c31859450119bf12674619429cab3119eec0e30a7a1029cb/pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", size = 6266564, upload-time = "2025-10-15T18:22:53.215Z" }, + { url = "https://files.pythonhosted.org/packages/98/59/dfb38f2a41240d2408096e1a76c671d0a105a4a8471b1871c6902719450c/pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", size = 8069260, upload-time = "2025-10-15T18:22:54.933Z" }, + { url = "https://files.pythonhosted.org/packages/dc/3d/378dbea5cd1874b94c312425ca77b0f47776c78e0df2df751b820c8c1d6c/pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", size = 6379248, upload-time = "2025-10-15T18:22:56.605Z" }, + { url = "https://files.pythonhosted.org/packages/84/b0/d525ef47d71590f1621510327acec75ae58c721dc071b17d8d652ca494d8/pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", size = 7066043, upload-time = "2025-10-15T18:22:58.53Z" }, + { url = "https://files.pythonhosted.org/packages/61/2c/aced60e9cf9d0cde341d54bf7932c9ffc33ddb4a1595798b3a5150c7ec4e/pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", size = 6490915, upload-time = "2025-10-15T18:23:00.582Z" }, + { url = "https://files.pythonhosted.org/packages/ef/26/69dcb9b91f4e59f8f34b2332a4a0a951b44f547c4ed39d3e4dcfcff48f89/pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", size = 7157998, upload-time = "2025-10-15T18:23:02.627Z" }, + { url = "https://files.pythonhosted.org/packages/61/2b/726235842220ca95fa441ddf55dd2382b52ab5b8d9c0596fe6b3f23dafe8/pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", size = 6306201, upload-time = "2025-10-15T18:23:04.709Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/2afaf4e840b2df71344ababf2f8edd75a705ce500e5dc1e7227808312ae1/pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", size = 7013165, upload-time = "2025-10-15T18:23:06.46Z" }, + { url = "https://files.pythonhosted.org/packages/6f/75/3fa09aa5cf6ed04bee3fa575798ddf1ce0bace8edb47249c798077a81f7f/pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", size = 2437834, upload-time = "2025-10-15T18:23:08.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" }, + { url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" }, + { url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" }, + { url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" }, + { url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222, upload-time = "2025-10-15T18:23:20.909Z" }, + { url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657, upload-time = "2025-10-15T18:23:23.077Z" }, + { url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" }, + { url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" }, + { url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" }, + { url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" }, + { url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" }, + { url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" }, + { url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" }, + { url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001, upload-time = "2025-10-15T18:23:44.29Z" }, + { url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" }, + { url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" }, ] [[package]] @@ -1269,7 +1272,7 @@ requires-dist = [ { name = "django-template-partials", specifier = ">=25.2" }, { name = "jsonschema", specifier = ">=4.25.1" }, { name = "kubernetes", specifier = ">=34.1.0" }, - { name = "pillow", specifier = ">=11.3.0" }, + { name = "pillow", specifier = ">=12.0.0" }, { name = "psycopg2-binary", specifier = ">=2.9.11" }, { name = "pyjwt", specifier = ">=2.10.1" }, { name = "requests", specifier = ">=2.32.5" }, From 6c2795d4faed723a58ef3961a7642d0ca9520f06 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 23 Oct 2025 03:01:33 +0000 Subject: [PATCH 090/185] Update https://github.com/renovatebot/github-action action to v43.0.18 --- .forgejo/workflows/renovate.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/renovate.yaml b/.forgejo/workflows/renovate.yaml index 0428714..300e7b3 100644 --- a/.forgejo/workflows/renovate.yaml +++ b/.forgejo/workflows/renovate.yaml @@ -19,7 +19,7 @@ jobs: node-version: "22" - name: Renovate - uses: https://github.com/renovatebot/github-action@v43.0.17 + uses: https://github.com/renovatebot/github-action@v43.0.18 with: token: ${{ secrets.RENOVATE_TOKEN }} env: From 214dff3ae48ecadb0d768ba04ed5715176841bc8 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 24 Oct 2025 03:02:01 +0000 Subject: [PATCH 091/185] Update dependency flake8-bugbear to v25 --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 61fa9e5..c8b34df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ dev = [ "coverage>=7.11.0", "djlint>=1.36.4", "flake8>=7.3.0", - "flake8-bugbear>=24.12.12", + "flake8-bugbear>=25.10.21", "flake8-pyproject>=1.2.3", "isort>=6.1.0", "pytest>=8.4.2", diff --git a/uv.lock b/uv.lock index 01b9943..81d9987 100644 --- a/uv.lock +++ b/uv.lock @@ -566,15 +566,15 @@ wheels = [ [[package]] name = "flake8-bugbear" -version = "24.12.12" +version = "25.10.21" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "flake8" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c7/25/48ba712ff589b0149f21135234f9bb45c14d6689acc6151b5e2ff8ac2ae9/flake8_bugbear-24.12.12.tar.gz", hash = "sha256:46273cef0a6b6ff48ca2d69e472f41420a42a46e24b2a8972e4f0d6733d12a64", size = 82907, upload-time = "2024-12-12T16:49:26.307Z" } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/21/0a875f75fbe4008bd171e2fefa413536258fe6b4cfaaa087986de74588f4/flake8_bugbear-24.12.12-py3-none-any.whl", hash = "sha256:1b6967436f65ca22a42e5373aaa6f2d87966ade9aa38d4baf2a1be550767545e", size = 36664, upload-time = "2024-12-12T16:49:23.584Z" }, + { 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" }, ] [[package]] @@ -1285,7 +1285,7 @@ dev = [ { name = "coverage", specifier = ">=7.11.0" }, { name = "djlint", specifier = ">=1.36.4" }, { name = "flake8", specifier = ">=7.3.0" }, - { name = "flake8-bugbear", specifier = ">=24.12.12" }, + { name = "flake8-bugbear", specifier = ">=25.10.21" }, { name = "flake8-pyproject", specifier = ">=1.2.3" }, { name = "isort", specifier = ">=6.1.0" }, { name = "pytest", specifier = ">=8.4.2" }, From 2f593360febd2d2187f3d7ce3e470fb672cc6eed Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Fri, 24 Oct 2025 11:28:30 +0200 Subject: [PATCH 092/185] Fix generated FQDN not being submitted Closes #241 --- src/servala/static/js/dynamic-array.js | 2 ++ src/servala/static/js/fqdn.js | 10 +++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/servala/static/js/dynamic-array.js b/src/servala/static/js/dynamic-array.js index b6fa4e5..2d0098b 100644 --- a/src/servala/static/js/dynamic-array.js +++ b/src/servala/static/js/dynamic-array.js @@ -129,6 +129,8 @@ const updateRemoveButtonVisibility = (container) => { }) } +window.updateHiddenInput = updateHiddenInput + document.addEventListener('DOMContentLoaded', initDynamicArrayWidget) document.addEventListener('htmx:afterSwap', initDynamicArrayWidget) document.addEventListener('htmx:afterSettle', initDynamicArrayWidget) diff --git a/src/servala/static/js/fqdn.js b/src/servala/static/js/fqdn.js index 7b61c9a..a92071d 100644 --- a/src/servala/static/js/fqdn.js +++ b/src/servala/static/js/fqdn.js @@ -1,6 +1,6 @@ const initializeFqdnGeneration = () => { - const nameField = document.querySelector('input[name="name"]'); + const nameField = document.querySelector('input#id_name'); const fqdnField = document.querySelector('label[for="id_spec.parameters.service.fqdn"] + div input.array-item-input'); if (nameField && fqdnField) { @@ -17,6 +17,10 @@ const initializeFqdnGeneration = () => { newNameField.addEventListener('input', function() { if (!newFqdnField.dataset.manuallyEdited) { newFqdnField.value = generateFqdn(this.value); + const container = newFqdnField.closest('.dynamic-array-widget'); + if (container && window.updateHiddenInput) { + window.updateHiddenInput(container); + } } }); @@ -26,6 +30,10 @@ const initializeFqdnGeneration = () => { if (newNameField.value && !newFqdnField.value) { newFqdnField.value = generateFqdn(newNameField.value); + const container = newFqdnField.closest('.dynamic-array-widget'); + if (container && window.updateHiddenInput) { + window.updateHiddenInput(container); + } } } } From bca79be02a28f0a0c6bcb024013051bd6599bba8 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Fri, 24 Oct 2025 11:55:56 +0200 Subject: [PATCH 093/185] Inline user info in service offering page ref #243 --- .../service_instance_detail.html | 11 ++++++- .../service_offering_detail.html | 27 ++++++++------- .../includes/control_plane_user_info.html | 33 +++++++------------ 3 files changed, 37 insertions(+), 34 deletions(-) diff --git a/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html b/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html index d375344..4f0f00e 100644 --- a/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html +++ b/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html @@ -102,7 +102,16 @@
{% endif %} - {% include "includes/control_plane_user_info.html" with control_plane=instance.context.control_plane %} + {% if control_plane.user_info %} +
+
+

{% translate "Service Provider Zone Information" %}

+
+
+ {% include "includes/control_plane_user_info.html" with control_plane=instance.context.control_plane %} +
+
+ {% endif %}
{% if instance.spec and spec_fieldsets %}
diff --git a/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html b/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html index 842e610..df57811 100644 --- a/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html +++ b/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html @@ -7,9 +7,14 @@ {{ offering }} {% endblock page_title %} {% endblock html_title %} -{% partialdef control-plane-info %} -{% if selected_plane %} - {% include "includes/control_plane_user_info.html" with control_plane=selected_plane %} +{% partialdef control-plane-info inline=True %} +{% if selected_plane and selected_plane.user_info %} +
+
+
{% translate "Service Provider Zone Information" %}
+ {% include "includes/control_plane_user_info.html" with control_plane=selected_plane %} +
+
{% endif %} {% endpartialdef %} {% partialdef service-form %} @@ -31,7 +36,7 @@ {% block content %}
-
+
{% if service.logo %} @@ -64,6 +69,13 @@ {{ select_form }} {% endif %} + {% if has_control_planes %} +
+ {% partial control-plane-info %} +
+ {% endif %} {% if service.external_links or offering.external_links %}
@@ -83,13 +95,6 @@
{% partial service-form %}
-
- {% if has_control_planes %} -
{% partial control-plane-info %}
- {% endif %} -
{% endblock content %} diff --git a/src/servala/frontend/templates/includes/control_plane_user_info.html b/src/servala/frontend/templates/includes/control_plane_user_info.html index b9ffe99..e8fcee8 100644 --- a/src/servala/frontend/templates/includes/control_plane_user_info.html +++ b/src/servala/frontend/templates/includes/control_plane_user_info.html @@ -1,26 +1,15 @@ {% load i18n %} -{% comment %} -Reusable snippet for displaying ControlPlane user_info -Usage: {% include "includes/control_plane_user_info.html" with control_plane=control_plane_object %} -{% endcomment %} {% if control_plane.user_info %} -
-
-

{% translate "Service Provider Zone Information" %}

-
-
-
- - - {% for key, value in control_plane.user_info.items %} - - - - - {% endfor %} - -
{{ key }}{{ value }}
-
-
+
+ + + {% for key, value in control_plane.user_info.items %} + + + + + {% endfor %} + +
{{ key }}{{ value }}
{% endif %} From 75fe0799e0f3dc6fe961d43fb4d725d0594aa815 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Fri, 24 Oct 2025 12:16:38 +0200 Subject: [PATCH 094/185] Implement user info help text with popovers --- src/servala/core/forms.py | 23 ++++++++++++++----- src/servala/core/models/service.py | 3 ++- .../service_instance_detail.html | 9 ++++++++ .../service_offering_detail.html | 15 ++++++++++++ .../includes/control_plane_user_info.html | 16 ++++++++++--- 5 files changed, 56 insertions(+), 10 deletions(-) diff --git a/src/servala/core/forms.py b/src/servala/core/forms.py index baa85fb..034d1c9 100644 --- a/src/servala/core/forms.py +++ b/src/servala/core/forms.py @@ -5,14 +5,25 @@ from django_jsonform.widgets import JSONFormWidget from servala.core.models import ControlPlane, ServiceDefinition CONTROL_PLANE_USER_INFO_SCHEMA = { - "type": "object", - "properties": { - "CNAME Record": { - "title": "CNAME Record", - "type": "string", + "type": "array", + "items": { + "type": "object", + "properties": { + "title": { + "type": "string", + "title": "Title", + }, + "content": { + "type": "string", + "title": "Content", + }, + "help_text": { + "type": "string", + "title": "Help Text (optional)", + }, }, + "required": ["title", "content"], }, - "additionalProperties": {"type": "string"}, } diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index 42fc500..3af8c89 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -156,7 +156,8 @@ class ControlPlane(ServalaModelMixin, models.Model): blank=True, verbose_name=_("User Information"), help_text=_( - "Key-value information displayed to users when selecting this control plane" + 'Array of info objects: [{"title": "…", "content": "…", "help_text": "…"}]. ' + "The help_text field is optional and will be shown as a hover popover on an info icon." ), ) wildcard_dns = models.CharField( diff --git a/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html b/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html index 4f0f00e..4aaef14 100644 --- a/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html +++ b/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html @@ -248,3 +248,12 @@
{% endblock content %} +{% block extra_js %} + +{% endblock extra_js %} diff --git a/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html b/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html index df57811..6fdc61a 100644 --- a/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html +++ b/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html @@ -108,4 +108,19 @@ {% endif %} + {% endblock extra_js %} diff --git a/src/servala/frontend/templates/includes/control_plane_user_info.html b/src/servala/frontend/templates/includes/control_plane_user_info.html index e8fcee8..21d5cdb 100644 --- a/src/servala/frontend/templates/includes/control_plane_user_info.html +++ b/src/servala/frontend/templates/includes/control_plane_user_info.html @@ -3,10 +3,20 @@
- {% for key, value in control_plane.user_info.items %} + {% for info in control_plane.user_info %} - - + + {% endfor %} From b8f3621b47038884f8d09e6e99aaa88d1d5e9c2a Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Fri, 24 Oct 2025 12:19:04 +0200 Subject: [PATCH 095/185] Migrate user info data --- .../0012_convert_user_info_to_array.py | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/servala/core/migrations/0012_convert_user_info_to_array.py diff --git a/src/servala/core/migrations/0012_convert_user_info_to_array.py b/src/servala/core/migrations/0012_convert_user_info_to_array.py new file mode 100644 index 0000000..892949e --- /dev/null +++ b/src/servala/core/migrations/0012_convert_user_info_to_array.py @@ -0,0 +1,65 @@ +# Generated by Django 5.2.7 on 2025-10-24 10:04 + +from django.db import migrations + + +def convert_user_info_to_array(apps, schema_editor): + """ + Convert user_info from object format {"key": "value"} to array format + [{"title": "key", "content": "value"}]. + """ + ControlPlane = apps.get_model("core", "ControlPlane") + + for control_plane in ControlPlane.objects.all(): + if not control_plane.user_info: + continue + + # If it's already an array (migration already run or new format), skip + if isinstance(control_plane.user_info, list): + continue + + # Convert from dict to array + if isinstance(control_plane.user_info, dict): + new_user_info = [] + for key, value in control_plane.user_info.items(): + new_user_info.append({"title": key, "content": value}) + + control_plane.user_info = new_user_info + control_plane.save(update_fields=["user_info"]) + + +def reverse_user_info_to_object(apps, schema_editor): + """ + Reverse the migration by converting array format back to object format. + Note: help_text will be lost during reversal. + """ + ControlPlane = apps.get_model("core", "ControlPlane") + + for control_plane in ControlPlane.objects.all(): + if not control_plane.user_info: + continue + + # If it's already an object, skip + if isinstance(control_plane.user_info, dict): + continue + + # Convert from array to dict + if isinstance(control_plane.user_info, list): + new_user_info = {} + for item in control_plane.user_info: + if isinstance(item, dict) and "title" in item and "content" in item: + new_user_info[item["title"]] = item["content"] + + control_plane.user_info = new_user_info + control_plane.save(update_fields=["user_info"]) + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0011_alter_organizationorigin_billing_entity"), + ] + + operations = [ + migrations.RunPython(convert_user_info_to_array, reverse_user_info_to_object), + ] From 1a919fa68e3579f1659c754ed1cf197b5a0e8df5 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 27 Oct 2025 03:01:55 +0000 Subject: [PATCH 096/185] Lock file maintenance --- uv.lock | 550 ++++++++++++++++++++++++++++++-------------------------- 1 file changed, 291 insertions(+), 259 deletions(-) diff --git a/uv.lock b/uv.lock index 81d9987..60f8e5f 100644 --- a/uv.lock +++ b/uv.lock @@ -11,47 +11,19 @@ name = "argon2-cffi" version = "25.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "argon2-cffi-bindings", version = "21.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, - { name = "argon2-cffi-bindings", version = "25.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "argon2-cffi-bindings" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, ] -[[package]] -name = "argon2-cffi-bindings" -version = "21.2.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14'", -] -dependencies = [ - { name = "cffi", marker = "python_full_version >= '3.14'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b9/e9/184b8ccce6683b0aa2fbb7ba5683ea4b9c5763f1356347f1312c32e3c66e/argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3", size = 1779911, upload-time = "2021-12-01T08:52:55.68Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/13/838ce2620025e9666aa8f686431f67a29052241692a3dd1ae9d3692a89d3/argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367", size = 29658, upload-time = "2021-12-01T09:09:17.016Z" }, - { url = "https://files.pythonhosted.org/packages/b3/02/f7f7bb6b6af6031edb11037639c697b912e1dea2db94d436e681aea2f495/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d", size = 80583, upload-time = "2021-12-01T09:09:19.546Z" }, - { url = "https://files.pythonhosted.org/packages/ec/f7/378254e6dd7ae6f31fe40c8649eea7d4832a42243acaf0f1fff9083b2bed/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae", size = 86168, upload-time = "2021-12-01T09:09:21.445Z" }, - { url = "https://files.pythonhosted.org/packages/74/f6/4a34a37a98311ed73bb80efe422fed95f2ac25a4cacc5ae1d7ae6a144505/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c", size = 82709, upload-time = "2021-12-01T09:09:18.182Z" }, - { url = "https://files.pythonhosted.org/packages/74/2b/73d767bfdaab25484f7e7901379d5f8793cccbb86c6e0cbc4c1b96f63896/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86", size = 83613, upload-time = "2021-12-01T09:09:22.741Z" }, - { url = "https://files.pythonhosted.org/packages/4f/fd/37f86deef67ff57c76f137a67181949c2d408077e2e3dd70c6c42912c9bf/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f", size = 84583, upload-time = "2021-12-01T09:09:24.177Z" }, - { url = "https://files.pythonhosted.org/packages/6f/52/5a60085a3dae8fded8327a4f564223029f5f54b0cb0455a31131b5363a01/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e", size = 88475, upload-time = "2021-12-01T09:09:26.673Z" }, - { url = "https://files.pythonhosted.org/packages/8b/95/143cd64feb24a15fa4b189a3e1e7efbaeeb00f39a51e99b26fc62fbacabd/argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082", size = 27698, upload-time = "2021-12-01T09:09:27.87Z" }, - { url = "https://files.pythonhosted.org/packages/37/2c/e34e47c7dee97ba6f01a6203e0383e15b60fb85d78ac9a15cd066f6fe28b/argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f", size = 30817, upload-time = "2021-12-01T09:09:30.267Z" }, - { url = "https://files.pythonhosted.org/packages/5a/e4/bf8034d25edaa495da3c8a3405627d2e35758e44ff6eaa7948092646fdcc/argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93", size = 53104, upload-time = "2021-12-01T09:09:31.335Z" }, -] - [[package]] name = "argon2-cffi-bindings" version = "25.1.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.14'", -] dependencies = [ - { name = "cffi", marker = "python_full_version < '3.14'" }, + { name = "cffi" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } wheels = [ @@ -79,20 +51,20 @@ wheels = [ [[package]] name = "asgiref" -version = "3.9.1" +version = "3.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/90/61/0aa957eec22ff70b830b22ff91f825e70e1ef732c06666a805730f28b36b/asgiref-3.9.1.tar.gz", hash = "sha256:a5ab6582236218e5ef1648f242fd9f10626cfd4de8dc377db215d5d5098e3142", size = 36870, upload-time = "2025-07-08T09:07:43.344Z" } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/3c/0464dcada90d5da0e71018c04a140ad6349558afb30b3051b4264cc5b965/asgiref-3.9.1-py3-none-any.whl", hash = "sha256:f3bba7092a48005b5f5bacd747d36ee4a5a61f4a269a6df590b43144355ebd2c", size = 23790, upload-time = "2025-07-08T09:07:41.548Z" }, + { 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" }, ] [[package]] name = "attrs" -version = "25.3.0" +version = "25.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] [[package]] @@ -118,30 +90,30 @@ wheels = [ [[package]] name = "boto3" -version = "1.40.19" +version = "1.40.59" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d4/d6/f67e90c53f499a12353e6f19104fc55f9fc9ec514207dbe08e1e1de9a45b/boto3-1.40.19.tar.gz", hash = "sha256:772f259fdef6efa752c5744e140c0371593a20a0c728cce91d67b8b58d1090e7", size = 111524, upload-time = "2025-08-27T19:19:38.453Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f4/65f3a0a58a42abaa57cb42968535dc7c209232c2614d5ac1d8354b0bc0b7/boto3-1.40.59.tar.gz", hash = "sha256:b1a5a203511e594872b39a129365f02eb5846eea990629e8daf47a3c01e7fd49", size = 111577, upload-time = "2025-10-24T19:23:33.763Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/15/3886b46973d10814f0aa89e94e03182f233fd478b2be317e706d9f0e85bd/boto3-1.40.19-py3-none-any.whl", hash = "sha256:9cdf01576fae6cb12b71fd6b793f34876feafa962cdaf3a9489253580355fc60", size = 139324, upload-time = "2025-08-27T19:19:36.693Z" }, + { url = "https://files.pythonhosted.org/packages/d4/61/25cbd486b03d4f786507584025a35ae74fa52fe2408fbf0318d91e3e96db/boto3-1.40.59-py3-none-any.whl", hash = "sha256:75752e7dc445131700a58926a50ca705794232f0f47d0e21edb59fbf1898db95", size = 139323, upload-time = "2025-10-24T19:23:31.824Z" }, ] [[package]] name = "botocore" -version = "1.40.19" +version = "1.40.59" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/8c/8e319b9936fea23a3be19fac2921dd91cc60a99c0cf771d7d676e3329bb3/botocore-1.40.19.tar.gz", hash = "sha256:becc101b3047ec4cffa6c86bab747b8312db20529ee0132fe77007092a9c9f85", size = 14320063, upload-time = "2025-08-27T19:19:27.94Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/0a/4abd361449e495bc6f0eb24dc14213c1468253a5be63cfcd3b6f9feca992/botocore-1.40.59.tar.gz", hash = "sha256:842a466d8735272a30fe5b7f97df559d9e211a18e412f62a17ed249fd62f85fe", size = 14472896, upload-time = "2025-10-24T19:23:22.467Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/11/a633975158d79bb361ebaa802c393f5408b4cf95226ba3abe573f29741fb/botocore-1.40.19-py3-none-any.whl", hash = "sha256:6a7c2ceaf8ed3321cf4bc15420dad4e778263d3b480c86f7fd9da982e1deaa64", size = 13985499, upload-time = "2025-08-27T19:19:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/50/34/72ba24f52b14669384ede828ea08927b444c52311e67e02d9cdc6f00b882/botocore-1.40.59-py3-none-any.whl", hash = "sha256:042dd844ca82155ca1ab9608b9bef36d517515c775d075f57b89257108ae843b", size = 14139459, upload-time = "2025-10-24T19:23:18.425Z" }, ] [[package]] @@ -161,20 +133,20 @@ wheels = [ [[package]] name = "cachetools" -version = "5.5.2" +version = "6.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380, upload-time = "2025-02-20T21:01:19.524Z" } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" }, + { 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" }, ] [[package]] name = "certifi" -version = "2025.8.3" +version = "2025.10.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, + { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" }, ] [[package]] @@ -224,45 +196,55 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.3" +version = "3.4.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, - { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, - { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, - { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, - { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, - { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, - { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, - { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, - { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, - { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, - { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, - { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, - { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, - { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, - { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, - { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, - { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, - { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, - { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, - { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, - { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] [[package]] name = "click" -version = "8.2.1" +version = "8.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, ] [[package]] @@ -590,43 +572,43 @@ wheels = [ [[package]] name = "google-auth" -version = "2.40.3" +version = "2.41.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, { name = "pyasn1-modules" }, { name = "rsa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/9b/e92ef23b84fa10a64ce4831390b7a4c2e53c0132568d99d4ae61d04c8855/google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77", size = 281029, upload-time = "2025-06-04T18:04:57.577Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/af/5129ce5b2f9688d2fa49b463e544972a7c82b0fdb50980dafee92e121d9f/google_auth-2.41.1.tar.gz", hash = "sha256:b76b7b1f9e61f0cb7e88870d14f6a94aeef248959ef6992670efee37709cbfd2", size = 292284, upload-time = "2025-09-30T22:51:26.363Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/63/b19553b658a1692443c62bd07e5868adaa0ad746a0751ba62c59568cd45b/google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca", size = 216137, upload-time = "2025-06-04T18:04:55.573Z" }, + { url = "https://files.pythonhosted.org/packages/be/a4/7319a2a8add4cc352be9e3efeff5e2aacee917c85ca2fa1647e29089983c/google_auth-2.41.1-py2.py3-none-any.whl", hash = "sha256:754843be95575b9a19c604a848a41be03f7f2afd8c019f716dc1f51ee41c639d", size = 221302, upload-time = "2025-09-30T22:51:24.212Z" }, ] [[package]] name = "idna" -version = "3.10" +version = "3.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] [[package]] name = "iniconfig" -version = "2.1.0" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] name = "isort" -version = "6.1.0" +version = "7.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1e/82/fa43935523efdfcce6abbae9da7f372b627b27142c3419fcf13bf5b0c397/isort-6.1.0.tar.gz", hash = "sha256:9b8f96a14cfee0677e78e941ff62f03769a06d412aabb9e2a90487b3b7e8d481", size = 824325, upload-time = "2025-10-01T16:26:45.027Z" } +sdist = { url = "https://files.pythonhosted.org/packages/63/53/4f3c058e3bace40282876f9b553343376ee687f3c35a525dc79dbd450f88/isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187", size = 805049, upload-time = "2025-10-11T13:30:59.107Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/cc/9b681a170efab4868a032631dea1e8446d8ec718a7f657b94d49d1a12643/isort-6.1.0-py3-none-any.whl", hash = "sha256:58d8927ecce74e5087aef019f778d4081a3b6c98f15a80ba35782ca8a2097784", size = 94329, upload-time = "2025-10-01T16:26:43.291Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ed/e3705d6d02b4f7aea715a353c8ce193efd0b5db13e204df895d38734c244/isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1", size = 94672, upload-time = "2025-10-11T13:30:57.665Z" }, ] [[package]] @@ -677,14 +659,14 @@ wheels = [ [[package]] name = "jsonschema-specifications" -version = "2025.4.1" +version = "2025.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "referencing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513, upload-time = "2025-04-23T12:34:07.418Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" }, + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] [[package]] @@ -764,66 +746,69 @@ wheels = [ [[package]] name = "pillow" -version = "11.3.0" +version = "12.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" }, - { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" }, - { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" }, - { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" }, - { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" }, - { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" }, - { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" }, - { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" }, - { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" }, - { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" }, - { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" }, - { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" }, - { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" }, - { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" }, - { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" }, - { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" }, - { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" }, - { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" }, - { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" }, - { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" }, - { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" }, - { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" }, - { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" }, - { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" }, - { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" }, - { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" }, - { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" }, - { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" }, - { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" }, - { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" }, - { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" }, - { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" }, - { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" }, - { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" }, - { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" }, - { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" }, - { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" }, - { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" }, - { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" }, - { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" }, - { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" }, - { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/de993bb2d21b33a98d031ecf6a978e4b61da207bef02f7b43093774c480d/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", size = 4045493, upload-time = "2025-10-15T18:22:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b6/bc8d0c4c9f6f111a783d045310945deb769b806d7574764234ffd50bc5ea/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", size = 4120461, upload-time = "2025-10-15T18:22:27.286Z" }, + { url = "https://files.pythonhosted.org/packages/5d/57/d60d343709366a353dc56adb4ee1e7d8a2cc34e3fbc22905f4167cfec119/pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", size = 3576912, upload-time = "2025-10-15T18:22:28.751Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a4/a0a31467e3f83b94d37568294b01d22b43ae3c5d85f2811769b9c66389dd/pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", size = 5249132, upload-time = "2025-10-15T18:22:30.641Z" }, + { url = "https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", size = 4650099, upload-time = "2025-10-15T18:22:32.73Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bd/69ed99fd46a8dba7c1887156d3572fe4484e3f031405fcc5a92e31c04035/pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", size = 6230808, upload-time = "2025-10-15T18:22:34.337Z" }, + { url = "https://files.pythonhosted.org/packages/ea/94/8fad659bcdbf86ed70099cb60ae40be6acca434bbc8c4c0d4ef356d7e0de/pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07", size = 8037804, upload-time = "2025-10-15T18:22:36.402Z" }, + { url = "https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e", size = 6345553, upload-time = "2025-10-15T18:22:38.066Z" }, + { url = "https://files.pythonhosted.org/packages/38/57/755dbd06530a27a5ed74f8cb0a7a44a21722ebf318edbe67ddbd7fb28f88/pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", size = 7037729, upload-time = "2025-10-15T18:22:39.769Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/7e94f4c41d238615674d06ed677c14883103dce1c52e4af16f000338cfd7/pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", size = 6459789, upload-time = "2025-10-15T18:22:41.437Z" }, + { url = "https://files.pythonhosted.org/packages/9c/14/4448bb0b5e0f22dd865290536d20ec8a23b64e2d04280b89139f09a36bb6/pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", size = 7130917, upload-time = "2025-10-15T18:22:43.152Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ca/16c6926cc1c015845745d5c16c9358e24282f1e588237a4c36d2b30f182f/pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", size = 6302391, upload-time = "2025-10-15T18:22:44.753Z" }, + { url = "https://files.pythonhosted.org/packages/6d/2a/dd43dcfd6dae9b6a49ee28a8eedb98c7d5ff2de94a5d834565164667b97b/pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", size = 7007477, upload-time = "2025-10-15T18:22:46.838Z" }, + { url = "https://files.pythonhosted.org/packages/77/f0/72ea067f4b5ae5ead653053212af05ce3705807906ba3f3e8f58ddf617e6/pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", size = 2435918, upload-time = "2025-10-15T18:22:48.399Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5e/9046b423735c21f0487ea6cb5b10f89ea8f8dfbe32576fe052b5ba9d4e5b/pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", size = 5251406, upload-time = "2025-10-15T18:22:49.905Z" }, + { url = "https://files.pythonhosted.org/packages/12/66/982ceebcdb13c97270ef7a56c3969635b4ee7cd45227fa707c94719229c5/pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", size = 4653218, upload-time = "2025-10-15T18:22:51.587Z" }, + { url = "https://files.pythonhosted.org/packages/16/b3/81e625524688c31859450119bf12674619429cab3119eec0e30a7a1029cb/pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", size = 6266564, upload-time = "2025-10-15T18:22:53.215Z" }, + { url = "https://files.pythonhosted.org/packages/98/59/dfb38f2a41240d2408096e1a76c671d0a105a4a8471b1871c6902719450c/pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", size = 8069260, upload-time = "2025-10-15T18:22:54.933Z" }, + { url = "https://files.pythonhosted.org/packages/dc/3d/378dbea5cd1874b94c312425ca77b0f47776c78e0df2df751b820c8c1d6c/pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", size = 6379248, upload-time = "2025-10-15T18:22:56.605Z" }, + { url = "https://files.pythonhosted.org/packages/84/b0/d525ef47d71590f1621510327acec75ae58c721dc071b17d8d652ca494d8/pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", size = 7066043, upload-time = "2025-10-15T18:22:58.53Z" }, + { url = "https://files.pythonhosted.org/packages/61/2c/aced60e9cf9d0cde341d54bf7932c9ffc33ddb4a1595798b3a5150c7ec4e/pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", size = 6490915, upload-time = "2025-10-15T18:23:00.582Z" }, + { url = "https://files.pythonhosted.org/packages/ef/26/69dcb9b91f4e59f8f34b2332a4a0a951b44f547c4ed39d3e4dcfcff48f89/pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", size = 7157998, upload-time = "2025-10-15T18:23:02.627Z" }, + { url = "https://files.pythonhosted.org/packages/61/2b/726235842220ca95fa441ddf55dd2382b52ab5b8d9c0596fe6b3f23dafe8/pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", size = 6306201, upload-time = "2025-10-15T18:23:04.709Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/2afaf4e840b2df71344ababf2f8edd75a705ce500e5dc1e7227808312ae1/pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", size = 7013165, upload-time = "2025-10-15T18:23:06.46Z" }, + { url = "https://files.pythonhosted.org/packages/6f/75/3fa09aa5cf6ed04bee3fa575798ddf1ce0bace8edb47249c798077a81f7f/pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", size = 2437834, upload-time = "2025-10-15T18:23:08.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" }, + { url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" }, + { url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" }, + { url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" }, + { url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222, upload-time = "2025-10-15T18:23:20.909Z" }, + { url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657, upload-time = "2025-10-15T18:23:23.077Z" }, + { url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" }, + { url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" }, + { url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" }, + { url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" }, + { url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" }, + { url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" }, + { url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" }, + { url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001, upload-time = "2025-10-15T18:23:44.29Z" }, + { url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" }, + { url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" }, ] [[package]] name = "platformdirs" -version = "4.4.0" +version = "4.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, + { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, ] [[package]] @@ -893,11 +878,11 @@ wheels = [ [[package]] name = "pycparser" -version = "2.22" +version = "2.23" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, ] [[package]] @@ -995,77 +980,124 @@ wheels = [ [[package]] name = "pytokens" -version = "0.1.10" +version = "0.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/5f/e959a442435e24f6fb5a01aec6c657079ceaca1b3baf18561c3728d681da/pytokens-0.1.10.tar.gz", hash = "sha256:c9a4bfa0be1d26aebce03e6884ba454e842f186a59ea43a6d3b25af58223c044", size = 12171, upload-time = "2025-02-19T14:51:22.001Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/c2/dbadcdddb412a267585459142bfd7cc241e6276db69339353ae6e241ab2b/pytokens-0.2.0.tar.gz", hash = "sha256:532d6421364e5869ea57a9523bf385f02586d4662acbcc0342afd69511b4dd43", size = 15368, upload-time = "2025-10-15T08:02:42.738Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/60/e5/63bed382f6a7a5ba70e7e132b8b7b8abbcf4888ffa6be4877698dcfbed7d/pytokens-0.1.10-py3-none-any.whl", hash = "sha256:db7b72284e480e69fb085d9f251f66b3d2df8b7166059261258ff35f50fb711b", size = 12046, upload-time = "2025-02-19T14:51:18.694Z" }, + { url = "https://files.pythonhosted.org/packages/89/5a/c269ea6b348b6f2c32686635df89f32dbe05df1088dd4579302a6f8f99af/pytokens-0.2.0-py3-none-any.whl", hash = "sha256:74d4b318c67f4295c13782ddd9abcb7e297ec5630ad060eb90abf7ebbefe59f8", size = 12038, upload-time = "2025-10-15T08:02:41.694Z" }, ] [[package]] name = "pyyaml" -version = "6.0.2" +version = "6.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, - { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, - { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, - { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, - { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] [[package]] name = "referencing" -version = "0.36.2" +version = "0.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "rpds-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, ] [[package]] name = "regex" -version = "2025.7.34" +version = "2025.10.23" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/de/e13fa6dc61d78b30ba47481f99933a3b49a57779d625c392d8036770a60d/regex-2025.7.34.tar.gz", hash = "sha256:9ead9765217afd04a86822dfcd4ed2747dfe426e887da413b15ff0ac2457e21a", size = 400714, upload-time = "2025-07-31T00:21:16.262Z" } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/16/b709b2119975035169a25aa8e4940ca177b1a2e25e14f8d996d09130368e/regex-2025.7.34-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c3c9740a77aeef3f5e3aaab92403946a8d34437db930a0280e7e81ddcada61f5", size = 485334, upload-time = "2025-07-31T00:19:56.58Z" }, - { url = "https://files.pythonhosted.org/packages/94/a6/c09136046be0595f0331bc58a0e5f89c2d324cf734e0b0ec53cf4b12a636/regex-2025.7.34-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:69ed3bc611540f2ea70a4080f853741ec698be556b1df404599f8724690edbcd", size = 289942, upload-time = "2025-07-31T00:19:57.943Z" }, - { url = "https://files.pythonhosted.org/packages/36/91/08fc0fd0f40bdfb0e0df4134ee37cfb16e66a1044ac56d36911fd01c69d2/regex-2025.7.34-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d03c6f9dcd562c56527c42b8530aad93193e0b3254a588be1f2ed378cdfdea1b", size = 285991, upload-time = "2025-07-31T00:19:59.837Z" }, - { url = "https://files.pythonhosted.org/packages/be/2f/99dc8f6f756606f0c214d14c7b6c17270b6bbe26d5c1f05cde9dbb1c551f/regex-2025.7.34-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6164b1d99dee1dfad33f301f174d8139d4368a9fb50bf0a3603b2eaf579963ad", size = 797415, upload-time = "2025-07-31T00:20:01.668Z" }, - { url = "https://files.pythonhosted.org/packages/62/cf/2fcdca1110495458ba4e95c52ce73b361cf1cafd8a53b5c31542cde9a15b/regex-2025.7.34-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1e4f4f62599b8142362f164ce776f19d79bdd21273e86920a7b604a4275b4f59", size = 862487, upload-time = "2025-07-31T00:20:03.142Z" }, - { url = "https://files.pythonhosted.org/packages/90/38/899105dd27fed394e3fae45607c1983e138273ec167e47882fc401f112b9/regex-2025.7.34-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:72a26dcc6a59c057b292f39d41465d8233a10fd69121fa24f8f43ec6294e5415", size = 910717, upload-time = "2025-07-31T00:20:04.727Z" }, - { url = "https://files.pythonhosted.org/packages/ee/f6/4716198dbd0bcc9c45625ac4c81a435d1c4d8ad662e8576dac06bab35b17/regex-2025.7.34-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5273fddf7a3e602695c92716c420c377599ed3c853ea669c1fe26218867002f", size = 801943, upload-time = "2025-07-31T00:20:07.1Z" }, - { url = "https://files.pythonhosted.org/packages/40/5d/cff8896d27e4e3dd11dd72ac78797c7987eb50fe4debc2c0f2f1682eb06d/regex-2025.7.34-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c1844be23cd40135b3a5a4dd298e1e0c0cb36757364dd6cdc6025770363e06c1", size = 786664, upload-time = "2025-07-31T00:20:08.818Z" }, - { url = "https://files.pythonhosted.org/packages/10/29/758bf83cf7b4c34f07ac3423ea03cee3eb3176941641e4ccc05620f6c0b8/regex-2025.7.34-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dde35e2afbbe2272f8abee3b9fe6772d9b5a07d82607b5788e8508974059925c", size = 856457, upload-time = "2025-07-31T00:20:10.328Z" }, - { url = "https://files.pythonhosted.org/packages/d7/30/c19d212b619963c5b460bfed0ea69a092c6a43cba52a973d46c27b3e2975/regex-2025.7.34-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f6e8e7af516a7549412ce57613e859c3be27d55341a894aacaa11703a4c31a", size = 849008, upload-time = "2025-07-31T00:20:11.823Z" }, - { url = "https://files.pythonhosted.org/packages/9e/b8/3c35da3b12c87e3cc00010ef6c3a4ae787cff0bc381aa3d251def219969a/regex-2025.7.34-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:469142fb94a869beb25b5f18ea87646d21def10fbacb0bcb749224f3509476f0", size = 788101, upload-time = "2025-07-31T00:20:13.729Z" }, - { url = "https://files.pythonhosted.org/packages/47/80/2f46677c0b3c2b723b2c358d19f9346e714113865da0f5f736ca1a883bde/regex-2025.7.34-cp313-cp313-win32.whl", hash = "sha256:da7507d083ee33ccea1310447410c27ca11fb9ef18c95899ca57ff60a7e4d8f1", size = 264401, upload-time = "2025-07-31T00:20:15.233Z" }, - { url = "https://files.pythonhosted.org/packages/be/fa/917d64dd074682606a003cba33585c28138c77d848ef72fc77cbb1183849/regex-2025.7.34-cp313-cp313-win_amd64.whl", hash = "sha256:9d644de5520441e5f7e2db63aec2748948cc39ed4d7a87fd5db578ea4043d997", size = 275368, upload-time = "2025-07-31T00:20:16.711Z" }, - { url = "https://files.pythonhosted.org/packages/65/cd/f94383666704170a2154a5df7b16be28f0c27a266bffcd843e58bc84120f/regex-2025.7.34-cp313-cp313-win_arm64.whl", hash = "sha256:7bf1c5503a9f2cbd2f52d7e260acb3131b07b6273c470abb78568174fe6bde3f", size = 268482, upload-time = "2025-07-31T00:20:18.189Z" }, - { url = "https://files.pythonhosted.org/packages/ac/23/6376f3a23cf2f3c00514b1cdd8c990afb4dfbac3cb4a68b633c6b7e2e307/regex-2025.7.34-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:8283afe7042d8270cecf27cca558873168e771183d4d593e3c5fe5f12402212a", size = 485385, upload-time = "2025-07-31T00:20:19.692Z" }, - { url = "https://files.pythonhosted.org/packages/73/5b/6d4d3a0b4d312adbfd6d5694c8dddcf1396708976dd87e4d00af439d962b/regex-2025.7.34-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6c053f9647e3421dd2f5dff8172eb7b4eec129df9d1d2f7133a4386319b47435", size = 289788, upload-time = "2025-07-31T00:20:21.941Z" }, - { url = "https://files.pythonhosted.org/packages/92/71/5862ac9913746e5054d01cb9fb8125b3d0802c0706ef547cae1e7f4428fa/regex-2025.7.34-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a16dd56bbcb7d10e62861c3cd000290ddff28ea142ffb5eb3470f183628011ac", size = 286136, upload-time = "2025-07-31T00:20:26.146Z" }, - { url = "https://files.pythonhosted.org/packages/27/df/5b505dc447eb71278eba10d5ec940769ca89c1af70f0468bfbcb98035dc2/regex-2025.7.34-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69c593ff5a24c0d5c1112b0df9b09eae42b33c014bdca7022d6523b210b69f72", size = 797753, upload-time = "2025-07-31T00:20:27.919Z" }, - { url = "https://files.pythonhosted.org/packages/86/38/3e3dc953d13998fa047e9a2414b556201dbd7147034fbac129392363253b/regex-2025.7.34-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98d0ce170fcde1a03b5df19c5650db22ab58af375aaa6ff07978a85c9f250f0e", size = 863263, upload-time = "2025-07-31T00:20:29.803Z" }, - { url = "https://files.pythonhosted.org/packages/68/e5/3ff66b29dde12f5b874dda2d9dec7245c2051f2528d8c2a797901497f140/regex-2025.7.34-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d72765a4bff8c43711d5b0f5b452991a9947853dfa471972169b3cc0ba1d0751", size = 910103, upload-time = "2025-07-31T00:20:31.313Z" }, - { url = "https://files.pythonhosted.org/packages/9e/fe/14176f2182125977fba3711adea73f472a11f3f9288c1317c59cd16ad5e6/regex-2025.7.34-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4494f8fd95a77eb434039ad8460e64d57baa0434f1395b7da44015bef650d0e4", size = 801709, upload-time = "2025-07-31T00:20:33.323Z" }, - { url = "https://files.pythonhosted.org/packages/5a/0d/80d4e66ed24f1ba876a9e8e31b709f9fd22d5c266bf5f3ab3c1afe683d7d/regex-2025.7.34-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4f42b522259c66e918a0121a12429b2abcf696c6f967fa37bdc7b72e61469f98", size = 786726, upload-time = "2025-07-31T00:20:35.252Z" }, - { url = "https://files.pythonhosted.org/packages/12/75/c3ebb30e04a56c046f5c85179dc173818551037daae2c0c940c7b19152cb/regex-2025.7.34-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:aaef1f056d96a0a5d53ad47d019d5b4c66fe4be2da87016e0d43b7242599ffc7", size = 857306, upload-time = "2025-07-31T00:20:37.12Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b2/a4dc5d8b14f90924f27f0ac4c4c4f5e195b723be98adecc884f6716614b6/regex-2025.7.34-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:656433e5b7dccc9bc0da6312da8eb897b81f5e560321ec413500e5367fcd5d47", size = 848494, upload-time = "2025-07-31T00:20:38.818Z" }, - { url = "https://files.pythonhosted.org/packages/0d/21/9ac6e07a4c5e8646a90b56b61f7e9dac11ae0747c857f91d3d2bc7c241d9/regex-2025.7.34-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e91eb2c62c39705e17b4d42d4b86c4e86c884c0d15d9c5a47d0835f8387add8e", size = 787850, upload-time = "2025-07-31T00:20:40.478Z" }, - { url = "https://files.pythonhosted.org/packages/be/6c/d51204e28e7bc54f9a03bb799b04730d7e54ff2718862b8d4e09e7110a6a/regex-2025.7.34-cp314-cp314-win32.whl", hash = "sha256:f978ddfb6216028c8f1d6b0f7ef779949498b64117fc35a939022f67f810bdcb", size = 269730, upload-time = "2025-07-31T00:20:42.253Z" }, - { url = "https://files.pythonhosted.org/packages/74/52/a7e92d02fa1fdef59d113098cb9f02c5d03289a0e9f9e5d4d6acccd10677/regex-2025.7.34-cp314-cp314-win_amd64.whl", hash = "sha256:4b7dc33b9b48fb37ead12ffc7bdb846ac72f99a80373c4da48f64b373a7abeae", size = 278640, upload-time = "2025-07-31T00:20:44.42Z" }, - { url = "https://files.pythonhosted.org/packages/d1/78/a815529b559b1771080faa90c3ab401730661f99d495ab0071649f139ebd/regex-2025.7.34-cp314-cp314-win_arm64.whl", hash = "sha256:4b8c4d39f451e64809912c82392933d80fe2e4a87eeef8859fcc5380d0173c64", size = 271757, upload-time = "2025-07-31T00:20:46.355Z" }, + { url = "https://files.pythonhosted.org/packages/28/c6/195a6217a43719d5a6a12cc192a22d12c40290cecfa577f00f4fb822f07d/regex-2025.10.23-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b7690f95404a1293923a296981fd943cca12c31a41af9c21ba3edd06398fc193", size = 488956, upload-time = "2025-10-21T15:55:42.887Z" }, + { url = "https://files.pythonhosted.org/packages/4c/93/181070cd1aa2fa541ff2d3afcf763ceecd4937b34c615fa92765020a6c90/regex-2025.10.23-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1a32d77aeaea58a13230100dd8797ac1a84c457f3af2fdf0d81ea689d5a9105b", size = 290997, upload-time = "2025-10-21T15:55:44.53Z" }, + { url = "https://files.pythonhosted.org/packages/b6/c5/9d37fbe3a40ed8dda78c23e1263002497540c0d1522ed75482ef6c2000f0/regex-2025.10.23-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b24b29402f264f70a3c81f45974323b41764ff7159655360543b7cabb73e7d2f", size = 288686, upload-time = "2025-10-21T15:55:46.186Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e7/db610ff9f10c2921f9b6ac0c8d8be4681b28ddd40fc0549429366967e61f/regex-2025.10.23-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:563824a08c7c03d96856d84b46fdb3bbb7cfbdf79da7ef68725cda2ce169c72a", size = 798466, upload-time = "2025-10-21T15:55:48.24Z" }, + { url = "https://files.pythonhosted.org/packages/90/10/aab883e1fa7fe2feb15ac663026e70ca0ae1411efa0c7a4a0342d9545015/regex-2025.10.23-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0ec8bdd88d2e2659c3518087ee34b37e20bd169419ffead4240a7004e8ed03b", size = 863996, upload-time = "2025-10-21T15:55:50.478Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b0/8f686dd97a51f3b37d0238cd00a6d0f9ccabe701f05b56de1918571d0d61/regex-2025.10.23-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b577601bfe1d33913fcd9276d7607bbac827c4798d9e14d04bf37d417a6c41cb", size = 912145, upload-time = "2025-10-21T15:55:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ca/639f8cd5b08797bca38fc5e7e07f76641a428cf8c7fca05894caf045aa32/regex-2025.10.23-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c9f2c68ac6cb3de94eea08a437a75eaa2bd33f9e97c84836ca0b610a5804368", size = 803370, upload-time = "2025-10-21T15:55:53.944Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1e/a40725bb76959eddf8abc42a967bed6f4851b39f5ac4f20e9794d7832aa5/regex-2025.10.23-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:89f8b9ea3830c79468e26b0e21c3585f69f105157c2154a36f6b7839f8afb351", size = 787767, upload-time = "2025-10-21T15:55:56.004Z" }, + { url = "https://files.pythonhosted.org/packages/3d/d8/8ee9858062936b0f99656dce390aa667c6e7fb0c357b1b9bf76fb5e2e708/regex-2025.10.23-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:98fd84c4e4ea185b3bb5bf065261ab45867d8875032f358a435647285c722673", size = 858335, upload-time = "2025-10-21T15:55:58.185Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0a/ed5faaa63fa8e3064ab670e08061fbf09e3a10235b19630cf0cbb9e48c0a/regex-2025.10.23-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1e11d3e5887b8b096f96b4154dfb902f29c723a9556639586cd140e77e28b313", size = 850402, upload-time = "2025-10-21T15:56:00.023Z" }, + { url = "https://files.pythonhosted.org/packages/79/14/d05f617342f4b2b4a23561da500ca2beab062bfcc408d60680e77ecaf04d/regex-2025.10.23-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f13450328a6634348d47a88367e06b64c9d84980ef6a748f717b13f8ce64e87", size = 789739, upload-time = "2025-10-21T15:56:01.967Z" }, + { url = "https://files.pythonhosted.org/packages/f9/7b/e8ce8eef42a15f2c3461f8b3e6e924bbc86e9605cb534a393aadc8d3aff8/regex-2025.10.23-cp313-cp313-win32.whl", hash = "sha256:37be9296598a30c6a20236248cb8b2c07ffd54d095b75d3a2a2ee5babdc51df1", size = 266054, upload-time = "2025-10-21T15:56:05.291Z" }, + { url = "https://files.pythonhosted.org/packages/71/2d/55184ed6be6473187868d2f2e6a0708195fc58270e62a22cbf26028f2570/regex-2025.10.23-cp313-cp313-win_amd64.whl", hash = "sha256:ea7a3c283ce0f06fe789365841e9174ba05f8db16e2fd6ae00a02df9572c04c0", size = 276917, upload-time = "2025-10-21T15:56:07.303Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d4/927eced0e2bd45c45839e556f987f8c8f8683268dd3c00ad327deb3b0172/regex-2025.10.23-cp313-cp313-win_arm64.whl", hash = "sha256:d9a4953575f300a7bab71afa4cd4ac061c7697c89590a2902b536783eeb49a4f", size = 270105, upload-time = "2025-10-21T15:56:09.857Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b3/95b310605285573341fc062d1d30b19a54f857530e86c805f942c4ff7941/regex-2025.10.23-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:7d6606524fa77b3912c9ef52a42ef63c6cfbfc1077e9dc6296cd5da0da286044", size = 491850, upload-time = "2025-10-21T15:56:11.685Z" }, + { url = "https://files.pythonhosted.org/packages/a4/8f/207c2cec01e34e56db1eff606eef46644a60cf1739ecd474627db90ad90b/regex-2025.10.23-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c037aadf4d64bdc38af7db3dbd34877a057ce6524eefcb2914d6d41c56f968cc", size = 292537, upload-time = "2025-10-21T15:56:13.963Z" }, + { url = "https://files.pythonhosted.org/packages/98/3b/025240af4ada1dc0b5f10d73f3e5122d04ce7f8908ab8881e5d82b9d61b6/regex-2025.10.23-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:99018c331fb2529084a0c9b4c713dfa49fafb47c7712422e49467c13a636c656", size = 290904, upload-time = "2025-10-21T15:56:16.016Z" }, + { url = "https://files.pythonhosted.org/packages/81/8e/104ac14e2d3450c43db18ec03e1b96b445a94ae510b60138f00ce2cb7ca1/regex-2025.10.23-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd8aba965604d70306eb90a35528f776e59112a7114a5162824d43b76fa27f58", size = 807311, upload-time = "2025-10-21T15:56:17.818Z" }, + { url = "https://files.pythonhosted.org/packages/19/63/78aef90141b7ce0be8a18e1782f764f6997ad09de0e05251f0d2503a914a/regex-2025.10.23-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:238e67264b4013e74136c49f883734f68656adf8257bfa13b515626b31b20f8e", size = 873241, upload-time = "2025-10-21T15:56:19.941Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a8/80eb1201bb49ae4dba68a1b284b4211ed9daa8e74dc600018a10a90399fb/regex-2025.10.23-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b2eb48bd9848d66fd04826382f5e8491ae633de3233a3d64d58ceb4ecfa2113a", size = 914794, upload-time = "2025-10-21T15:56:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d5/1984b6ee93281f360a119a5ca1af6a8ca7d8417861671388bf750becc29b/regex-2025.10.23-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d36591ce06d047d0c0fe2fc5f14bfbd5b4525d08a7b6a279379085e13f0e3d0e", size = 812581, upload-time = "2025-10-21T15:56:24.319Z" }, + { url = "https://files.pythonhosted.org/packages/c4/39/11ebdc6d9927172a64ae237d16763145db6bd45ebb4055c17b88edab72a7/regex-2025.10.23-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b5d4ece8628d6e364302006366cea3ee887db397faebacc5dacf8ef19e064cf8", size = 795346, upload-time = "2025-10-21T15:56:26.232Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b4/89a591bcc08b5e436af43315284bd233ba77daf0cf20e098d7af12f006c1/regex-2025.10.23-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:39a7e8083959cb1c4ff74e483eecb5a65d3b3e1d821b256e54baf61782c906c6", size = 868214, upload-time = "2025-10-21T15:56:28.597Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/58ba98409c1dbc8316cdb20dafbc63ed267380a07780cafecaf5012dabc9/regex-2025.10.23-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:842d449a8fefe546f311656cf8c0d6729b08c09a185f1cad94c756210286d6a8", size = 854540, upload-time = "2025-10-21T15:56:30.875Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f2/4a9e9338d67626e2071b643f828a482712ad15889d7268e11e9a63d6f7e9/regex-2025.10.23-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d614986dc68506be8f00474f4f6960e03e4ca9883f7df47744800e7d7c08a494", size = 799346, upload-time = "2025-10-21T15:56:32.725Z" }, + { url = "https://files.pythonhosted.org/packages/63/be/543d35c46bebf6f7bf2be538cca74d6585f25714700c36f37f01b92df551/regex-2025.10.23-cp313-cp313t-win32.whl", hash = "sha256:a5b7a26b51a9df473ec16a1934d117443a775ceb7b39b78670b2e21893c330c9", size = 268657, upload-time = "2025-10-21T15:56:34.577Z" }, + { url = "https://files.pythonhosted.org/packages/14/9f/4dd6b7b612037158bb2c9bcaa710e6fb3c40ad54af441b9c53b3a137a9f1/regex-2025.10.23-cp313-cp313t-win_amd64.whl", hash = "sha256:ce81c5544a5453f61cb6f548ed358cfb111e3b23f3cd42d250a4077a6be2a7b6", size = 280075, upload-time = "2025-10-21T15:56:36.767Z" }, + { url = "https://files.pythonhosted.org/packages/81/7a/5bd0672aa65d38c8da6747c17c8b441bdb53d816c569e3261013af8e83cf/regex-2025.10.23-cp313-cp313t-win_arm64.whl", hash = "sha256:e9bf7f6699f490e4e43c44757aa179dab24d1960999c84ab5c3d5377714ed473", size = 271219, upload-time = "2025-10-21T15:56:39.033Z" }, + { 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" }, ] [[package]] @@ -1098,68 +1130,68 @@ wheels = [ [[package]] name = "rpds-py" -version = "0.27.1" +version = "0.28.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479, upload-time = "2025-08-27T12:16:36.024Z" } +sdist = { url = "https://files.pythonhosted.org/packages/48/dc/95f074d43452b3ef5d06276696ece4b3b5d696e7c9ad7173c54b1390cd70/rpds_py-0.28.0.tar.gz", hash = "sha256:abd4df20485a0983e2ca334a216249b6186d6e3c1627e106651943dbdb791aea", size = 27419, upload-time = "2025-10-22T22:24:29.327Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/77/610aeee8d41e39080c7e14afa5387138e3c9fa9756ab893d09d99e7d8e98/rpds_py-0.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b", size = 361741, upload-time = "2025-08-27T12:13:31.039Z" }, - { url = "https://files.pythonhosted.org/packages/3a/fc/c43765f201c6a1c60be2043cbdb664013def52460a4c7adace89d6682bf4/rpds_py-0.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf", size = 345574, upload-time = "2025-08-27T12:13:32.902Z" }, - { url = "https://files.pythonhosted.org/packages/20/42/ee2b2ca114294cd9847d0ef9c26d2b0851b2e7e00bf14cc4c0b581df0fc3/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83", size = 385051, upload-time = "2025-08-27T12:13:34.228Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e8/1e430fe311e4799e02e2d1af7c765f024e95e17d651612425b226705f910/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf", size = 398395, upload-time = "2025-08-27T12:13:36.132Z" }, - { url = "https://files.pythonhosted.org/packages/82/95/9dc227d441ff2670651c27a739acb2535ccaf8b351a88d78c088965e5996/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2", size = 524334, upload-time = "2025-08-27T12:13:37.562Z" }, - { url = "https://files.pythonhosted.org/packages/87/01/a670c232f401d9ad461d9a332aa4080cd3cb1d1df18213dbd0d2a6a7ab51/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0", size = 407691, upload-time = "2025-08-27T12:13:38.94Z" }, - { url = "https://files.pythonhosted.org/packages/03/36/0a14aebbaa26fe7fab4780c76f2239e76cc95a0090bdb25e31d95c492fcd/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418", size = 386868, upload-time = "2025-08-27T12:13:40.192Z" }, - { url = "https://files.pythonhosted.org/packages/3b/03/8c897fb8b5347ff6c1cc31239b9611c5bf79d78c984430887a353e1409a1/rpds_py-0.27.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d", size = 405469, upload-time = "2025-08-27T12:13:41.496Z" }, - { url = "https://files.pythonhosted.org/packages/da/07/88c60edc2df74850d496d78a1fdcdc7b54360a7f610a4d50008309d41b94/rpds_py-0.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274", size = 422125, upload-time = "2025-08-27T12:13:42.802Z" }, - { url = "https://files.pythonhosted.org/packages/6b/86/5f4c707603e41b05f191a749984f390dabcbc467cf833769b47bf14ba04f/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd", size = 562341, upload-time = "2025-08-27T12:13:44.472Z" }, - { url = "https://files.pythonhosted.org/packages/b2/92/3c0cb2492094e3cd9baf9e49bbb7befeceb584ea0c1a8b5939dca4da12e5/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2", size = 592511, upload-time = "2025-08-27T12:13:45.898Z" }, - { url = "https://files.pythonhosted.org/packages/10/bb/82e64fbb0047c46a168faa28d0d45a7851cd0582f850b966811d30f67ad8/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002", size = 557736, upload-time = "2025-08-27T12:13:47.408Z" }, - { url = "https://files.pythonhosted.org/packages/00/95/3c863973d409210da7fb41958172c6b7dbe7fc34e04d3cc1f10bb85e979f/rpds_py-0.27.1-cp313-cp313-win32.whl", hash = "sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3", size = 221462, upload-time = "2025-08-27T12:13:48.742Z" }, - { url = "https://files.pythonhosted.org/packages/ce/2c/5867b14a81dc217b56d95a9f2a40fdbc56a1ab0181b80132beeecbd4b2d6/rpds_py-0.27.1-cp313-cp313-win_amd64.whl", hash = "sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83", size = 232034, upload-time = "2025-08-27T12:13:50.11Z" }, - { url = "https://files.pythonhosted.org/packages/c7/78/3958f3f018c01923823f1e47f1cc338e398814b92d83cd278364446fac66/rpds_py-0.27.1-cp313-cp313-win_arm64.whl", hash = "sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d", size = 222392, upload-time = "2025-08-27T12:13:52.587Z" }, - { url = "https://files.pythonhosted.org/packages/01/76/1cdf1f91aed5c3a7bf2eba1f1c4e4d6f57832d73003919a20118870ea659/rpds_py-0.27.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228", size = 358355, upload-time = "2025-08-27T12:13:54.012Z" }, - { url = "https://files.pythonhosted.org/packages/c3/6f/bf142541229374287604caf3bb2a4ae17f0a580798fd72d3b009b532db4e/rpds_py-0.27.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92", size = 342138, upload-time = "2025-08-27T12:13:55.791Z" }, - { url = "https://files.pythonhosted.org/packages/1a/77/355b1c041d6be40886c44ff5e798b4e2769e497b790f0f7fd1e78d17e9a8/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2", size = 380247, upload-time = "2025-08-27T12:13:57.683Z" }, - { url = "https://files.pythonhosted.org/packages/d6/a4/d9cef5c3946ea271ce2243c51481971cd6e34f21925af2783dd17b26e815/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723", size = 390699, upload-time = "2025-08-27T12:13:59.137Z" }, - { url = "https://files.pythonhosted.org/packages/3a/06/005106a7b8c6c1a7e91b73169e49870f4af5256119d34a361ae5240a0c1d/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802", size = 521852, upload-time = "2025-08-27T12:14:00.583Z" }, - { url = "https://files.pythonhosted.org/packages/e5/3e/50fb1dac0948e17a02eb05c24510a8fe12d5ce8561c6b7b7d1339ab7ab9c/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f", size = 402582, upload-time = "2025-08-27T12:14:02.034Z" }, - { url = "https://files.pythonhosted.org/packages/cb/b0/f4e224090dc5b0ec15f31a02d746ab24101dd430847c4d99123798661bfc/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2", size = 384126, upload-time = "2025-08-27T12:14:03.437Z" }, - { url = "https://files.pythonhosted.org/packages/54/77/ac339d5f82b6afff1df8f0fe0d2145cc827992cb5f8eeb90fc9f31ef7a63/rpds_py-0.27.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21", size = 399486, upload-time = "2025-08-27T12:14:05.443Z" }, - { url = "https://files.pythonhosted.org/packages/d6/29/3e1c255eee6ac358c056a57d6d6869baa00a62fa32eea5ee0632039c50a3/rpds_py-0.27.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef", size = 414832, upload-time = "2025-08-27T12:14:06.902Z" }, - { url = "https://files.pythonhosted.org/packages/3f/db/6d498b844342deb3fa1d030598db93937a9964fcf5cb4da4feb5f17be34b/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081", size = 557249, upload-time = "2025-08-27T12:14:08.37Z" }, - { url = "https://files.pythonhosted.org/packages/60/f3/690dd38e2310b6f68858a331399b4d6dbb9132c3e8ef8b4333b96caf403d/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd", size = 587356, upload-time = "2025-08-27T12:14:10.034Z" }, - { url = "https://files.pythonhosted.org/packages/86/e3/84507781cccd0145f35b1dc32c72675200c5ce8d5b30f813e49424ef68fc/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7", size = 555300, upload-time = "2025-08-27T12:14:11.783Z" }, - { url = "https://files.pythonhosted.org/packages/e5/ee/375469849e6b429b3516206b4580a79e9ef3eb12920ddbd4492b56eaacbe/rpds_py-0.27.1-cp313-cp313t-win32.whl", hash = "sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688", size = 216714, upload-time = "2025-08-27T12:14:13.629Z" }, - { url = "https://files.pythonhosted.org/packages/21/87/3fc94e47c9bd0742660e84706c311a860dcae4374cf4a03c477e23ce605a/rpds_py-0.27.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797", size = 228943, upload-time = "2025-08-27T12:14:14.937Z" }, - { url = "https://files.pythonhosted.org/packages/70/36/b6e6066520a07cf029d385de869729a895917b411e777ab1cde878100a1d/rpds_py-0.27.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334", size = 362472, upload-time = "2025-08-27T12:14:16.333Z" }, - { url = "https://files.pythonhosted.org/packages/af/07/b4646032e0dcec0df9c73a3bd52f63bc6c5f9cda992f06bd0e73fe3fbebd/rpds_py-0.27.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33", size = 345676, upload-time = "2025-08-27T12:14:17.764Z" }, - { url = "https://files.pythonhosted.org/packages/b0/16/2f1003ee5d0af4bcb13c0cf894957984c32a6751ed7206db2aee7379a55e/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a", size = 385313, upload-time = "2025-08-27T12:14:19.829Z" }, - { url = "https://files.pythonhosted.org/packages/05/cd/7eb6dd7b232e7f2654d03fa07f1414d7dfc980e82ba71e40a7c46fd95484/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b", size = 399080, upload-time = "2025-08-27T12:14:21.531Z" }, - { url = "https://files.pythonhosted.org/packages/20/51/5829afd5000ec1cb60f304711f02572d619040aa3ec033d8226817d1e571/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7", size = 523868, upload-time = "2025-08-27T12:14:23.485Z" }, - { url = "https://files.pythonhosted.org/packages/05/2c/30eebca20d5db95720ab4d2faec1b5e4c1025c473f703738c371241476a2/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136", size = 408750, upload-time = "2025-08-27T12:14:24.924Z" }, - { url = "https://files.pythonhosted.org/packages/90/1a/cdb5083f043597c4d4276eae4e4c70c55ab5accec078da8611f24575a367/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff", size = 387688, upload-time = "2025-08-27T12:14:27.537Z" }, - { url = "https://files.pythonhosted.org/packages/7c/92/cf786a15320e173f945d205ab31585cc43969743bb1a48b6888f7a2b0a2d/rpds_py-0.27.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9", size = 407225, upload-time = "2025-08-27T12:14:28.981Z" }, - { url = "https://files.pythonhosted.org/packages/33/5c/85ee16df5b65063ef26017bef33096557a4c83fbe56218ac7cd8c235f16d/rpds_py-0.27.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60", size = 423361, upload-time = "2025-08-27T12:14:30.469Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8e/1c2741307fcabd1a334ecf008e92c4f47bb6f848712cf15c923becfe82bb/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e", size = 562493, upload-time = "2025-08-27T12:14:31.987Z" }, - { url = "https://files.pythonhosted.org/packages/04/03/5159321baae9b2222442a70c1f988cbbd66b9be0675dd3936461269be360/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212", size = 592623, upload-time = "2025-08-27T12:14:33.543Z" }, - { url = "https://files.pythonhosted.org/packages/ff/39/c09fd1ad28b85bc1d4554a8710233c9f4cefd03d7717a1b8fbfd171d1167/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675", size = 558800, upload-time = "2025-08-27T12:14:35.436Z" }, - { url = "https://files.pythonhosted.org/packages/c5/d6/99228e6bbcf4baa764b18258f519a9035131d91b538d4e0e294313462a98/rpds_py-0.27.1-cp314-cp314-win32.whl", hash = "sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3", size = 221943, upload-time = "2025-08-27T12:14:36.898Z" }, - { url = "https://files.pythonhosted.org/packages/be/07/c802bc6b8e95be83b79bdf23d1aa61d68324cb1006e245d6c58e959e314d/rpds_py-0.27.1-cp314-cp314-win_amd64.whl", hash = "sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456", size = 233739, upload-time = "2025-08-27T12:14:38.386Z" }, - { url = "https://files.pythonhosted.org/packages/c8/89/3e1b1c16d4c2d547c5717377a8df99aee8099ff050f87c45cb4d5fa70891/rpds_py-0.27.1-cp314-cp314-win_arm64.whl", hash = "sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3", size = 223120, upload-time = "2025-08-27T12:14:39.82Z" }, - { url = "https://files.pythonhosted.org/packages/62/7e/dc7931dc2fa4a6e46b2a4fa744a9fe5c548efd70e0ba74f40b39fa4a8c10/rpds_py-0.27.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2", size = 358944, upload-time = "2025-08-27T12:14:41.199Z" }, - { url = "https://files.pythonhosted.org/packages/e6/22/4af76ac4e9f336bfb1a5f240d18a33c6b2fcaadb7472ac7680576512b49a/rpds_py-0.27.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4", size = 342283, upload-time = "2025-08-27T12:14:42.699Z" }, - { url = "https://files.pythonhosted.org/packages/1c/15/2a7c619b3c2272ea9feb9ade67a45c40b3eeb500d503ad4c28c395dc51b4/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e", size = 380320, upload-time = "2025-08-27T12:14:44.157Z" }, - { url = "https://files.pythonhosted.org/packages/a2/7d/4c6d243ba4a3057e994bb5bedd01b5c963c12fe38dde707a52acdb3849e7/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817", size = 391760, upload-time = "2025-08-27T12:14:45.845Z" }, - { url = "https://files.pythonhosted.org/packages/b4/71/b19401a909b83bcd67f90221330bc1ef11bc486fe4e04c24388d28a618ae/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec", size = 522476, upload-time = "2025-08-27T12:14:47.364Z" }, - { url = "https://files.pythonhosted.org/packages/e4/44/1a3b9715c0455d2e2f0f6df5ee6d6f5afdc423d0773a8a682ed2b43c566c/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a", size = 403418, upload-time = "2025-08-27T12:14:49.991Z" }, - { url = "https://files.pythonhosted.org/packages/1c/4b/fb6c4f14984eb56673bc868a66536f53417ddb13ed44b391998100a06a96/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8", size = 384771, upload-time = "2025-08-27T12:14:52.159Z" }, - { url = "https://files.pythonhosted.org/packages/c0/56/d5265d2d28b7420d7b4d4d85cad8ef891760f5135102e60d5c970b976e41/rpds_py-0.27.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48", size = 400022, upload-time = "2025-08-27T12:14:53.859Z" }, - { url = "https://files.pythonhosted.org/packages/8f/e9/9f5fc70164a569bdd6ed9046486c3568d6926e3a49bdefeeccfb18655875/rpds_py-0.27.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb", size = 416787, upload-time = "2025-08-27T12:14:55.673Z" }, - { url = "https://files.pythonhosted.org/packages/d4/64/56dd03430ba491db943a81dcdef115a985aac5f44f565cd39a00c766d45c/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734", size = 557538, upload-time = "2025-08-27T12:14:57.245Z" }, - { url = "https://files.pythonhosted.org/packages/3f/36/92cc885a3129993b1d963a2a42ecf64e6a8e129d2c7cc980dbeba84e55fb/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb", size = 588512, upload-time = "2025-08-27T12:14:58.728Z" }, - { url = "https://files.pythonhosted.org/packages/dd/10/6b283707780a81919f71625351182b4f98932ac89a09023cb61865136244/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0", size = 555813, upload-time = "2025-08-27T12:15:00.334Z" }, - { url = "https://files.pythonhosted.org/packages/04/2e/30b5ea18c01379da6272a92825dd7e53dc9d15c88a19e97932d35d430ef7/rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a", size = 217385, upload-time = "2025-08-27T12:15:01.937Z" }, - { url = "https://files.pythonhosted.org/packages/32/7d/97119da51cb1dd3f2f3c0805f155a3aa4a95fa44fe7d78ae15e69edf4f34/rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772", size = 230097, upload-time = "2025-08-27T12:15:03.961Z" }, + { url = "https://files.pythonhosted.org/packages/d3/03/ce566d92611dfac0085c2f4b048cd53ed7c274a5c05974b882a908d540a2/rpds_py-0.28.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e9e184408a0297086f880556b6168fa927d677716f83d3472ea333b42171ee3b", size = 366235, upload-time = "2025-10-22T22:22:28.397Z" }, + { url = "https://files.pythonhosted.org/packages/00/34/1c61da1b25592b86fd285bd7bd8422f4c9d748a7373b46126f9ae792a004/rpds_py-0.28.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:edd267266a9b0448f33dc465a97cfc5d467594b600fe28e7fa2f36450e03053a", size = 348241, upload-time = "2025-10-22T22:22:30.171Z" }, + { url = "https://files.pythonhosted.org/packages/fc/00/ed1e28616848c61c493a067779633ebf4b569eccaacf9ccbdc0e7cba2b9d/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85beb8b3f45e4e32f6802fb6cd6b17f615ef6c6a52f265371fb916fae02814aa", size = 378079, upload-time = "2025-10-22T22:22:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/11/b2/ccb30333a16a470091b6e50289adb4d3ec656fd9951ba8c5e3aaa0746a67/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d2412be8d00a1b895f8ad827cc2116455196e20ed994bb704bf138fe91a42724", size = 393151, upload-time = "2025-10-22T22:22:33.453Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d0/73e2217c3ee486d555cb84920597480627d8c0240ff3062005c6cc47773e/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf128350d384b777da0e68796afdcebc2e9f63f0e9f242217754e647f6d32491", size = 517520, upload-time = "2025-10-22T22:22:34.949Z" }, + { url = "https://files.pythonhosted.org/packages/c4/91/23efe81c700427d0841a4ae7ea23e305654381831e6029499fe80be8a071/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2036d09b363aa36695d1cc1a97b36865597f4478470b0697b5ee9403f4fe399", size = 408699, upload-time = "2025-10-22T22:22:36.584Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ee/a324d3198da151820a326c1f988caaa4f37fc27955148a76fff7a2d787a9/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8e1e9be4fa6305a16be628959188e4fd5cd6f1b0e724d63c6d8b2a8adf74ea6", size = 385720, upload-time = "2025-10-22T22:22:38.014Z" }, + { url = "https://files.pythonhosted.org/packages/19/ad/e68120dc05af8b7cab4a789fccd8cdcf0fe7e6581461038cc5c164cd97d2/rpds_py-0.28.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0a403460c9dd91a7f23fc3188de6d8977f1d9603a351d5db6cf20aaea95b538d", size = 401096, upload-time = "2025-10-22T22:22:39.869Z" }, + { url = "https://files.pythonhosted.org/packages/99/90/c1e070620042459d60df6356b666bb1f62198a89d68881816a7ed121595a/rpds_py-0.28.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d7366b6553cdc805abcc512b849a519167db8f5e5c3472010cd1228b224265cb", size = 411465, upload-time = "2025-10-22T22:22:41.395Z" }, + { url = "https://files.pythonhosted.org/packages/68/61/7c195b30d57f1b8d5970f600efee72a4fad79ec829057972e13a0370fd24/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b43c6a3726efd50f18d8120ec0551241c38785b68952d240c45ea553912ac41", size = 558832, upload-time = "2025-10-22T22:22:42.871Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3d/06f3a718864773f69941d4deccdf18e5e47dd298b4628062f004c10f3b34/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0cb7203c7bc69d7c1585ebb33a2e6074492d2fc21ad28a7b9d40457ac2a51ab7", size = 583230, upload-time = "2025-10-22T22:22:44.877Z" }, + { url = "https://files.pythonhosted.org/packages/66/df/62fc783781a121e77fee9a21ead0a926f1b652280a33f5956a5e7833ed30/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a52a5169c664dfb495882adc75c304ae1d50df552fbd68e100fdc719dee4ff9", size = 553268, upload-time = "2025-10-22T22:22:46.441Z" }, + { url = "https://files.pythonhosted.org/packages/84/85/d34366e335140a4837902d3dea89b51f087bd6a63c993ebdff59e93ee61d/rpds_py-0.28.0-cp313-cp313-win32.whl", hash = "sha256:2e42456917b6687215b3e606ab46aa6bca040c77af7df9a08a6dcfe8a4d10ca5", size = 217100, upload-time = "2025-10-22T22:22:48.342Z" }, + { url = "https://files.pythonhosted.org/packages/3c/1c/f25a3f3752ad7601476e3eff395fe075e0f7813fbb9862bd67c82440e880/rpds_py-0.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:e0a0311caedc8069d68fc2bf4c9019b58a2d5ce3cd7cb656c845f1615b577e1e", size = 227759, upload-time = "2025-10-22T22:22:50.219Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d6/5f39b42b99615b5bc2f36ab90423ea404830bdfee1c706820943e9a645eb/rpds_py-0.28.0-cp313-cp313-win_arm64.whl", hash = "sha256:04c1b207ab8b581108801528d59ad80aa83bb170b35b0ddffb29c20e411acdc1", size = 217326, upload-time = "2025-10-22T22:22:51.647Z" }, + { url = "https://files.pythonhosted.org/packages/5c/8b/0c69b72d1cee20a63db534be0df271effe715ef6c744fdf1ff23bb2b0b1c/rpds_py-0.28.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f296ea3054e11fc58ad42e850e8b75c62d9a93a9f981ad04b2e5ae7d2186ff9c", size = 355736, upload-time = "2025-10-22T22:22:53.211Z" }, + { url = "https://files.pythonhosted.org/packages/f7/6d/0c2ee773cfb55c31a8514d2cece856dd299170a49babd50dcffb15ddc749/rpds_py-0.28.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5a7306c19b19005ad98468fcefeb7100b19c79fc23a5f24a12e06d91181193fa", size = 342677, upload-time = "2025-10-22T22:22:54.723Z" }, + { url = "https://files.pythonhosted.org/packages/e2/1c/22513ab25a27ea205144414724743e305e8153e6abe81833b5e678650f5a/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5d9b86aa501fed9862a443c5c3116f6ead8bc9296185f369277c42542bd646b", size = 371847, upload-time = "2025-10-22T22:22:56.295Z" }, + { url = "https://files.pythonhosted.org/packages/60/07/68e6ccdb4b05115ffe61d31afc94adef1833d3a72f76c9632d4d90d67954/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e5bbc701eff140ba0e872691d573b3d5d30059ea26e5785acba9132d10c8c31d", size = 381800, upload-time = "2025-10-22T22:22:57.808Z" }, + { url = "https://files.pythonhosted.org/packages/73/bf/6d6d15df80781d7f9f368e7c1a00caf764436518c4877fb28b029c4624af/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a5690671cd672a45aa8616d7374fdf334a1b9c04a0cac3c854b1136e92374fe", size = 518827, upload-time = "2025-10-22T22:22:59.826Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d3/2decbb2976cc452cbf12a2b0aaac5f1b9dc5dd9d1f7e2509a3ee00421249/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9f1d92ecea4fa12f978a367c32a5375a1982834649cdb96539dcdc12e609ab1a", size = 399471, upload-time = "2025-10-22T22:23:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2c/f30892f9e54bd02e5faca3f6a26d6933c51055e67d54818af90abed9748e/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d252db6b1a78d0a3928b6190156042d54c93660ce4d98290d7b16b5296fb7cc", size = 377578, upload-time = "2025-10-22T22:23:03.52Z" }, + { url = "https://files.pythonhosted.org/packages/f0/5d/3bce97e5534157318f29ac06bf2d279dae2674ec12f7cb9c12739cee64d8/rpds_py-0.28.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d61b355c3275acb825f8777d6c4505f42b5007e357af500939d4a35b19177259", size = 390482, upload-time = "2025-10-22T22:23:05.391Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f0/886bd515ed457b5bd93b166175edb80a0b21a210c10e993392127f1e3931/rpds_py-0.28.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:acbe5e8b1026c0c580d0321c8aae4b0a1e1676861d48d6e8c6586625055b606a", size = 402447, upload-time = "2025-10-22T22:23:06.93Z" }, + { url = "https://files.pythonhosted.org/packages/42/b5/71e8777ac55e6af1f4f1c05b47542a1eaa6c33c1cf0d300dca6a1c6e159a/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8aa23b6f0fc59b85b4c7d89ba2965af274346f738e8d9fc2455763602e62fd5f", size = 552385, upload-time = "2025-10-22T22:23:08.557Z" }, + { url = "https://files.pythonhosted.org/packages/5d/cb/6ca2d70cbda5a8e36605e7788c4aa3bea7c17d71d213465a5a675079b98d/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7b14b0c680286958817c22d76fcbca4800ddacef6f678f3a7c79a1fe7067fe37", size = 575642, upload-time = "2025-10-22T22:23:10.348Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d4/407ad9960ca7856d7b25c96dcbe019270b5ffdd83a561787bc682c797086/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bcf1d210dfee61a6c86551d67ee1031899c0fdbae88b2d44a569995d43797712", size = 544507, upload-time = "2025-10-22T22:23:12.434Z" }, + { url = "https://files.pythonhosted.org/packages/51/31/2f46fe0efcac23fbf5797c6b6b7e1c76f7d60773e525cb65fcbc582ee0f2/rpds_py-0.28.0-cp313-cp313t-win32.whl", hash = "sha256:3aa4dc0fdab4a7029ac63959a3ccf4ed605fee048ba67ce89ca3168da34a1342", size = 205376, upload-time = "2025-10-22T22:23:13.979Z" }, + { url = "https://files.pythonhosted.org/packages/92/e4/15947bda33cbedfc134490a41841ab8870a72a867a03d4969d886f6594a2/rpds_py-0.28.0-cp313-cp313t-win_amd64.whl", hash = "sha256:7b7d9d83c942855e4fdcfa75d4f96f6b9e272d42fffcb72cd4bb2577db2e2907", size = 215907, upload-time = "2025-10-22T22:23:15.5Z" }, + { 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" }, ] [[package]] @@ -1185,14 +1217,14 @@ wheels = [ [[package]] name = "s3transfer" -version = "0.13.1" +version = "0.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6d/05/d52bf1e65044b4e5e27d4e63e8d1579dbdec54fce685908ae09bc3720030/s3transfer-0.13.1.tar.gz", hash = "sha256:c3fdba22ba1bd367922f27ec8032d6a1cf5f10c934fb5d68cf60fd5a23d936cf", size = 150589, upload-time = "2025-07-18T19:22:42.31Z" } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/4f/d073e09df851cfa251ef7840007d04db3293a0482ce607d2b993926089be/s3transfer-0.13.1-py3-none-any.whl", hash = "sha256:a981aa7429be23fe6dfc13e80e4020057cbab622b08c0315288758d67cabc724", size = 85308, upload-time = "2025-07-18T19:22:40.947Z" }, + { 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" }, ] [[package]] @@ -1362,9 +1394,9 @@ wheels = [ [[package]] name = "websocket-client" -version = "1.8.0" +version = "1.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648, upload-time = "2024-04-23T22:16:16.976Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826, upload-time = "2024-04-23T22:16:14.422Z" }, + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, ] From 6a677f265368be8a74c8da53c68d8537bc3c86c0 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 27 Oct 2025 11:13:48 +0100 Subject: [PATCH 097/185] restyle service information cards --- .../service_offering_detail.html | 126 +++++++++++------- .../includes/control_plane_user_info.html | 43 +++--- 2 files changed, 101 insertions(+), 68 deletions(-) diff --git a/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html b/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html index 6fdc61a..3305328 100644 --- a/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html +++ b/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html @@ -9,9 +9,8 @@ {% endblock html_title %} {% partialdef control-plane-info inline=True %} {% if selected_plane and selected_plane.user_info %} -
-
-
{% translate "Service Provider Zone Information" %}
+
+
{% include "includes/control_plane_user_info.html" with control_plane=selected_plane %}
@@ -35,64 +34,97 @@ {% endpartialdef %} {% block content %}
-
-
-
-
- {% if service.logo %} - {{ service.name }} - {% endif %} -
-

{{ offering }}

- {{ offering.service.category }} + {% if not has_control_planes %} + +
+
+ -
- {% if offering.description %} -
-
-

{{ offering.description|urlize }}

-
-
- {% endif %} - {% if not has_control_planes %} -

{% translate "We currently cannot offer this service, sorry!" %}

- {% else %} +
+
+ {% else %} + +
+ +
+
+
+
{% translate "Service Provider Zone" %}
+
+
+ hx-swap="outerHTML" + class="control-plane-select-form"> {{ select_form }} - {% endif %} - {% if has_control_planes %} +
{% partial control-plane-info %}
- {% endif %} - {% if service.external_links or offering.external_links %} -
-
-
{% translate "External Links" %}
-
- {% for link in service.external_links %} - {% include "includes/external_link.html" %} - {% endfor %} - {% for link in offering.external_links %} - {% include "includes/external_link.html" %} - {% endfor %} -
-
-
- {% endif %} +
+ +
+
+
+
{% translate "Service Information" %}
+
+
+ {% if offering.service.logo or offering.description %} +
+ {% if offering.service.logo %} +
+ {{ offering.service.name }} +
+ {% endif %} + {% if offering.description %} +
+

{{ offering.description|urlize }}

+
+ {% endif %} +
+ {% endif %} + {% if offering.service.external_links or offering.external_links %} + {% if offering.service.logo or offering.description %}
{% endif %} +
{% translate "External Links" %}
+
+ {% for link in offering.service.external_links %} + {% include "includes/external_link.html" %} + {% endfor %} + {% for link in offering.external_links %} + {% include "includes/external_link.html" %} + {% endfor %} +
+ {% else %} + {% if not offering.service.logo and not offering.description %} +

{% translate "No additional information available." %}

+ {% endif %} + {% endif %} +
+
+
+
+ {% endif %} + +
+
{% partial service-form %}
diff --git a/src/servala/frontend/templates/includes/control_plane_user_info.html b/src/servala/frontend/templates/includes/control_plane_user_info.html index 21d5cdb..a3a27f5 100644 --- a/src/servala/frontend/templates/includes/control_plane_user_info.html +++ b/src/servala/frontend/templates/includes/control_plane_user_info.html @@ -1,25 +1,26 @@ {% load i18n %} {% if control_plane.user_info %} -
-
{{ key }}{{ value }} + {{ info.title }} + {% if info.help_text %} + + {% endif %} + {{ info.content }}
- - {% for info in control_plane.user_info %} - - - - - {% endfor %} - -
- {{ info.title }} - {% if info.help_text %} - - {% endif %} - {{ info.content }}
+
+ {% for info in control_plane.user_info %} +
+
+ + {{ info.title }} + + {% if info.help_text %} + + {% endif %} +
+
+ {{ info.content }} +
+
+ {% endfor %}
{% endif %} From 4edb59c39c0d22c700e69d4d8766d830114fefbc Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sun, 26 Oct 2025 03:01:29 +0000 Subject: [PATCH 098/185] Update https://github.com/astral-sh/setup-uv action to v7 --- .forgejo/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/tests.yaml b/.forgejo/workflows/tests.yaml index 3d637f3..2447fa8 100644 --- a/.forgejo/workflows/tests.yaml +++ b/.forgejo/workflows/tests.yaml @@ -26,7 +26,7 @@ jobs: node-version: "22" - name: Install uv - uses: https://github.com/astral-sh/setup-uv@v6 + uses: https://github.com/astral-sh/setup-uv@v7 - name: Run tests run: uv run --env-file=.env.example pytest From eea743e5ae9f25a2d9dd7b5189f42f9e9f9942d5 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Fri, 24 Oct 2025 12:40:10 +0200 Subject: [PATCH 099/185] Restrict user input to more sensible ranges ref #223 --- src/servala/core/models/organization.py | 14 ++- src/servala/frontend/forms/organization.py | 116 +++++++++++++++++++-- 2 files changed, 123 insertions(+), 7 deletions(-) diff --git a/src/servala/core/models/organization.py b/src/servala/core/models/organization.py index 646849f..09205dc 100644 --- a/src/servala/core/models/organization.py +++ b/src/servala/core/models/organization.py @@ -6,6 +6,7 @@ from auditlog.registry import auditlog from django.conf import settings from django.contrib.sites.shortcuts import get_current_site from django.core.mail import send_mail +from django.core.validators import RegexValidator from django.db import models, transaction from django.http import HttpRequest from django.utils.functional import cached_property @@ -20,7 +21,18 @@ from servala.core.odoo import CLIENT class Organization(ServalaModelMixin, models.Model): - name = models.CharField(max_length=100, verbose_name=_("Name")) + name = models.CharField( + max_length=32, + verbose_name=_("Name"), + validators=[ + RegexValidator( + regex=r"^[A-Za-z0-9\s]+$", + message=_( + "Organization name can only contain letters, numbers, and spaces." + ), + ) + ], + ) # The namespace is generated as "org-{id}" in accordance with RFC 1035 Label Names. # It is nullable as we need to write to the database in order to read the ID, but should # not be null in practical use. diff --git a/src/servala/frontend/forms/organization.py b/src/servala/frontend/forms/organization.py index 27e6a09..96b06e0 100644 --- a/src/servala/frontend/forms/organization.py +++ b/src/servala/frontend/forms/organization.py @@ -1,5 +1,6 @@ from django import forms from django.core.exceptions import ValidationError +from django.core.validators import RegexValidator from django.forms import ModelForm from django.utils.translation import gettext_lazy as _ @@ -16,9 +17,43 @@ class OrganizationForm(HtmxMixin, ModelForm): class Meta: model = Organization fields = ("name",) + widgets = { + "name": forms.TextInput( + attrs={ + "maxlength": "32", + "pattern": "[A-Za-z0-9\\s]+", + "title": _( + "Organization name can only contain letters, numbers, and spaces" + ), + } + ), + } class OrganizationCreateForm(OrganizationForm): + address_validator = RegexValidator( + regex=r"^[\w\s\.,\-/()\']+$", + message=_( + "This field can only contain letters, numbers, spaces, and basic punctuation (.,-/()')." + ), + ) + city_validator = RegexValidator( + regex=r"^[\w\s\-\']+$", + message=_("City name contains invalid characters."), + ) + postal_code_validator = RegexValidator( + regex=r"^[\w\s\-]+$", + message=_( + "Postal code can only contain letters, numbers, spaces, and hyphens." + ), + ) + phone_validator = RegexValidator( + regex=r"^[0-9\s\+\-()]+$", + message=_( + "Phone number can only contain numbers, spaces, and basic punctuation (+,-,())." + ), + ) + billing_processing_choice = forms.ChoiceField( choices=[ ("existing", _("Use an existing billing address")), @@ -34,17 +69,86 @@ class OrganizationCreateForm(OrganizationForm): ) # Fields for creating a new billing address in Odoo, prefixed with 'invoice_' - invoice_street = forms.CharField(label=_("Line 1"), required=False, max_length=100) - invoice_street2 = forms.CharField(label=_("Line 2"), required=False, max_length=100) - invoice_city = forms.CharField(label=_("City"), required=False, max_length=100) - invoice_zip = forms.CharField(label=_("Postal Code"), required=False, max_length=20) + invoice_street = forms.CharField( + label=_("Line 1"), + required=False, + max_length=128, + validators=[address_validator], + widget=forms.TextInput( + attrs={ + "maxlength": "128", + "title": _( + "Letters, numbers, spaces, and basic punctuation allowed. Emoji not allowed." + ), + } + ), + ) + invoice_street2 = forms.CharField( + label=_("Line 2"), + required=False, + max_length=128, + validators=[address_validator], + widget=forms.TextInput( + attrs={ + "maxlength": "128", + "title": _( + "Letters, numbers, spaces, and basic punctuation allowed. Emoji not allowed." + ), + } + ), + ) + invoice_city = forms.CharField( + label=_("City"), + required=False, + max_length=64, + validators=[city_validator], + widget=forms.TextInput( + attrs={ + "maxlength": "64", + "title": _( + "Letters, spaces, hyphens, and apostrophes allowed. Emoji not allowed." + ), + } + ), + ) + invoice_zip = forms.CharField( + label=_("Postal Code"), + required=False, + max_length=20, + validators=[postal_code_validator], + widget=forms.TextInput( + attrs={ + "maxlength": "20", + "title": _( + "Letters, numbers, spaces, and hyphens allowed. Emoji not allowed." + ), + } + ), + ) invoice_country = forms.ChoiceField( label=_("Country"), required=False, choices=get_odoo_countries(), ) - invoice_email = forms.EmailField(label=_("Billing Email"), required=False) - invoice_phone = forms.CharField(label=_("Phone"), required=False, max_length=30) + invoice_email = forms.EmailField( + label=_("Billing Email"), + required=False, + max_length=254, + widget=forms.EmailInput(attrs={"maxlength": "254"}), + ) + invoice_phone = forms.CharField( + label=_("Phone"), + required=False, + max_length=30, + validators=[phone_validator], + widget=forms.TextInput( + attrs={ + "maxlength": "30", + "pattern": r"[0-9\s\+\-()]+", + "title": _("Only numbers, spaces, and basic punctuation allowed"), + } + ), + ) class Meta(OrganizationForm.Meta): pass From d3e38a0ecb00f17e7b2cd20bdc2e4198cc2b0e4f Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 27 Oct 2025 11:30:59 +0100 Subject: [PATCH 100/185] relaxed organization name validation pattern --- src/servala/frontend/forms/organization.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/servala/frontend/forms/organization.py b/src/servala/frontend/forms/organization.py index 96b06e0..86ba0ab 100644 --- a/src/servala/frontend/forms/organization.py +++ b/src/servala/frontend/forms/organization.py @@ -9,7 +9,17 @@ from servala.core.odoo import get_invoice_addresses, get_odoo_countries from servala.frontend.forms.mixins import HtmxMixin +ORG_NAME_PATTERN = r"[\w\s\-.,&'()+]+" + + class OrganizationForm(HtmxMixin, ModelForm): + name_validator = RegexValidator( + regex=f"^{ORG_NAME_PATTERN}$", + message=_( + "Organization name can only contain letters, numbers, spaces, and common punctuation (-.,&'()+)." + ), + ) + # def __init__(self, *args, **kwargs): # super().__init__(*args, **kwargs) # if self.instance and self.instance.has_inherited_billing_entity: @@ -20,15 +30,20 @@ class OrganizationForm(HtmxMixin, ModelForm): widgets = { "name": forms.TextInput( attrs={ - "maxlength": "32", - "pattern": "[A-Za-z0-9\\s]+", + "maxlength": "100", + "pattern": ORG_NAME_PATTERN, "title": _( - "Organization name can only contain letters, numbers, and spaces" + "Organization name can contain letters, numbers, spaces, and common punctuation (-.,&'()+). Emoji not allowed." ), } ), } + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["name"].validators.append(self.name_validator) + self.fields["name"].max_length = 100 + class OrganizationCreateForm(OrganizationForm): address_validator = RegexValidator( From ddab04cb38e742862cd99e2de0179851edb640bb Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 27 Oct 2025 11:35:53 +0100 Subject: [PATCH 101/185] smaller login button and title update --- src/servala/frontend/templates/account/login.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/servala/frontend/templates/account/login.html b/src/servala/frontend/templates/account/login.html index 58cd49b..f4ca590 100644 --- a/src/servala/frontend/templates/account/login.html +++ b/src/servala/frontend/templates/account/login.html @@ -5,7 +5,7 @@ {% translate "Sign in" %} {% endblock html_title %} {% block page_title %} - {% translate "Welcome to Servala" %} + {% translate "Welcome to Servala - Sovereign App Store" %} {% endblock page_title %} {% block card_header %}
{% for provider in socialaccount_providers %} {% provider_login_url provider process=process scope=scope auth_params=auth_params as href %} -
+ {% csrf_token %} {{ redirect_field }} -
- {% endif %}
- diff --git a/src/servala/static/css/servala.css b/src/servala/static/css/servala.css index cb7061a..067720b 100644 --- a/src/servala/static/css/servala.css +++ b/src/servala/static/css/servala.css @@ -306,33 +306,6 @@ html[data-bs-theme="dark"] .crd-form .nav-tabs .nav-link .mandatory-indicator { margin-left: auto !important } -/* Advanced fields tab flash animation */ -@keyframes tab-pulse { - 0%, 100% { - background-color: transparent; - box-shadow: none; - } - 50% { - background-color: var(--brand-light); - box-shadow: 0 0 10px rgba(154, 99, 236, 0.3); - } -} - -html[data-bs-theme="dark"] @keyframes tab-pulse { - 0%, 100% { - background-color: transparent; - box-shadow: none; - } - 50% { - background-color: rgba(154, 99, 236, 0.2); - box-shadow: 0 0 10px rgba(154, 99, 236, 0.4); - } -} - -.nav-tabs .nav-link.tab-flash { - animation: tab-pulse 1s ease-in-out 2; -} - .beta-banner { background: linear-gradient(135deg, var(--bs-primary) 0%, var(--brand-mid) 100%); color: white; diff --git a/src/servala/static/js/advanced-fields.js b/src/servala/static/js/advanced-fields.js deleted file mode 100644 index 989e61a..0000000 --- a/src/servala/static/js/advanced-fields.js +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Advanced Fields Toggle - * Handles showing/hiding advanced fields in CRD forms - */ -(function() { - 'use strict'; - - function flashTabsWithAdvancedFields() { - const advancedGroups = document.querySelectorAll('.advanced-field-group'); - const tabsToFlash = new Set(); - advancedGroups.forEach(function(group) { - const tabPane = group.closest('.tab-pane'); - if (tabPane) { - const tabId = tabPane.getAttribute('id'); - if (tabId) { - const tabButton = document.querySelector(`[data-bs-target="#${tabId}"]`); - if (tabButton && !tabButton.classList.contains('active')) { - tabsToFlash.add(tabButton); - } - } - } - }); - - tabsToFlash.forEach(function(tab) { - tab.classList.add('tab-flash'); - setTimeout(function() { - tab.classList.remove('tab-flash'); - }, 2000); - }); - } - - function initializeAdvancedFields() { - const advancedInputs = document.querySelectorAll('[data-advanced="true"]'); - - if (advancedInputs.length === 0) { - return; - } - - advancedInputs.forEach(function(input) { - const formGroup = input.closest('.form-group, .mb-3, .col-12, .col-md-6'); - if (formGroup) { - formGroup.classList.add('advanced-field-group', 'collapse'); - } - }); - - const toggleButton = document.getElementById('advanced-toggle'); - if (toggleButton) { - let isExpanded = false; - - document.querySelectorAll('.advanced-field-group').forEach(function(group) { - group.addEventListener('shown.bs.collapse', function() { - toggleButton.innerHTML = ' Hide Advanced Options'; - if (!isExpanded) { - isExpanded = true; - setTimeout(flashTabsWithAdvancedFields, 100); - } - }); - - group.addEventListener('hidden.bs.collapse', function() { - const anyVisible = Array.from(document.querySelectorAll('.advanced-field-group')).some( - g => g.classList.contains('show') - ); - if (!anyVisible) { - toggleButton.innerHTML = ' Show Advanced Options'; - isExpanded = false; - } - }); - }); - } - } - - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initializeAdvancedFields); - } else { - initializeAdvancedFields(); - } - - document.body.addEventListener('htmx:afterSwap', function(event) { - if (event.detail.target.id === 'service-form' || event.detail.target.closest('.crd-form')) { - setTimeout(initializeAdvancedFields, 100); - } - }); -})(); From 1cf1947539c45f8e22830bbc6705f880d87b6195 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Fri, 31 Oct 2025 11:49:04 +0100 Subject: [PATCH 111/185] Provide form building schema --- src/servala/core/forms.py | 21 +++ src/servala/core/models/service.py | 10 ++ .../core/schemas/form_config_schema.json | 132 ++++++++++++++++++ 3 files changed, 163 insertions(+) create mode 100644 src/servala/core/schemas/form_config_schema.json diff --git a/src/servala/core/forms.py b/src/servala/core/forms.py index 034d1c9..55b67d0 100644 --- a/src/servala/core/forms.py +++ b/src/servala/core/forms.py @@ -1,3 +1,7 @@ +import json +from pathlib import Path + +import jsonschema from django import forms from django.utils.translation import gettext_lazy as _ from django_jsonform.widgets import JSONFormWidget @@ -124,6 +128,10 @@ class ServiceDefinitionAdminForm(forms.ModelForm): self.fields["api_version"].initial = api_def.get("version", "") self.fields["api_kind"].initial = api_def.get("kind", "") + schema_path = Path(__file__).parent / "schemas" / "form_config_schema.json" + with open(schema_path) as f: + self.form_config_schema = json.load(f) + def clean(self): cleaned_data = super().clean() @@ -151,6 +159,19 @@ class ServiceDefinitionAdminForm(forms.ModelForm): api_def["kind"] = api_kind cleaned_data["api_definition"] = api_def + form_config = cleaned_data.get("form_config") + if form_config: + try: + jsonschema.validate(instance=form_config, schema=self.form_config_schema) + except jsonschema.ValidationError as e: + raise forms.ValidationError( + {"form_config": _("Invalid form configuration: {}").format(e.message)} + ) + except jsonschema.SchemaError as e: + raise forms.ValidationError( + {"form_config": _("Schema error: {}").format(e.message)} + ) + return cleaned_data def save(self, *args, **kwargs): diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index d03ef6b..470d928 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -360,6 +360,16 @@ class ServiceDefinition(ServalaModelMixin, models.Model): null=True, blank=True, ) + form_config = models.JSONField( + verbose_name=_("Form Configuration"), + help_text=_( + "Optional custom form configuration. When provided, this configuration will be used " + "to render the service form instead of auto-generating it from the OpenAPI spec. " + "Format: {\"fieldsets\": [{\"title\": \"Section\", \"fields\": [{...}]}]}" + ), + null=True, + blank=True, + ) service = models.ForeignKey( to="Service", on_delete=models.CASCADE, diff --git a/src/servala/core/schemas/form_config_schema.json b/src/servala/core/schemas/form_config_schema.json new file mode 100644 index 0000000..33955d8 --- /dev/null +++ b/src/servala/core/schemas/form_config_schema.json @@ -0,0 +1,132 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Service Definition Form Configuration Schema", + "description": "Schema for custom form configuration in ServiceDefinition", + "type": "object", + "required": ["fieldsets"], + "properties": { + "fieldsets": { + "type": "array", + "description": "Array of fieldset objects defining form sections", + "minItems": 1, + "items": { + "type": "object", + "required": ["fields"], + "properties": { + "title": { + "type": "string", + "description": "Optional title for the fieldset/tab" + }, + "fields": { + "type": "array", + "description": "Array of field definitions in this fieldset", + "minItems": 1, + "items": { + "type": "object", + "required": ["name", "type", "label", "controlplane_field_mapping"], + "properties": { + "name": { + "type": "string", + "description": "Unique field name/identifier", + "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$" + }, + "type": { + "type": "string", + "description": "Field type", + "enum": ["text", "email", "textarea", "number", "choice", "checkbox", "array"] + }, + "label": { + "type": "string", + "description": "Human-readable field label" + }, + "help_text": { + "type": "string", + "description": "Optional help text displayed below the field" + }, + "required": { + "type": "boolean", + "description": "Whether the field is required", + "default": false + }, + "default": { + "description": "Default value for the field" + }, + "controlplane_field_mapping": { + "type": "string", + "description": "Dot-notation path mapping to Kubernetes spec field (e.g., 'spec.parameters.service.fqdn')" + }, + "max_length": { + "type": "integer", + "description": "Maximum length for text/textarea fields", + "minimum": 1 + }, + "rows": { + "type": "integer", + "description": "Number of rows for textarea fields", + "minimum": 1 + }, + "min_value": { + "type": "number", + "description": "Minimum value for number fields" + }, + "max_value": { + "type": "number", + "description": "Maximum value for number fields" + }, + "choices": { + "type": "array", + "description": "Array of [value, label] pairs for choice fields", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + {"type": "string"}, + {"type": "string"} + ] + } + }, + "min_values": { + "type": "integer", + "description": "Minimum number of values for array fields", + "minimum": 0 + }, + "max_values": { + "type": "integer", + "description": "Maximum number of values for array fields", + "minimum": 1 + }, + "validators": { + "type": "array", + "description": "Array of validator names (for future use)", + "items": { + "type": "string", + "enum": ["email", "fqdn", "url", "ipv4", "ipv6"] + } + }, + "generators": { + "type": "array", + "description": "Array of generator function names (for future use)", + "items": { + "type": "string", + "enum": ["suggest_fqdn_from_name"] + } + } + }, + "allOf": [ + { + "if": { + "properties": {"type": {"const": "choice"}} + }, + "then": { + "required": ["choices"] + } + } + ] + } + } + } + } + } + } +} From 357e39b54369f6250725a59ee83a056a4cbd7910 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 5 Nov 2025 08:57:28 +0100 Subject: [PATCH 112/185] Remove expert fields, add form config --- src/servala/core/admin.py | 27 +++++++--------- .../migrations/0012_remove_advanced_fields.py | 32 +++++++++++++++++++ .../core/migrations/0013_add_form_config.py | 23 +++++++++++++ src/servala/core/models/service.py | 2 +- .../core/schemas/form_config_schema.json | 24 +++----------- src/servala/static/css/servala.css | 16 +++++----- 6 files changed, 80 insertions(+), 44 deletions(-) create mode 100644 src/servala/core/migrations/0012_remove_advanced_fields.py create mode 100644 src/servala/core/migrations/0013_add_form_config.py diff --git a/src/servala/core/admin.py b/src/servala/core/admin.py index 87da376..fb64a2b 100644 --- a/src/servala/core/admin.py +++ b/src/servala/core/admin.py @@ -1,3 +1,6 @@ +import json +from pathlib import Path + from django.contrib import admin, messages from django.utils.translation import gettext_lazy as _ from django_jsonform.widgets import JSONFormWidget @@ -313,9 +316,9 @@ class ServiceDefinitionAdmin(admin.ModelAdmin): ( _("Form Configuration"), { - "fields": ("advanced_fields",), + "fields": ("form_config",), "description": _( - "Configure which fields should be hidden behind an 'Advanced' toggle in the form" + "Optional custom form configuration. When provided, this will be used instead of auto-generating the form from the OpenAPI spec." ), }, ), @@ -323,19 +326,13 @@ class ServiceDefinitionAdmin(admin.ModelAdmin): def get_form(self, request, obj=None, **kwargs): form = super().get_form(request, obj, **kwargs) - # JSON schema for advanced_fields field - advanced_fields_schema = { - "type": "array", - "title": "Advanced Fields", - "items": { - "type": "string", - "title": "Field Name", - "description": "Field name in dot notation (e.g., spec.parameters.monitoring.enabled)", - }, - } - if "advanced_fields" in form.base_fields: - form.base_fields["advanced_fields"].widget = JSONFormWidget( - schema=advanced_fields_schema + schema_path = Path(__file__).parent / "schemas" / "form_config_schema.json" + with open(schema_path) as f: + form_config_schema = json.load(f) + + if "form_config" in form.base_fields: + form.base_fields["form_config"].widget = JSONFormWidget( + schema=form_config_schema ) return form diff --git a/src/servala/core/migrations/0012_remove_advanced_fields.py b/src/servala/core/migrations/0012_remove_advanced_fields.py new file mode 100644 index 0000000..d60d4cc --- /dev/null +++ b/src/servala/core/migrations/0012_remove_advanced_fields.py @@ -0,0 +1,32 @@ +# Generated by Django 5.2.7 on 2025-10-31 10:40 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0011_alter_organizationorigin_billing_entity"), + ] + + operations = [ + migrations.RemoveField( + model_name="servicedefinition", + name="advanced_fields", + ), + migrations.AlterField( + model_name="organization", + name="name", + field=models.CharField( + max_length=32, + validators=[ + django.core.validators.RegexValidator( + message="Organization name can only contain letters, numbers, and spaces.", + regex="^[A-Za-z0-9\\s]+$", + ) + ], + verbose_name="Name", + ), + ), + ] diff --git a/src/servala/core/migrations/0013_add_form_config.py b/src/servala/core/migrations/0013_add_form_config.py new file mode 100644 index 0000000..bd35891 --- /dev/null +++ b/src/servala/core/migrations/0013_add_form_config.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.7 on 2025-10-31 10:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0012_remove_advanced_fields"), + ] + + operations = [ + migrations.AddField( + model_name="servicedefinition", + name="form_config", + field=models.JSONField( + blank=True, + help_text='Optional custom form configuration. When provided, this configuration will be used to render the service form instead of auto-generating it from the OpenAPI spec. Format: {"fieldsets": [{"title": "Section", "fields": [{...}]}]}', + null=True, + verbose_name="Form Configuration", + ), + ), + ] diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index 470d928..7a6f390 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -365,7 +365,7 @@ class ServiceDefinition(ServalaModelMixin, models.Model): help_text=_( "Optional custom form configuration. When provided, this configuration will be used " "to render the service form instead of auto-generating it from the OpenAPI spec. " - "Format: {\"fieldsets\": [{\"title\": \"Section\", \"fields\": [{...}]}]}" + 'Format: {"fieldsets": [{"title": "Section", "fields": [{...}]}]}' ), null=True, blank=True, diff --git a/src/servala/core/schemas/form_config_schema.json b/src/servala/core/schemas/form_config_schema.json index 33955d8..1049ed8 100644 --- a/src/servala/core/schemas/form_config_schema.json +++ b/src/servala/core/schemas/form_config_schema.json @@ -48,9 +48,6 @@ "description": "Whether the field is required", "default": false }, - "default": { - "description": "Default value for the field" - }, "controlplane_field_mapping": { "type": "string", "description": "Dot-notation path mapping to Kubernetes spec field (e.g., 'spec.parameters.service.fqdn')" @@ -78,12 +75,9 @@ "description": "Array of [value, label] pairs for choice fields", "items": { "type": "array", - "minItems": 2, - "maxItems": 2, - "items": [ - {"type": "string"}, - {"type": "string"} - ] + "items": { + "type": "string" + } } }, "min_values": { @@ -112,17 +106,7 @@ "enum": ["suggest_fqdn_from_name"] } } - }, - "allOf": [ - { - "if": { - "properties": {"type": {"const": "choice"}} - }, - "then": { - "required": ["choices"] - } - } - ] + } } } } diff --git a/src/servala/static/css/servala.css b/src/servala/static/css/servala.css index 067720b..cc69a4f 100644 --- a/src/servala/static/css/servala.css +++ b/src/servala/static/css/servala.css @@ -237,42 +237,42 @@ a.btn-keycloak { flex-grow: 1; } -/* CRD Form mandatory field styling */ -.crd-form .form-group.mandatory .form-label { +/* Expert CRD Form mandatory field styling */ +.expert-crd-form .form-group.mandatory .form-label { font-weight: bold; position: relative; } -.crd-form .form-group.mandatory .form-label::after { +.expert-crd-form .form-group.mandatory .form-label::after { content: " *"; color: #dc3545; font-weight: bold; } -.crd-form .form-group.mandatory { +.expert-crd-form .form-group.mandatory { border-left: 3px solid #dc3545; padding-left: 10px; background-color: rgba(220, 53, 69, 0.05); border-radius: 3px; } -.crd-form .nav-tabs .nav-link .mandatory-indicator { +.expert-crd-form .nav-tabs .nav-link .mandatory-indicator { color: #dc3545; font-weight: bold; font-size: 1.1em; margin-left: 4px; } -html[data-bs-theme="dark"] .crd-form .form-group.mandatory { +html[data-bs-theme="dark"] .expert-crd-form .form-group.mandatory { background-color: rgba(220, 53, 69, 0.1); border-left-color: #ff6b6b; } -html[data-bs-theme="dark"] .crd-form .form-group.mandatory .form-label::after { +html[data-bs-theme="dark"] .expert-crd-form .form-group.mandatory .form-label::after { color: #ff6b6b; } -html[data-bs-theme="dark"] .crd-form .nav-tabs .nav-link .mandatory-indicator { +html[data-bs-theme="dark"] .expert-crd-form .nav-tabs .nav-link .mandatory-indicator { color: #ff6b6b; } From 0045e532ee6027801936fc7ba4f737ee171ea63b Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 5 Nov 2025 08:58:35 +0100 Subject: [PATCH 113/185] Generate custom service form from config --- src/servala/core/crd.py | 186 +++++++++++++++++++++++++---- src/servala/core/forms.py | 10 +- src/servala/core/models/service.py | 16 +++ 3 files changed, 187 insertions(+), 25 deletions(-) diff --git a/src/servala/core/crd.py b/src/servala/core/crd.py index 5d5c34e..5681ae0 100644 --- a/src/servala/core/crd.py +++ b/src/servala/core/crd.py @@ -7,6 +7,7 @@ from django.forms.models import ModelForm, ModelFormMetaclass from django.utils.translation import gettext_lazy as _ from servala.core.models import ServiceInstance +from servala.frontend.forms.widgets import DynamicArrayField, DynamicArrayWidget class CRDModel(models.Model): @@ -22,17 +23,11 @@ class CRDModel(models.Model): def duplicate_field(field_name, model): - # Get the field from the model field = model._meta.get_field(field_name) - - # Create a new field with the same attributes new_field = type(field).__new__(type(field)) new_field.__dict__.update(field.__dict__) - - # Ensure the field is not linked to the original model new_field.model = None new_field.auto_created = False - return new_field @@ -250,8 +245,6 @@ def get_django_field( ) elif field_type == "array": kwargs["help_text"] = field_schema.get("description") or _("List of values") - from servala.frontend.forms.widgets import DynamicArrayField - field = models.JSONField(**kwargs) formfield_kwargs = { "label": field.verbose_name, @@ -291,7 +284,29 @@ def unnest_data(data): return result -class CrdModelFormMixin: +class FormGeneratorMixin: + IS_CUSTOM_FORM = False + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + for field in ("organization", "context"): + if field in self.fields: + self.fields[field].widget = forms.HiddenInput() + + if self.instance and hasattr(self.instance, "name") and self.instance.name: + if "name" in self.fields: + self.fields["name"].disabled = True + self.fields["name"].widget = forms.HiddenInput() + + def has_mandatory_fields(self, field_list): + for field_name in field_list: + if field_name in self.fields and self.fields[field_name].required: + return True + return False + + +class CrdModelFormMixin(FormGeneratorMixin): HIDDEN_FIELDS = [ "spec.compositeDeletePolicy", "spec.compositionRef", @@ -317,9 +332,6 @@ class CrdModelFormMixin: super().__init__(*args, **kwargs) self.schema = self._meta.model.SCHEMA - for field in ("organization", "context"): - self.fields[field].widget = forms.HiddenInput() - for name, field in self.fields.items(): if name in self.HIDDEN_FIELDS or any( name.startswith(f) for f in self.HIDDEN_FIELDS @@ -327,22 +339,11 @@ class CrdModelFormMixin: field.widget = forms.HiddenInput() field.required = False - if self.instance and self.instance.pk: - self.fields["name"].disabled = True - self.fields["name"].help_text = _("Name cannot be changed after creation.") - self.fields["name"].widget = forms.HiddenInput() - def strip_title(self, field_name, label): field = self.fields[field_name] if field and field.label and (position := field.label.find(label)) != -1: field.label = field.label[position + len(label) :] - def has_mandatory_fields(self, field_list): - for field_name in field_list: - if field_name in self.fields and self.fields[field_name].required: - return True - return False - def get_fieldsets(self): fieldsets = [] @@ -524,3 +525,142 @@ def generate_model_form_class(model): } class_name = f"{model.__name__}ModelForm" return ModelFormMetaclass(class_name, (CrdModelFormMixin, ModelForm), fields) + + +class CustomFormMixin(FormGeneratorMixin): + """ + Base for custom (user-friendly) forms generated from ServiceDefinition.form_config. + """ + + IS_CUSTOM_FORM = True + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._apply_field_config() + if ( + self.instance + and hasattr(self.instance, "name") + and self.instance.name + and "name" in self.fields + ): + self.fields["name"].widget = forms.HiddenInput() + self.fields["name"].disabled = True + self.fields.pop("context", None) + self.fields.pop("organization", None) + + def _apply_field_config(self): + for fieldset in self.form_config.get("fieldsets", []): + for field_config in fieldset.get("fields", []): + field_name = field_config.get("controlplane_field_mapping") + + if field_name not in self.fields: + continue + + field = self.fields[field_name] + field_type = field_config.get("type") + + field.label = field_config.get("label", field_config["name"]) + field.help_text = field_config.get("help_text", "") + field.required = field_config.get("required", False) + + if field_type == "textarea": + field.widget = forms.Textarea( + attrs={"rows": field_config.get("rows", 4)} + ) + elif field_type == "array": + field.widget = DynamicArrayWidget() + + if field_type == "number": + min_val = field_config.get("min_value") + max_val = field_config.get("max_value") + + validators = [] + if min_val is not None: + validators.append(MinValueValidator(min_val)) + if max_val is not None: + validators.append(MaxValueValidator(max_val)) + + if validators: + field.validators.extend(validators) + + field.controlplane_field_mapping = field_name + + def get_fieldsets(self): + fieldsets = [] + for fieldset_config in self.form_config.get("fieldsets", []): + field_names = [ + f["controlplane_field_mapping"] + for f in fieldset_config.get("fields", []) + ] + fieldset = { + "title": fieldset_config.get("title", "General"), + "fields": field_names, + "fieldsets": [], + "has_mandatory": self.has_mandatory_fields(field_names), + } + fieldsets.append(fieldset) + + return fieldsets + + def get_nested_data(self): + nested = {} + for field_name in self.fields.keys(): + if field_name in ("organization", "context"): + value = self.cleaned_data.get(field_name) + if value is not None: + nested[field_name] = value + continue + + mapping = field_name + value = self.cleaned_data.get(field_name) + parts = mapping.split(".") + current = nested + for part in parts[:-1]: + if part not in current: + current[part] = {} + current = current[part] + + current[parts[-1]] = value + + return nested + + +def generate_custom_form_class(form_config, model): + """ + Generate a custom (user-friendly) form class from form_config JSON. + """ + field_list = ["organization", "context", "name"] + + for fieldset in form_config.get("fieldsets", []): + for field_config in fieldset.get("fields", []): + field_name = field_config.get("controlplane_field_mapping") + if field_name: + field_list.append(field_name) + + fields = { + "organization": forms.ModelChoiceField( + queryset=None, + required=True, + widget=forms.HiddenInput(), + ), + "context": forms.ModelChoiceField( + queryset=None, + required=True, + widget=forms.HiddenInput(), + ), + } + + meta_attrs = { + "model": model, + "fields": field_list, + } + + form_fields = { + "Meta": type("Meta", (object,), meta_attrs), + "__module__": "crd_models", + "form_config": form_config, + **fields, + } + + class_name = f"{model.__name__}CustomForm" + return ModelFormMetaclass(class_name, (CustomFormMixin, ModelForm), form_fields) diff --git a/src/servala/core/forms.py b/src/servala/core/forms.py index 55b67d0..9742233 100644 --- a/src/servala/core/forms.py +++ b/src/servala/core/forms.py @@ -162,10 +162,16 @@ class ServiceDefinitionAdminForm(forms.ModelForm): form_config = cleaned_data.get("form_config") if form_config: try: - jsonschema.validate(instance=form_config, schema=self.form_config_schema) + jsonschema.validate( + instance=form_config, schema=self.form_config_schema + ) except jsonschema.ValidationError as e: raise forms.ValidationError( - {"form_config": _("Invalid form configuration: {}").format(e.message)} + { + "form_config": _("Invalid form configuration: {}").format( + e.message + ) + } ) except jsonschema.SchemaError as e: raise forms.ValidationError( diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index 7a6f390..280c780 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -512,6 +512,22 @@ class ControlPlaneCRD(ServalaModelMixin, models.Model): return return generate_model_form_class(self.django_model) + @cached_property + def custom_model_form_class(self): + from servala.core.crd import generate_custom_form_class + + if not self.django_model: + return + if not ( + self.service_definition + and self.service_definition.form_config + and self.service_definition.form_config.get("fieldsets") + ): + return + return generate_custom_form_class( + self.service_definition.form_config, self.django_model + ) + class ServiceOffering(ServalaModelMixin, models.Model): """ From cedcab85c4a0c5a0b8503b9ca1e25759c5374ab9 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 5 Nov 2025 09:01:00 +0100 Subject: [PATCH 114/185] Use custom forms in instance update --- .../service_instance_update.html | 2 +- .../includes/tabbed_fieldset_form.html | 161 +++++++++++++----- src/servala/frontend/views/service.py | 66 ++++++- src/servala/static/js/expert-mode.js | 48 ++++++ 4 files changed, 237 insertions(+), 40 deletions(-) create mode 100644 src/servala/static/js/expert-mode.js diff --git a/src/servala/frontend/templates/frontend/organizations/service_instance_update.html b/src/servala/frontend/templates/frontend/organizations/service_instance_update.html index 51a9213..74259e6 100644 --- a/src/servala/frontend/templates/frontend/organizations/service_instance_update.html +++ b/src/servala/frontend/templates/frontend/organizations/service_instance_update.html @@ -22,7 +22,7 @@ {% translate "Oops! Something went wrong with the service form generation. Please try again later." %}
{% else %} - {% include "includes/tabbed_fieldset_form.html" with form=form %} + {% include "includes/tabbed_fieldset_form.html" with form=custom_form expert_form=form %} {% endif %}
diff --git a/src/servala/frontend/templates/includes/tabbed_fieldset_form.html b/src/servala/frontend/templates/includes/tabbed_fieldset_form.html index c34c41a..744df5f 100644 --- a/src/servala/frontend/templates/includes/tabbed_fieldset_form.html +++ b/src/servala/frontend/templates/includes/tabbed_fieldset_form.html @@ -6,48 +6,130 @@ {% if form_action %}action="{{ form_action }}"{% endif %}> {% csrf_token %} {% include "frontend/forms/errors.html" %} - -
- {% for fieldset in form.get_fieldsets %} -
- {% for field in fieldset.fields %} - {% with field=form|get_field:field %}{{ field.as_field_group }}{% endwith %} - {% endfor %} - {% for subfieldset in fieldset.fieldsets %} - {% if subfieldset.fields %} -
-

{{ subfieldset.title }}

- {% for field in subfieldset.fields %} - {% with field=form|get_field:field %}{{ field.as_field_group }}{% endwith %} - {% endfor %} -
+ {% if form %} +
+ +
+ {% endif %} +
+ {% if form and form.get_fieldsets|length == 1 %} + {# Single fieldset - render without tabs #} + {% for fieldset in form.get_fieldsets %} +
+ {% for field in fieldset.fields %} + {% with field=form|get_field:field %}{{ field.as_field_group }}{% endwith %} + {% endfor %} + {% for subfieldset in fieldset.fieldsets %} + {% if subfieldset.fields %} +
+

{{ subfieldset.title }}

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

{{ subfieldset.title }}

+ {% for field in subfieldset.fields %} + {% with field=form|get_field:field %}{{ field.as_field_group }}{% endwith %} + {% endfor %} +
+ {% endif %} + {% endfor %} +
+ {% endfor %}
- {% endfor %} + {% endif %}
+ {% if form and expert_form %} + + {% endif %} + {% if form %} + + {% endif %}
+{% if form %} + +{% endif %} diff --git a/src/servala/frontend/views/service.py b/src/servala/frontend/views/service.py index 689f381..52b0c71 100644 --- a/src/servala/frontend/views/service.py +++ b/src/servala/frontend/views/service.py @@ -393,11 +393,75 @@ class ServiceInstanceUpdateView( def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs["instance"] = self.object.spec_object + kwargs["prefix"] = "expert" return kwargs + def get_form(self, *args, ignore_data=False, **kwargs): + if not ignore_data: + return super().get_form(*args, **kwargs) + cls = self.get_form_class() + kwargs = self.get_form_kwargs() + if ignore_data: + kwargs.pop("data", None) + return cls(**kwargs) + + def get_custom_form(self, ignore_data=False): + cls = self.object.context.custom_model_form_class + if not cls: + return + kwargs = self.get_form_kwargs() + kwargs["prefix"] = "custom" + if ignore_data: + kwargs.pop("data", None) + return cls(**kwargs) + + @property + def is_custom_form(self): + # Note: "custom form" = user-friendly, subset of fields + # vs "expert form" = auto-generated (all technical fields) + return self.request.POST.get("active_form", "expert") == "custom" + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + + if self.is_custom_form: + form = self.get_custom_form() + else: + form = self.get_form() + + if form.is_valid(): + return self.form_valid(form) + return self.form_invalid(form) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + if self.request.method == "POST": + if self.is_custom_form: + context["custom_form"] = self.get_custom_form() + context["form"] = self.get_form(ignore_data=True) + else: + context["custom_form"] = self.get_custom_form(ignore_data=True) + else: + context["custom_form"] = self.get_custom_form() + return context + + def _deep_merge(self, base, update): + for key, value in update.items(): + if key in base and isinstance(base[key], dict) and isinstance(value, dict): + self._deep_merge(base[key], value) + else: + base[key] = value + return base + def form_valid(self, form): try: - spec_data = form.get_nested_data().get("spec") + form_data = form.get_nested_data() + spec_data = form_data.get("spec") + + if self.is_custom_form: + current_spec = dict(self.object.spec) if self.object.spec else {} + spec_data = self._deep_merge(current_spec, spec_data) + self.object.update_spec(spec_data=spec_data, updated_by=self.request.user) messages.success( self.request, diff --git a/src/servala/static/js/expert-mode.js b/src/servala/static/js/expert-mode.js new file mode 100644 index 0000000..d83bb69 --- /dev/null +++ b/src/servala/static/js/expert-mode.js @@ -0,0 +1,48 @@ +(function() { + 'use strict'; + + let isExpertMode = false; + + function initExpertMode() { + const toggleButton = document.getElementById('expert-mode-toggle'); + if (!toggleButton) return; + + const customFormContainer = document.getElementById('custom-form-container'); + const expertFormContainer = document.getElementById('expert-form-container'); + + if (!customFormContainer || !expertFormContainer) { + console.warn('Expert mode containers not found'); + return; + } + + toggleButton.addEventListener('click', function() { + isExpertMode = !isExpertMode; + + const activeFormInput = document.getElementById('active-form-input'); + + if (isExpertMode) { + customFormContainer.style.display = 'none'; + expertFormContainer.style.display = 'block'; + toggleButton.innerHTML = ' Show Simplified Form'; + if (activeFormInput) activeFormInput.value = 'expert'; + } else { + customFormContainer.style.display = 'block'; + expertFormContainer.style.display = 'none'; + toggleButton.innerHTML = ' Show Expert Mode'; + if (activeFormInput) activeFormInput.value = 'custom'; + } + }); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initExpertMode); + } else { + initExpertMode(); + } + + document.addEventListener('htmx:afterSwap', function(event) { + if (event.detail.target.id === 'service-form' || event.detail.target.classList.contains('crd-form')) { + initExpertMode(); + } + }); +})(); From 9e7330e24d9da4a5f3407ad8d25ce89b7fbbe229 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 5 Nov 2025 09:01:32 +0100 Subject: [PATCH 115/185] Fix form display details --- src/servala/frontend/templates/frontend/forms/field.html | 2 +- src/servala/frontend/templatetags/get_field.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/servala/frontend/templates/frontend/forms/field.html b/src/servala/frontend/templates/frontend/forms/field.html index 3e0a30b..b09f812 100644 --- a/src/servala/frontend/templates/frontend/forms/field.html +++ b/src/servala/frontend/templates/frontend/forms/field.html @@ -14,7 +14,7 @@ {% endif %} {% if field.use_fieldset %}{% endif %} {% for text in field.errors %}
{{ text }}
{% endfor %} - {% if field.help_text %} + {% if field.help_text and not field.is_hidden and not field.field.widget.input_type == "hidden" %} {{ field.help_text|safe }} {% endif %} diff --git a/src/servala/frontend/templatetags/get_field.py b/src/servala/frontend/templatetags/get_field.py index 3214beb..2141178 100644 --- a/src/servala/frontend/templatetags/get_field.py +++ b/src/servala/frontend/templatetags/get_field.py @@ -1,3 +1,5 @@ +from contextlib import suppress + from django import template register = template.Library() @@ -5,4 +7,5 @@ register = template.Library() @register.filter def get_field(form, field_name): - return form[field_name] + with suppress(KeyError): + return form[field_name] From 652e0798f4a1e3c4a7589328dcdd4e60d4fe51e7 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 5 Nov 2025 09:23:59 +0100 Subject: [PATCH 116/185] Make sure FQDN generation works with custom form --- src/servala/static/js/fqdn.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/servala/static/js/fqdn.js b/src/servala/static/js/fqdn.js index a92071d..2e20f6d 100644 --- a/src/servala/static/js/fqdn.js +++ b/src/servala/static/js/fqdn.js @@ -1,7 +1,7 @@ -const initializeFqdnGeneration = () => { - const nameField = document.querySelector('input#id_name'); - const fqdnField = document.querySelector('label[for="id_spec.parameters.service.fqdn"] + div input.array-item-input'); +const initializeFqdnGeneration = (prefix) => { + const nameField = document.querySelector(`input#id_${prefix}-name`); + const fqdnField = document.getElementById(`${prefix}-spec.parameters.service.fqdn_container`).querySelector('input.array-item-input'); if (nameField && fqdnField) { const generateFqdn = (instanceName) => { @@ -38,9 +38,10 @@ const initializeFqdnGeneration = () => { } } -document.addEventListener('DOMContentLoaded', initializeFqdnGeneration); +document.addEventListener('DOMContentLoaded', () => {initializeFqdnGeneration("custom"), initializeFqdnGeneration("expert")}); document.body.addEventListener('htmx:afterSwap', function(event) { if (event.detail.target.id === 'service-form') { - initializeFqdnGeneration(); + initializeFqdnGeneration("custom"); + initializeFqdnGeneration("expert"); } }); From 63039171c16f801a582cb11144c71f87797ea571 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 5 Nov 2025 10:14:52 +0100 Subject: [PATCH 117/185] Fix FQDN generation --- src/servala/static/js/fqdn.js | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/servala/static/js/fqdn.js b/src/servala/static/js/fqdn.js index 2e20f6d..178c586 100644 --- a/src/servala/static/js/fqdn.js +++ b/src/servala/static/js/fqdn.js @@ -9,28 +9,23 @@ const initializeFqdnGeneration = (prefix) => { return `${instanceName}-${fqdnConfig.namespace}.${fqdnConfig.wildcardDns}`; } - const newNameField = nameField.cloneNode(true); - nameField.parentNode.replaceChild(newNameField, nameField); - const newFqdnField = fqdnField.cloneNode(true); - fqdnField.parentNode.replaceChild(newFqdnField, fqdnField); - - newNameField.addEventListener('input', function() { - if (!newFqdnField.dataset.manuallyEdited) { - newFqdnField.value = generateFqdn(this.value); - const container = newFqdnField.closest('.dynamic-array-widget'); + 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); } } }); - newFqdnField.addEventListener('input', function() { + fqdnField.addEventListener('input', function() { this.dataset.manuallyEdited = 'true'; }); - if (newNameField.value && !newFqdnField.value) { - newFqdnField.value = generateFqdn(newNameField.value); - const container = newFqdnField.closest('.dynamic-array-widget'); + if (nameField.value && !fqdnField.value) { + fqdnField.value = generateFqdn(nameField.value); + const container = fqdnField.closest('.dynamic-array-widget'); if (container && window.updateHiddenInput) { window.updateHiddenInput(container); } From 2931315b969ccabd55e6315b04b9e2f096cfa36d Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 5 Nov 2025 10:15:23 +0100 Subject: [PATCH 118/185] Remove org field from generated form --- src/servala/core/crd.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/servala/core/crd.py b/src/servala/core/crd.py index 5681ae0..aebb99f 100644 --- a/src/servala/core/crd.py +++ b/src/servala/core/crd.py @@ -6,7 +6,7 @@ from django.db import models from django.forms.models import ModelForm, ModelFormMetaclass from django.utils.translation import gettext_lazy as _ -from servala.core.models import ServiceInstance +from servala.core.models import ServiceInstance, ControlPlaneCRD from servala.frontend.forms.widgets import DynamicArrayField, DynamicArrayWidget @@ -37,7 +37,7 @@ def generate_django_model(schema, group, version, kind): """ # We always need these three fields to know our own name and our full namespace model_fields = {"__module__": "crd_models"} - for field_name in ("name", "organization", "context"): + for field_name in ("name", "context"): model_fields[field_name] = duplicate_field(field_name, ServiceInstance) # All other fields are generated from the schema, except for the @@ -290,9 +290,10 @@ class FormGeneratorMixin: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - for field in ("organization", "context"): - if field in self.fields: - self.fields[field].widget = forms.HiddenInput() + if "context" in self.fields: + self.fields["context"].widget = forms.HiddenInput() + if "context" in self.initial: + self.fields["context"].queryset = ControlPlaneCRD.objects.filter(pk=self.initial["context"].pk) if self.instance and hasattr(self.instance, "name") and self.instance.name: if "name" in self.fields: @@ -546,7 +547,6 @@ class CustomFormMixin(FormGeneratorMixin): self.fields["name"].widget = forms.HiddenInput() self.fields["name"].disabled = True self.fields.pop("context", None) - self.fields.pop("organization", None) def _apply_field_config(self): for fieldset in self.form_config.get("fieldsets", []): @@ -605,7 +605,7 @@ class CustomFormMixin(FormGeneratorMixin): def get_nested_data(self): nested = {} for field_name in self.fields.keys(): - if field_name in ("organization", "context"): + if field_name == "context": value = self.cleaned_data.get(field_name) if value is not None: nested[field_name] = value @@ -629,7 +629,7 @@ def generate_custom_form_class(form_config, model): """ Generate a custom (user-friendly) form class from form_config JSON. """ - field_list = ["organization", "context", "name"] + field_list = ["context", "name"] for fieldset in form_config.get("fieldsets", []): for field_config in fieldset.get("fields", []): @@ -638,13 +638,8 @@ def generate_custom_form_class(form_config, model): field_list.append(field_name) fields = { - "organization": forms.ModelChoiceField( - queryset=None, - required=True, - widget=forms.HiddenInput(), - ), "context": forms.ModelChoiceField( - queryset=None, + queryset=ControlPlaneCRD.objects.none(), required=True, widget=forms.HiddenInput(), ), From 7f99c78084dc352888dc588fbc7ef61b79f0df2b Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 5 Nov 2025 10:18:46 +0100 Subject: [PATCH 119/185] Fix array widget container ID --- .../frontend/templates/frontend/forms/dynamic_array.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/servala/frontend/templates/frontend/forms/dynamic_array.html b/src/servala/frontend/templates/frontend/forms/dynamic_array.html index 9d61825..00c23b0 100644 --- a/src/servala/frontend/templates/frontend/forms/dynamic_array.html +++ b/src/servala/frontend/templates/frontend/forms/dynamic_array.html @@ -1,5 +1,5 @@
Date: Wed, 5 Nov 2025 10:19:56 +0100 Subject: [PATCH 120/185] Implement instance creation from custom form --- src/servala/core/crd.py | 5 +- src/servala/core/models/service.py | 1 - .../service_offering_detail.html | 4 +- .../includes/tabbed_fieldset_form.html | 12 ++-- src/servala/frontend/views/service.py | 61 +++++++++++-------- 5 files changed, 47 insertions(+), 36 deletions(-) diff --git a/src/servala/core/crd.py b/src/servala/core/crd.py index aebb99f..e414168 100644 --- a/src/servala/core/crd.py +++ b/src/servala/core/crd.py @@ -292,8 +292,9 @@ class FormGeneratorMixin: if "context" in self.fields: self.fields["context"].widget = forms.HiddenInput() - if "context" in self.initial: - self.fields["context"].queryset = ControlPlaneCRD.objects.filter(pk=self.initial["context"].pk) + if crd := self.initial.get("context"): + crd = getattr(crd, "pk", crd) # can be int or object + self.fields["context"].queryset = ControlPlaneCRD.objects.filter(pk=crd) if self.instance and hasattr(self.instance, "name") and self.instance.name: if "name" in self.fields: diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index 280c780..1535703 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -891,7 +891,6 @@ class ServiceInstance(ServalaModelMixin, models.Model): return return self.context.django_model( name=self.name, - organization=self.organization, context=self.context, spec=self.spec, # We pass -1 as ID in order to make it clear that a) this object exists (remotely), diff --git a/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html b/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html index 3305328..53e32d2 100644 --- a/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html +++ b/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html @@ -17,7 +17,7 @@ {% endif %} {% endpartialdef %} {% partialdef service-form %} -{% if service_form %} +{% if service_form or custom_service_form %}
@@ -26,7 +26,7 @@ {% translate "Oops! Something went wrong with the service form generation. Please try again later." %}
{% else %} - {% include "includes/tabbed_fieldset_form.html" with form=service_form %} + {% include "includes/tabbed_fieldset_form.html" with form=custom_service_form expert_form=service_form %} {% endif %}
diff --git a/src/servala/frontend/templates/includes/tabbed_fieldset_form.html b/src/servala/frontend/templates/includes/tabbed_fieldset_form.html index 744df5f..41f1af2 100644 --- a/src/servala/frontend/templates/includes/tabbed_fieldset_form.html +++ b/src/servala/frontend/templates/includes/tabbed_fieldset_form.html @@ -16,6 +16,7 @@
{% endif %}
+ {% if form and form.context %}{{ form.context }}{% endif %} {% if form and form.get_fieldsets|length == 1 %} {# Single fieldset - render without tabs #} {% for fieldset in form.get_fieldsets %} @@ -131,13 +132,10 @@ {% endif %}
- +
{% if form %} diff --git a/src/servala/frontend/views/service.py b/src/servala/frontend/views/service.py index 52b0c71..2c6923e 100644 --- a/src/servala/frontend/views/service.py +++ b/src/servala/frontend/views/service.py @@ -123,7 +123,7 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView def context_object(self): if self.request.method == "POST": return ControlPlaneCRD.objects.filter( - pk=self.request.POST.get("context"), + pk=self.request.POST.get("expert-context", self.request.POST.get("custom-context")), # Make sure we don’t use a malicious ID control_plane__in=self.planes, ).first() @@ -131,37 +131,49 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView control_plane=self.selected_plane, service_offering=self.object ).first() - def get_instance_form(self): - if not self.context_object or not self.context_object.model_form_class: - return None - initial = { + def get_instance_form_kwargs(self, ignore_data=False): + return {"initial": { "organization": self.request.organization, "context": self.context_object, - } + }, "prefix": "expert", "data": self.request.POST if (self.request.method == "POST" and not ignore_data) else None + } - # Pre-populate FQDN field if it exists and control plane has wildcard DNS - form_class = self.context_object.model_form_class - if ( - "spec.parameters.service.fqdn" in form_class.base_fields - and self.context_object.control_plane.wildcard_dns - ): - # Generate initial FQDN: instancename-namespace.wildcard_dns - # We'll set a placeholder that JavaScript will replace dynamically - initial["spec.parameters.service.fqdn"] = "" + def get_instance_form(self, ignore_data=False): + if not self.context_object or not self.context_object.model_form_class: + return - return form_class( - data=self.request.POST if self.request.method == "POST" else None, - initial=initial, - ) + return self.context_object.model_form_class(**self.get_instance_form_kwargs(ignore_data=ignore_data)) + + def get_custom_instance_form(self, ignore_data=False): + if not self.context_object or not self.context_object.custom_model_form_class: + return + kwargs = self.get_instance_form_kwargs(ignore_data=ignore_data) + kwargs["prefix"] = "custom" + return self.context_object.custom_model_form_class(**kwargs) + + @property + def is_custom_form(self): + # Note: "custom form" = user-friendly, subset of fields + # vs "expert form" = auto-generated (all technical fields) + return self.request.POST.get("active_form", "expert") == "custom" 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["service_form"] = self.get_instance_form() - # Pass data for dynamic FQDN generation + if self.request.method == "POST": + if self.is_custom_form: + context["service_form"] = self.get_instance_form(ignore_data=True) + context["custom_service_form"] = self.get_custom_instance_form() + else: + context["service_form"] = self.get_instance_form() + context["custom_service_form"] = self.get_custom_instance_form(ignore_data=True) + else: + context["service_form"] = self.get_instance_form() + context["custom_service_form"] = self.get_custom_instance_form() + if self.selected_plane and self.selected_plane.wildcard_dns: context["wildcard_dns"] = self.selected_plane.wildcard_dns context["organization_namespace"] = self.request.organization.namespace @@ -175,7 +187,10 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView context["form_error"] = True return self.render_to_response(context) - form = self.get_instance_form() + if self.is_custom_form: + form = self.get_custom_instance_form() + else: + form = self.get_instance_form() if not form: # Should not happen if context_object is valid, but as a safeguard messages.error( self.request, @@ -203,8 +218,6 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView ) form.add_error(None, error_message) - # If the form is not valid or if the service creation failed, we render it again - context["service_form"] = form return self.render_to_response(context) From 5cc582b638b9f2fd5b34a2db0b23cdf3e72e520a Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 5 Nov 2025 10:37:01 +0100 Subject: [PATCH 121/185] Validate fields used in custom form config --- src/servala/core/forms.py | 72 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/src/servala/core/forms.py b/src/servala/core/forms.py index 9742233..36f8be5 100644 --- a/src/servala/core/forms.py +++ b/src/servala/core/forms.py @@ -178,8 +178,80 @@ class ServiceDefinitionAdminForm(forms.ModelForm): {"form_config": _("Schema error: {}").format(e.message)} ) + self._validate_field_mappings(form_config, cleaned_data) + return cleaned_data + def _validate_field_mappings(self, form_config, cleaned_data): + if not self.instance.pk: + return + crd = self.instance.offering_control_planes.all().first() + if not crd: + return + + schema = None + try: + schema = crd.resource_schema + except Exception as e: + pass + + if not schema or not (spec_schema := schema.get("properties", {}).get("spec")): + return + + valid_paths = self._extract_field_paths(spec_schema, "spec") | {"name"} + included_mappings = set() + errors = [] + for fieldset in form_config.get("fieldsets", []): + for field in fieldset.get("fields", []): + mapping = field.get("controlplane_field_mapping") + included_mappings.add(mapping) + if mapping and mapping not in valid_paths: + field_name = field.get("name", mapping) + errors.append( + _( + "Field '{}' has invalid mapping '{}'. Valid paths are: {}" + ).format( + field_name, + mapping, + ", ".join(sorted(valid_paths)[:10]) + + ("..." if len(valid_paths) > 10 else ""), + ) + ) + + if "name" not in included_mappings: + raise forms.ValidationError( + { + "form_config": _( + "You must include a `name` field in the custom form config." + ) + } + ) + + if errors: + raise forms.ValidationError({"form_config": errors}) + + def _extract_field_paths(self, schema, prefix=""): + paths = set() + + if not isinstance(schema, dict): + return paths + + if "type" in schema and schema["type"] != "object": + if prefix: + paths.add(prefix) + + if schema.get("properties"): + for prop_name, prop_schema in schema["properties"].items(): + new_prefix = f"{prefix}.{prop_name}" if prefix else prop_name + paths.add(new_prefix) + paths.update(self._extract_field_paths(prop_schema, new_prefix)) + + if schema.get("type") == "array" and "items" in schema: + if prefix: + paths.add(prefix) + + return paths + def save(self, *args, **kwargs): self.instance.api_definition = self.cleaned_data["api_definition"] return super().save(*args, **kwargs) From a5d46b696f5b8a9aacecd277b1e9c397e28b9cb8 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 5 Nov 2025 10:37:11 +0100 Subject: [PATCH 122/185] Code style --- src/servala/core/crd.py | 2 +- src/servala/frontend/forms/organization.py | 1 - src/servala/frontend/views/service.py | 30 +++++++++++++++------- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/servala/core/crd.py b/src/servala/core/crd.py index e414168..8c31a27 100644 --- a/src/servala/core/crd.py +++ b/src/servala/core/crd.py @@ -6,7 +6,7 @@ from django.db import models from django.forms.models import ModelForm, ModelFormMetaclass from django.utils.translation import gettext_lazy as _ -from servala.core.models import ServiceInstance, ControlPlaneCRD +from servala.core.models import ControlPlaneCRD, ServiceInstance from servala.frontend.forms.widgets import DynamicArrayField, DynamicArrayWidget diff --git a/src/servala/frontend/forms/organization.py b/src/servala/frontend/forms/organization.py index 86ba0ab..45e7b11 100644 --- a/src/servala/frontend/forms/organization.py +++ b/src/servala/frontend/forms/organization.py @@ -8,7 +8,6 @@ from servala.core.models import Organization, OrganizationInvitation, Organizati from servala.core.odoo import get_invoice_addresses, get_odoo_countries from servala.frontend.forms.mixins import HtmxMixin - ORG_NAME_PATTERN = r"[\w\s\-.,&'()+]+" diff --git a/src/servala/frontend/views/service.py b/src/servala/frontend/views/service.py index 2c6923e..0f9800e 100644 --- a/src/servala/frontend/views/service.py +++ b/src/servala/frontend/views/service.py @@ -123,7 +123,9 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView def context_object(self): if self.request.method == "POST": return ControlPlaneCRD.objects.filter( - pk=self.request.POST.get("expert-context", self.request.POST.get("custom-context")), + pk=self.request.POST.get( + "expert-context", self.request.POST.get("custom-context") + ), # Make sure we don’t use a malicious ID control_plane__in=self.planes, ).first() @@ -131,19 +133,27 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView control_plane=self.selected_plane, service_offering=self.object ).first() - def get_instance_form_kwargs(self, ignore_data=False): - return {"initial": { - "organization": self.request.organization, - "context": self.context_object, - }, "prefix": "expert", "data": self.request.POST if (self.request.method == "POST" and not ignore_data) else None - } + return { + "initial": { + "organization": self.request.organization, + "context": self.context_object, + }, + "prefix": "expert", + "data": ( + self.request.POST + if (self.request.method == "POST" and not ignore_data) + else None + ), + } def get_instance_form(self, ignore_data=False): if not self.context_object or not self.context_object.model_form_class: return - return self.context_object.model_form_class(**self.get_instance_form_kwargs(ignore_data=ignore_data)) + return self.context_object.model_form_class( + **self.get_instance_form_kwargs(ignore_data=ignore_data) + ) def get_custom_instance_form(self, ignore_data=False): if not self.context_object or not self.context_object.custom_model_form_class: @@ -169,7 +179,9 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView context["custom_service_form"] = self.get_custom_instance_form() else: context["service_form"] = self.get_instance_form() - context["custom_service_form"] = self.get_custom_instance_form(ignore_data=True) + context["custom_service_form"] = self.get_custom_instance_form( + ignore_data=True + ) else: context["service_form"] = self.get_instance_form() context["custom_service_form"] = self.get_custom_instance_form() From 59e7a75c51f2e4231793e13782af33cbcefab474 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 5 Nov 2025 10:42:41 +0100 Subject: [PATCH 123/185] Simplify form configuration, remove generators reference --- src/servala/core/crd.py | 2 +- src/servala/core/schemas/form_config_schema.json | 15 +-------------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/src/servala/core/crd.py b/src/servala/core/crd.py index 8c31a27..6ae70c9 100644 --- a/src/servala/core/crd.py +++ b/src/servala/core/crd.py @@ -560,7 +560,7 @@ class CustomFormMixin(FormGeneratorMixin): field = self.fields[field_name] field_type = field_config.get("type") - field.label = field_config.get("label", field_config["name"]) + field.label = field_config.get("label", field_name) field.help_text = field_config.get("help_text", "") field.required = field_config.get("required", False) diff --git a/src/servala/core/schemas/form_config_schema.json b/src/servala/core/schemas/form_config_schema.json index 1049ed8..15f79df 100644 --- a/src/servala/core/schemas/form_config_schema.json +++ b/src/servala/core/schemas/form_config_schema.json @@ -23,13 +23,8 @@ "minItems": 1, "items": { "type": "object", - "required": ["name", "type", "label", "controlplane_field_mapping"], + "required": ["type", "label", "controlplane_field_mapping"], "properties": { - "name": { - "type": "string", - "description": "Unique field name/identifier", - "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$" - }, "type": { "type": "string", "description": "Field type", @@ -97,14 +92,6 @@ "type": "string", "enum": ["email", "fqdn", "url", "ipv4", "ipv6"] } - }, - "generators": { - "type": "array", - "description": "Array of generator function names (for future use)", - "items": { - "type": "string", - "enum": ["suggest_fqdn_from_name"] - } } } } From ca485978b93b8997bdbe774127503dcdc1765e9f Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 5 Nov 2025 10:45:37 +0100 Subject: [PATCH 124/185] Code style --- src/servala/core/forms.py | 2 +- .../core/migrations/0013_add_form_config.py | 6 ++++- .../frontend/templates/account/login.html | 4 ++- .../service_offering_detail.html | 4 ++- .../includes/control_plane_user_info.html | 6 ++--- .../includes/tabbed_fieldset_form.html | 27 +++++++++++-------- 6 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/servala/core/forms.py b/src/servala/core/forms.py index 36f8be5..dacd956 100644 --- a/src/servala/core/forms.py +++ b/src/servala/core/forms.py @@ -192,7 +192,7 @@ class ServiceDefinitionAdminForm(forms.ModelForm): schema = None try: schema = crd.resource_schema - except Exception as e: + except Exception: pass if not schema or not (spec_schema := schema.get("properties", {}).get("spec")): diff --git a/src/servala/core/migrations/0013_add_form_config.py b/src/servala/core/migrations/0013_add_form_config.py index bd35891..2819a6c 100644 --- a/src/servala/core/migrations/0013_add_form_config.py +++ b/src/servala/core/migrations/0013_add_form_config.py @@ -15,7 +15,11 @@ class Migration(migrations.Migration): name="form_config", field=models.JSONField( blank=True, - help_text='Optional custom form configuration. When provided, this configuration will be used to render the service form instead of auto-generating it from the OpenAPI spec. Format: {"fieldsets": [{"title": "Section", "fields": [{...}]}]}', + help_text=( + "Optional custom form configuration. When provided, this configuration will " + "be used to render the service form instead of auto-generating it from the OpenAPI spec. " + 'Format: {"fieldsets": [{"title": "Section", "fields": [{...}]}]}' + ), null=True, verbose_name="Form Configuration", ), diff --git a/src/servala/frontend/templates/account/login.html b/src/servala/frontend/templates/account/login.html index f4ca590..906a9fa 100644 --- a/src/servala/frontend/templates/account/login.html +++ b/src/servala/frontend/templates/account/login.html @@ -31,7 +31,9 @@
{% for provider in socialaccount_providers %} {% provider_login_url provider process=process scope=scope auth_params=auth_params as href %} -
+ {% csrf_token %} {{ redirect_field }}
diff --git a/src/servala/frontend/templates/includes/control_plane_user_info.html b/src/servala/frontend/templates/includes/control_plane_user_info.html index a3a27f5..fdcc995 100644 --- a/src/servala/frontend/templates/includes/control_plane_user_info.html +++ b/src/servala/frontend/templates/includes/control_plane_user_info.html @@ -4,9 +4,7 @@ {% for info in control_plane.user_info %}
- - {{ info.title }} - + {{ info.title }} {% if info.help_text %} + font-size: 0.875rem"> {% endif %}
diff --git a/src/servala/frontend/templates/includes/tabbed_fieldset_form.html b/src/servala/frontend/templates/includes/tabbed_fieldset_form.html index 41f1af2..f9ed2a4 100644 --- a/src/servala/frontend/templates/includes/tabbed_fieldset_form.html +++ b/src/servala/frontend/templates/includes/tabbed_fieldset_form.html @@ -15,7 +15,8 @@
{% endif %} -
+
{% if form and form.context %}{{ form.context }}{% endif %} {% if form and form.get_fieldsets|length == 1 %} {# Single fieldset - render without tabs #} @@ -41,8 +42,7 @@
{% endif %}
- {% if form and expert_form %} + {% if expert_form %} + {% if form %} {% endif %} diff --git a/src/servala/static/js/bootstrap-tabs.js b/src/servala/static/js/bootstrap-tabs.js new file mode 100644 index 0000000..d382475 --- /dev/null +++ b/src/servala/static/js/bootstrap-tabs.js @@ -0,0 +1,30 @@ +// Bootstrap 5 automatically initializes tabs with data-bs-toggle="tab" +// but we need to ensure they work after HTMX swaps +(function() { + 'use strict'; + + const initBootstrapTabs = () => { + const customTabList = document.querySelectorAll('#myTab button[data-bs-toggle="tab"]'); + customTabList.forEach(function(tabButton) { + new bootstrap.Tab(tabButton); + }); + + const expertTabList = document.querySelectorAll('#expertTab button[data-bs-toggle="tab"]'); + expertTabList.forEach(function(tabButton) { + new bootstrap.Tab(tabButton); + }); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initBootstrapTabs); + } else { + initBootstrapTabs(); + } + + document.addEventListener('htmx:afterSwap', function(event) { + if (event.detail.target.id === 'service-form' || + event.detail.target.classList.contains('crd-form')) { + initBootstrapTabs(); + } + }); +})(); From 089dbb663a9f8766a287f9539f39cf183b45821b Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Thu, 6 Nov 2025 15:55:27 +0100 Subject: [PATCH 133/185] Fix custom choices not being used --- src/servala/core/crd/forms.py | 24 ++++ src/servala/core/forms.py | 61 +++++++++ src/tests/test_form_config.py | 241 ++++++++++++++++++++++++++++++++++ 3 files changed, 326 insertions(+) diff --git a/src/servala/core/crd/forms.py b/src/servala/core/crd/forms.py index c82d971..71fac67 100644 --- a/src/servala/core/crd/forms.py +++ b/src/servala/core/crd/forms.py @@ -297,6 +297,11 @@ class CustomFormMixin(FormGeneratorMixin): ) elif field_type == "array": field.widget = DynamicArrayWidget() + elif field_type == "choice": + if hasattr(field, "choices") and field.choices: + field._controlplane_choices = list(field.choices) + if custom_choices := field_config.get("choices"): + field.choices = [tuple(choice) for choice in custom_choices] if field_type == "number": min_val = field_config.get("min_value") @@ -330,6 +335,25 @@ class CustomFormMixin(FormGeneratorMixin): return fieldsets + def clean(self): + cleaned_data = super().clean() + + for field_name, field in self.fields.items(): + if hasattr(field, "_controlplane_choices"): + value = cleaned_data.get(field_name) + if value: + valid_values = [choice[0] for choice in field._controlplane_choices] + if value not in valid_values: + self.add_error( + field_name, + forms.ValidationError( + f"'{value}' is not a valid choice. " + f"Must be one of: {valid_values.join(', ')}" + ), + ) + + return cleaned_data + def get_nested_data(self): nested = {} for field_name in self.fields.keys(): diff --git a/src/servala/core/forms.py b/src/servala/core/forms.py index dacd956..2b283f2 100644 --- a/src/servala/core/forms.py +++ b/src/servala/core/forms.py @@ -218,6 +218,11 @@ class ServiceDefinitionAdminForm(forms.ModelForm): ) ) + if field.get("type") == "choice" and field.get("choices"): + self._validate_choice_field( + field, mapping, spec_schema, "spec", errors + ) + if "name" not in included_mappings: raise forms.ValidationError( { @@ -230,6 +235,62 @@ class ServiceDefinitionAdminForm(forms.ModelForm): if errors: raise forms.ValidationError({"form_config": errors}) + def _validate_choice_field(self, field, mapping, spec_schema, prefix, errors): + if not mapping: + return + + field_schema = self._get_field_schema(spec_schema, mapping, prefix) + if not field_schema: + return + + control_plane_choices = field_schema.get("enum", []) + if not control_plane_choices: + return + + custom_choices = field.get("choices", []) + custom_choice_values = [choice[0] for choice in custom_choices] + + invalid_choices = [ + value + for value in custom_choice_values + if value not in control_plane_choices + ] + + if invalid_choices: + field_name = field.get("label", mapping) + errors.append( + _( + "Field '{}' has invalid choice values: {}. " + "Valid choices from control plane are: {}" + ).format( + field_name, + ", ".join(f"'{c}'" for c in invalid_choices), + ", ".join(f"'{c}'" for c in control_plane_choices), + ) + ) + + def _get_field_schema(self, schema, field_path, prefix): + if not field_path or not schema: + return None + + if field_path.startswith(prefix + "."): + field_path = field_path[len(prefix) + 1 :] + + parts = field_path.split(".") + current_schema = schema + + for part in parts: + if not isinstance(current_schema, dict): + return None + + properties = current_schema.get("properties", {}) + if part not in properties: + return None + + current_schema = properties[part] + + return current_schema + def _extract_field_paths(self, schema, prefix=""): paths = set() diff --git a/src/tests/test_form_config.py b/src/tests/test_form_config.py index 913eb59..32c2bb8 100644 --- a/src/tests/test_form_config.py +++ b/src/tests/test_form_config.py @@ -216,3 +216,244 @@ def test_form_config_schema_validates_full_config(): ] } jsonschema.validate(instance=full_config, schema=schema) + + +def test_choice_field_uses_custom_choices_from_form_config(): + """Test that choice fields use custom choices when provided in form_config""" + + class TestModel(models.Model): + name = models.CharField(max_length=100) + environment = models.CharField( + max_length=20, + choices=[ + ("dev", "Development"), + ("staging", "Staging"), + ("prod", "Production"), + ("test", "Testing"), + ], + ) + + class Meta: + app_label = "test" + + form_config = { + "fieldsets": [ + { + "title": "General", + "fields": [ + { + "type": "text", + "label": "Name", + "controlplane_field_mapping": "name", + "required": True, + }, + { + "type": "choice", + "label": "Environment", + "controlplane_field_mapping": "environment", + "required": True, + "choices": [["dev", "Development"], ["prod", "Production"]], + }, + ], + } + ] + } + + form_class = generate_custom_form_class(form_config, TestModel) + form = form_class() + + environment_field = form.fields["environment"] + assert list(environment_field.choices) == [ + ("dev", "Development"), + ("prod", "Production"), + ] + + assert hasattr(environment_field, "_controlplane_choices") + assert len(environment_field._controlplane_choices) == 5 # 4 choices + empty choice + + +def test_choice_field_uses_control_plane_choices_when_no_custom_choices(): + + class TestModel(models.Model): + name = models.CharField(max_length=100) + environment = models.CharField( + max_length=20, + choices=[ + ("dev", "Development"), + ("staging", "Staging"), + ("prod", "Production"), + ], + ) + + class Meta: + app_label = "test" + + form_config = { + "fieldsets": [ + { + "title": "General", + "fields": [ + { + "type": "text", + "label": "Name", + "controlplane_field_mapping": "name", + "required": True, + }, + { + "type": "choice", + "label": "Environment", + "controlplane_field_mapping": "environment", + "required": True, + }, + ], + } + ] + } + + form_class = generate_custom_form_class(form_config, TestModel) + form = form_class() + + environment_field = form.fields["environment"] + choices_list = list(environment_field.choices) + assert len(choices_list) == 4 # 3 choices + empty choice + assert ("dev", "Development") in choices_list + + +def test_choice_field_validates_against_control_plane_choices(): + class TestModel(models.Model): + name = models.CharField(max_length=100) + environment = models.CharField( + max_length=20, + choices=[ + ("dev", "Development"), + ("staging", "Staging"), + ("prod", "Production"), + ], + ) + + class Meta: + app_label = "test" + + form_config = { + "fieldsets": [ + { + "title": "General", + "fields": [ + { + "type": "text", + "label": "Name", + "controlplane_field_mapping": "name", + "required": True, + }, + { + "type": "choice", + "label": "Environment", + "controlplane_field_mapping": "environment", + "required": True, + "choices": [["dev", "Development"], ["prod", "Production"]], + }, + ], + } + ] + } + + form_class = generate_custom_form_class(form_config, TestModel) + + form = form_class(data={"name": "test-service", "environment": "dev"}) + form.fields["context"].required = False # Skip context validation + assert form.is_valid(), f"Form should be valid but has errors: {form.errors}" + + form = form_class(data={"name": "test-service", "environment": "prod"}) + form.fields["context"].required = False # Skip context validation + assert form.is_valid(), f"Form should be valid but has errors: {form.errors}" + + form = form_class(data={"name": "test-service", "environment": "invalid"}) + form.fields["context"].required = False # Skip context validation + assert not form.is_valid() + assert "environment" in form.errors + + +def test_admin_form_validates_choice_values_against_schema(): + from servala.core.forms import ServiceDefinitionAdminForm + + form = ServiceDefinitionAdminForm() + mock_crd = Mock() + mock_crd.resource_schema = { + "properties": { + "spec": { + "properties": { + "environment": { + "type": "string", + "enum": ["dev", "staging", "prod"], + } + } + } + } + } + + valid_form_config = { + "fieldsets": [ + { + "fields": [ + { + "type": "text", + "label": "Name", + "controlplane_field_mapping": "name", + }, + { + "type": "choice", + "label": "Environment", + "controlplane_field_mapping": "spec.environment", + "choices": [["dev", "Development"], ["prod", "Production"]], + }, + ] + } + ] + } + + spec_schema = mock_crd.resource_schema["properties"]["spec"] + errors = [] + + for field in valid_form_config["fieldsets"][0]["fields"]: + if field.get("type") == "choice": + form._validate_choice_field( + field, field["controlplane_field_mapping"], spec_schema, "spec", errors + ) + + assert len(errors) == 0, f"Expected no errors but got: {errors}" + + invalid_form_config = { + "fieldsets": [ + { + "fields": [ + { + "type": "text", + "label": "Name", + "controlplane_field_mapping": "name", + }, + { + "type": "choice", + "label": "Environment", + "controlplane_field_mapping": "spec.environment", + "choices": [ + ["dev", "Development"], + ["invalid", "Invalid Environment"], + ], + }, + ] + } + ] + } + + errors = [] + + for field in invalid_form_config["fieldsets"][0]["fields"]: + if field.get("type") == "choice": + form._validate_choice_field( + field, field["controlplane_field_mapping"], spec_schema, "spec", errors + ) + + assert len(errors) > 0, "Expected validation errors but got none" + error_message = str(errors[0]) + assert "invalid" in error_message.lower() + assert "Environment" in error_message From bab9d636ee1c0ad8db1d523b1c4175f6db61683d Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Thu, 6 Nov 2025 15:58:56 +0100 Subject: [PATCH 134/185] Fix missing number validators on custom form widgets --- src/servala/core/crd/forms.py | 2 ++ src/tests/test_form_config.py | 64 ++++++++++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/servala/core/crd/forms.py b/src/servala/core/crd/forms.py index 71fac67..6eb8c74 100644 --- a/src/servala/core/crd/forms.py +++ b/src/servala/core/crd/forms.py @@ -310,8 +310,10 @@ class CustomFormMixin(FormGeneratorMixin): validators = [] if min_val is not None: validators.append(MinValueValidator(min_val)) + field.widget.attrs["min"] = min_val if max_val is not None: validators.append(MaxValueValidator(max_val)) + field.widget.attrs["max"] = max_val if validators: field.validators.extend(validators) diff --git a/src/tests/test_form_config.py b/src/tests/test_form_config.py index 32c2bb8..3b6a197 100644 --- a/src/tests/test_form_config.py +++ b/src/tests/test_form_config.py @@ -1,6 +1,7 @@ from unittest.mock import Mock import jsonschema +from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from servala.core.crd import generate_custom_form_class @@ -374,7 +375,6 @@ def test_choice_field_validates_against_control_plane_choices(): def test_admin_form_validates_choice_values_against_schema(): - from servala.core.forms import ServiceDefinitionAdminForm form = ServiceDefinitionAdminForm() mock_crd = Mock() @@ -457,3 +457,65 @@ def test_admin_form_validates_choice_values_against_schema(): error_message = str(errors[0]) assert "invalid" in error_message.lower() assert "Environment" in error_message + + +def test_number_field_min_max_sets_widget_attributes(): + class TestModel(models.Model): + name = models.CharField(max_length=100) + port = models.IntegerField() + replica_count = models.IntegerField() + + class Meta: + app_label = "test" + + form_config = { + "fieldsets": [ + { + "title": "General", + "fields": [ + { + "type": "text", + "label": "Name", + "controlplane_field_mapping": "name", + "required": True, + }, + { + "type": "number", + "label": "Port", + "controlplane_field_mapping": "port", + "required": True, + "min_value": 1, + "max_value": 65535, + }, + { + "type": "number", + "label": "Replicas", + "controlplane_field_mapping": "replica_count", + "required": True, + "min_value": 1, + "max_value": 10, + }, + ], + } + ] + } + + form_class = generate_custom_form_class(form_config, TestModel) + form = form_class() + + port_field = form.fields["port"] + assert port_field.widget.attrs.get("min") == 1 + assert port_field.widget.attrs.get("max") == 65535 + + replica_field = form.fields["replica_count"] + assert replica_field.widget.attrs.get("min") == 1 + assert replica_field.widget.attrs.get("max") == 10 + + port_validators = port_field.validators + assert any( + isinstance(v, MinValueValidator) and v.limit_value == 1 for v in port_validators + ) + assert any( + isinstance(v, MaxValueValidator) and v.limit_value == 65535 + for v in port_validators + ) From ece60ad3b198f31e43437507aae4298b8937b9b8 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Thu, 6 Nov 2025 16:14:02 +0100 Subject: [PATCH 135/185] Implement default values in custom forms --- src/servala/core/crd/forms.py | 3 + .../core/schemas/form_config_schema.json | 4 + src/tests/test_form_config.py | 114 ++++++++++++++++++ 3 files changed, 121 insertions(+) diff --git a/src/servala/core/crd/forms.py b/src/servala/core/crd/forms.py index 6eb8c74..99e2737 100644 --- a/src/servala/core/crd/forms.py +++ b/src/servala/core/crd/forms.py @@ -318,6 +318,9 @@ class CustomFormMixin(FormGeneratorMixin): if validators: field.validators.extend(validators) + if "default_value" in field_config and field.initial is None: + field.initial = field_config["default_value"] + field.controlplane_field_mapping = field_name def get_fieldsets(self): diff --git a/src/servala/core/schemas/form_config_schema.json b/src/servala/core/schemas/form_config_schema.json index 62fa91b..4cdc061 100644 --- a/src/servala/core/schemas/form_config_schema.json +++ b/src/servala/core/schemas/form_config_schema.json @@ -92,6 +92,10 @@ "type": "string", "enum": ["email", "fqdn", "url", "ipv4", "ipv6"] } + }, + "default_value": { + "type": "string", + "description": "Default value for the field when creating new instances" } } } diff --git a/src/tests/test_form_config.py b/src/tests/test_form_config.py index 3b6a197..0228107 100644 --- a/src/tests/test_form_config.py +++ b/src/tests/test_form_config.py @@ -519,3 +519,117 @@ def test_number_field_min_max_sets_widget_attributes(): isinstance(v, MaxValueValidator) and v.limit_value == 65535 for v in port_validators ) + + +def test_default_value_for_all_field_types(): + + class TestModel(models.Model): + name = models.CharField(max_length=100) + description = models.TextField() + port = models.IntegerField() + environment = models.CharField( + max_length=20, + choices=[ + ("dev", "Development"), + ("staging", "Staging"), + ("prod", "Production"), + ], + ) + monitoring_enabled = models.BooleanField() + tags = models.JSONField() + + class Meta: + app_label = "test" + + form_config = { + "fieldsets": [ + { + "fields": [ + { + "type": "text", + "label": "Name", + "controlplane_field_mapping": "name", + "default_value": "default-name", + }, + { + "type": "textarea", + "label": "Description", + "controlplane_field_mapping": "description", + "default_value": "Default description text", + }, + { + "type": "number", + "label": "Port", + "controlplane_field_mapping": "port", + "default_value": "8080", + }, + { + "type": "choice", + "label": "Environment", + "controlplane_field_mapping": "environment", + "default_value": "dev", + }, + { + "type": "checkbox", + "label": "Enable Monitoring", + "controlplane_field_mapping": "monitoring_enabled", + "default_value": "true", + }, + { + "type": "array", + "label": "Tags", + "controlplane_field_mapping": "tags", + "default_value": "tag1,tag2,tag3", + }, + ], + } + ] + } + + form_class = generate_custom_form_class(form_config, TestModel) + form = form_class() + + assert form.fields["name"].initial == "default-name" + assert form.fields["description"].initial == "Default description text" + assert form.fields["port"].initial == "8080" + assert form.fields["environment"].initial == "dev" + assert form.fields["monitoring_enabled"].initial == "true" + assert form.fields["tags"].initial == "tag1,tag2,tag3" + + +def test_default_value_not_override_existing_instance(): + + class TestModel(models.Model): + name = models.CharField(max_length=100) + port = models.IntegerField() + + class Meta: + app_label = "test" + + form_config = { + "fieldsets": [ + { + "fields": [ + { + "type": "text", + "label": "Name", + "controlplane_field_mapping": "name", + "default_value": "default-name", + }, + { + "type": "number", + "label": "Port", + "controlplane_field_mapping": "port", + "default_value": "8080", + }, + ], + } + ] + } + + instance = TestModel(name="existing-name", port=3000) + form_class = generate_custom_form_class(form_config, TestModel) + form = form_class(instance=instance) + + assert form.initial["name"] == "existing-name" + assert form.initial["port"] == 3000 From fa7a1708718b09ed76f0e13db9c20dca43b080e3 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Thu, 6 Nov 2025 16:38:13 +0100 Subject: [PATCH 136/185] Skip offering selection if there is only one closes #258 --- src/servala/frontend/views/service.py | 27 ++++++- src/tests/test_views.py | 101 ++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 4 deletions(-) diff --git a/src/servala/frontend/views/service.py b/src/servala/frontend/views/service.py index 689f381..5f2d914 100644 --- a/src/servala/frontend/views/service.py +++ b/src/servala/frontend/views/service.py @@ -66,14 +66,33 @@ class ServiceDetailView(OrganizationViewMixin, DetailView): def get_queryset(self): return self.request.organization.get_visible_services() - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - offerings = context["service"].offerings.all() + @cached_property + def visible_offerings(self): + offerings = self.object.offerings.all() if self.request.organization.limit_cloudproviders.exists(): offerings = offerings.filter( provider__in=self.request.organization.limit_cloudproviders.all() ) - context["visible_offerings"] = offerings.select_related("provider") + return offerings + + def get(self, request, *args, **kwargs): + self.object = self.get_object() + + # If there's exactly one offering, skip provider selection and go directly to it + if self.visible_offerings.count() == 1: + offering = self.visible_offerings.first() + return redirect( + "frontend:organization.offering", + organization=self.request.organization.slug, + slug=self.object.slug, + pk=offering.pk, + ) + + return super().get(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["visible_offerings"] = self.visible_offerings.select_related("provider") return context diff --git a/src/tests/test_views.py b/src/tests/test_views.py index 5ec429e..a97ecd1 100644 --- a/src/tests/test_views.py +++ b/src/tests/test_views.py @@ -1,5 +1,6 @@ import pytest +from servala.core.models.service import CloudProvider, ServiceOffering @pytest.mark.parametrize( "url,redirect", @@ -45,3 +46,103 @@ def test_organization_linked_in_sidebar( assert response.status_code == 200 assert organization.name in response.content.decode() assert other_organization.name not in response.content.decode() + + +@pytest.mark.django_db +def test_service_detail_redirects_with_single_offering( + client, org_owner, organization, test_service, test_service_offering +): + client.force_login(org_owner) + url = f"/org/{organization.slug}/services/{test_service.slug}/" + response = client.get(url) + + assert response.status_code == 302 + expected_url = f"/org/{organization.slug}/services/{test_service.slug}/offering/{test_service_offering.pk}/" + assert response.url == expected_url + + +@pytest.mark.django_db +def test_service_detail_shows_multiple_offerings( + client, org_owner, organization, test_service, test_service_offering +): + second_provider = CloudProvider.objects.create( + name="AWS", description="Amazon Web Services" + ) + second_offering = ServiceOffering.objects.create( + service=test_service, + provider=second_provider, + description="Redis on AWS", + osb_plan_id="test-plan-456", + ) + + client.force_login(org_owner) + url = f"/org/{organization.slug}/services/{test_service.slug}/" + response = client.get(url) + + assert response.status_code == 200 + content = response.content.decode() + + assert test_service_offering.provider.name in content + assert second_offering.provider.name in content + assert "Create Instance" in content + + +@pytest.mark.django_db +def test_service_detail_respects_cloud_provider_restrictions( + client, org_owner, organization, test_service, test_service_offering +): + second_provider = CloudProvider.objects.create( + name="AWS", description="Amazon Web Services" + ) + ServiceOffering.objects.create( + service=test_service, + provider=second_provider, + description="Redis on AWS", + osb_plan_id="test-plan-456", + ) + organization.origin.limit_cloudproviders.add(test_service_offering.provider) + + client.force_login(org_owner) + url = f"/org/{organization.slug}/services/{test_service.slug}/" + response = client.get(url) + + assert response.status_code == 302 + expected_url = f"/org/{organization.slug}/services/{test_service.slug}/offering/{test_service_offering.pk}/" + assert response.url == expected_url + + +@pytest.mark.django_db +def test_service_detail_no_redirect_with_restricted_multiple_offerings( + client, org_owner, organization, test_service, test_service_offering +): + second_provider = CloudProvider.objects.create( + name="AWS", description="Amazon Web Services" + ) + second_offering = ServiceOffering.objects.create( + service=test_service, + provider=second_provider, + description="Redis on AWS", + osb_plan_id="test-plan-456", + ) + third_provider = CloudProvider.objects.create( + name="Azure", description="Microsoft Azure" + ) + third_offering = ServiceOffering.objects.create( + service=test_service, + provider=third_provider, + description="Redis on Azure", + osb_plan_id="test-plan-789", + ) + organization.origin.limit_cloudproviders.add( + test_service_offering.provider, second_provider + ) + + client.force_login(org_owner) + url = f"/org/{organization.slug}/services/{test_service.slug}/" + response = client.get(url) + + assert response.status_code == 200 + content = response.content.decode() + assert test_service_offering.provider.name in content + assert second_offering.provider.name in content + assert third_offering.provider.name not in content From 1ed261d4b246d1754530cc107fedac7dd935d386 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Fri, 7 Nov 2025 11:28:49 +0100 Subject: [PATCH 137/185] Coerce string-numbers to numbers in admin form --- src/servala/core/forms.py | 39 ++++++++++++ src/tests/test_form_config.py | 116 ++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+) diff --git a/src/servala/core/forms.py b/src/servala/core/forms.py index 2b283f2..116e4d9 100644 --- a/src/servala/core/forms.py +++ b/src/servala/core/forms.py @@ -161,6 +161,9 @@ class ServiceDefinitionAdminForm(forms.ModelForm): form_config = cleaned_data.get("form_config") if form_config: + form_config = self._normalize_form_config_types(form_config) + cleaned_data["form_config"] = form_config + try: jsonschema.validate( instance=form_config, schema=self.form_config_schema @@ -182,6 +185,42 @@ class ServiceDefinitionAdminForm(forms.ModelForm): return cleaned_data + def _normalize_form_config_types(self, form_config): + """ + Normalize form_config by converting string representations of numbers + to actual integers/floats. The JSON form widget sends all values + as strings, but the schema expects proper types. + """ + if not isinstance(form_config, dict): + return form_config + + integer_fields = ["max_length", "rows", "min_values", "max_values"] + number_fields = ["min_value", "max_value"] + + for fieldset in form_config.get("fieldsets", []): + for field in fieldset.get("fields", []): + for field_name in integer_fields: + if field_name in field and field[field_name] is not None: + value = field[field_name] + if isinstance(value, str): + try: + field[field_name] = int(value) if value else None + except (ValueError, TypeError): + pass + + for field_name in number_fields: + if field_name in field and field[field_name] is not None: + value = field[field_name] + if isinstance(value, str): + try: + field[field_name] = ( + int(value) if "." not in value else float(value) + ) + except (ValueError, TypeError): + pass + + return form_config + def _validate_field_mappings(self, form_config, cleaned_data): if not self.instance.pk: return diff --git a/src/tests/test_form_config.py b/src/tests/test_form_config.py index 0228107..feede60 100644 --- a/src/tests/test_form_config.py +++ b/src/tests/test_form_config.py @@ -633,3 +633,119 @@ def test_default_value_not_override_existing_instance(): assert form.initial["name"] == "existing-name" assert form.initial["port"] == 3000 + + +def test_form_config_coerces_string_numbers_to_integers(): + form = ServiceDefinitionAdminForm() + schema = form.form_config_schema + + config_with_string_numbers = { + "fieldsets": [ + { + "fields": [ + { + "type": "text", + "label": "Service Name", + "controlplane_field_mapping": "spec.serviceName", + "max_length": "64", # String instead of integer + "required": True, + }, + { + "type": "textarea", + "label": "Description", + "controlplane_field_mapping": "spec.description", + "rows": "5", # String instead of integer + "max_length": "500", # String instead of integer + }, + { + "type": "number", + "label": "Port", + "controlplane_field_mapping": "spec.port", + "min_value": "1", # String instead of integer + "max_value": "65535", # String instead of integer + }, + { + "type": "array", + "label": "Tags", + "controlplane_field_mapping": "spec.tags", + "min_values": "0", # String instead of integer + "max_values": "10", # String instead of integer + }, + ] + } + ] + } + + normalized_config = form._normalize_form_config_types(config_with_string_numbers) + fields = normalized_config["fieldsets"][0]["fields"] + + assert fields[0]["max_length"] == 64 + assert isinstance(fields[0]["max_length"], int) + + assert fields[1]["rows"] == 5 + assert isinstance(fields[1]["rows"], int) + assert fields[1]["max_length"] == 500 + assert isinstance(fields[1]["max_length"], int) + + assert fields[2]["min_value"] == 1 + assert isinstance(fields[2]["min_value"], int) + assert fields[2]["max_value"] == 65535 + assert isinstance(fields[2]["max_value"], int) + + assert fields[3]["min_values"] == 0 + assert isinstance(fields[3]["min_values"], int) + assert fields[3]["max_values"] == 10 + assert isinstance(fields[3]["max_values"], int) + + jsonschema.validate(instance=normalized_config, schema=schema) + + +def test_form_config_handles_float_numbers(): + form = ServiceDefinitionAdminForm() + + config_with_floats = { + "fieldsets": [ + { + "fields": [ + { + "type": "number", + "label": "Price", + "controlplane_field_mapping": "spec.price", + "min_value": "0.01", # String float + "max_value": "999.99", # String float + }, + ] + } + ] + } + + normalized_config = form._normalize_form_config_types(config_with_floats) + field = normalized_config["fieldsets"][0]["fields"][0] + + assert field["min_value"] == 0.01 + assert isinstance(field["min_value"], float) + assert field["max_value"] == 999.99 + assert isinstance(field["max_value"], float) + + +def test_form_config_handles_empty_string_as_none(): + form = ServiceDefinitionAdminForm() + + config_with_empty_strings = { + "fieldsets": [ + { + "fields": [ + { + "type": "text", + "label": "Name", + "controlplane_field_mapping": "name", + "max_length": "", # Empty string + }, + ] + } + ] + } + + normalized_config = form._normalize_form_config_types(config_with_empty_strings) + field = normalized_config["fieldsets"][0]["fields"][0] + assert field["max_length"] is None From 6182b36daf16bcd4f7b4e50696219b3ec1e83494 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Fri, 7 Nov 2025 11:36:28 +0100 Subject: [PATCH 138/185] Implicitly fix admin choices configuration --- src/servala/core/forms.py | 28 +++++- src/tests/test_form_config.py | 155 ++++++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+), 2 deletions(-) diff --git a/src/servala/core/forms.py b/src/servala/core/forms.py index 116e4d9..076a9d9 100644 --- a/src/servala/core/forms.py +++ b/src/servala/core/forms.py @@ -278,6 +278,32 @@ class ServiceDefinitionAdminForm(forms.ModelForm): if not mapping: return + field_name = field.get("label", mapping) + custom_choices = field.get("choices", []) + + # Single-element choices [value] are transformed to [value, value] + for i, choice in enumerate(custom_choices): + if not isinstance(choice, (list, tuple)): + errors.append( + _( + "Field '{}': Choice at index {} must be a list or tuple, " + "but got: {}" + ).format(field_name, i, repr(choice)) + ) + return + + choice_len = len(choice) + if choice_len == 1: + custom_choices[i] = [choice[0], choice[0]] + elif choice_len == 0 or choice_len > 2: + errors.append( + _( + "Field '{}': Choice at index {} must have 1 or 2 elements " + "(got {}): {}" + ).format(field_name, i, choice_len, repr(choice)) + ) + return + field_schema = self._get_field_schema(spec_schema, mapping, prefix) if not field_schema: return @@ -286,7 +312,6 @@ class ServiceDefinitionAdminForm(forms.ModelForm): if not control_plane_choices: return - custom_choices = field.get("choices", []) custom_choice_values = [choice[0] for choice in custom_choices] invalid_choices = [ @@ -296,7 +321,6 @@ class ServiceDefinitionAdminForm(forms.ModelForm): ] if invalid_choices: - field_name = field.get("label", mapping) errors.append( _( "Field '{}' has invalid choice values: {}. " diff --git a/src/tests/test_form_config.py b/src/tests/test_form_config.py index feede60..ba44d72 100644 --- a/src/tests/test_form_config.py +++ b/src/tests/test_form_config.py @@ -749,3 +749,158 @@ def test_form_config_handles_empty_string_as_none(): normalized_config = form._normalize_form_config_types(config_with_empty_strings) field = normalized_config["fieldsets"][0]["fields"][0] assert field["max_length"] is None + + +def test_single_element_choices_are_normalized(): + form = ServiceDefinitionAdminForm() + mock_crd = Mock() + mock_crd.resource_schema = { + "properties": { + "spec": { + "properties": { + "version": { + "type": "string", + "enum": ["6.2", "7.0", "7.2"], + } + } + } + } + } + + config_with_single_choices = { + "fieldsets": [ + { + "fields": [ + { + "type": "text", + "label": "Name", + "controlplane_field_mapping": "name", + }, + { + "type": "choice", + "label": "Version", + "controlplane_field_mapping": "spec.version", + "choices": [["6.2"]], # Single element - should be transformed + }, + ] + } + ] + } + + spec_schema = mock_crd.resource_schema["properties"]["spec"] + errors = [] + + for field in config_with_single_choices["fieldsets"][0]["fields"]: + if field.get("type") == "choice": + form._validate_choice_field( + field, field["controlplane_field_mapping"], spec_schema, "spec", errors + ) + + assert len(errors) == 0, f"Expected no errors but got: {errors}" + version_field = config_with_single_choices["fieldsets"][0]["fields"][1] + assert version_field["choices"] == [["6.2", "6.2"]] + + +def test_two_element_choices_work_correctly(): + form = ServiceDefinitionAdminForm() + mock_crd = Mock() + mock_crd.resource_schema = { + "properties": { + "spec": { + "properties": { + "version": { + "type": "string", + "enum": ["6.2", "7.0"], + } + } + } + } + } + + config_with_proper_choices = { + "fieldsets": [ + { + "fields": [ + { + "type": "choice", + "label": "Version", + "controlplane_field_mapping": "spec.version", + "choices": [["6.2", "Version 6.2"], ["7.0", "Version 7.0"]], + }, + ] + } + ] + } + + spec_schema = mock_crd.resource_schema["properties"]["spec"] + errors = [] + + for field in config_with_proper_choices["fieldsets"][0]["fields"]: + if field.get("type") == "choice": + form._validate_choice_field( + field, field["controlplane_field_mapping"], spec_schema, "spec", errors + ) + + assert len(errors) == 0, f"Expected no errors but got: {errors}" + version_field = config_with_proper_choices["fieldsets"][0]["fields"][0] + assert version_field["choices"] == [["6.2", "Version 6.2"], ["7.0", "Version 7.0"]] + + +def test_empty_choices_fail_validation(): + form = ServiceDefinitionAdminForm() + config_with_empty_choice = { + "fieldsets": [ + { + "fields": [ + { + "type": "choice", + "label": "Version", + "controlplane_field_mapping": "spec.version", + "choices": [[]], # Empty choice - invalid + }, + ] + } + ] + } + + errors = [] + + for field in config_with_empty_choice["fieldsets"][0]["fields"]: + if field.get("type") == "choice": + form._validate_choice_field( + field, field["controlplane_field_mapping"], {}, "spec", errors + ) + + assert len(errors) > 0 + assert "must have 1 or 2 elements" in str(errors[0]) + + +def test_three_plus_element_choices_fail_validation(): + form = ServiceDefinitionAdminForm() + config_with_long_choice = { + "fieldsets": [ + { + "fields": [ + { + "type": "choice", + "label": "Version", + "controlplane_field_mapping": "spec.version", + "choices": [ + ["6.2", "Version 6.2", "Extra"] + ], # 3 elements - invalid + }, + ] + } + ] + } + + errors = [] + + for field in config_with_long_choice["fieldsets"][0]["fields"]: + if field.get("type") == "choice": + form._validate_choice_field( + field, field["controlplane_field_mapping"], {}, "spec", errors + ) + + assert len(errors) > 0 + assert "must have 1 or 2 elements" in str(errors[0]) From 985e4b47c00f2ed9bbe1b456ec7853933acc1608 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Fri, 7 Nov 2025 11:41:08 +0100 Subject: [PATCH 139/185] Make expert mode toggle less prominent --- .../templates/includes/tabbed_fieldset_form.html | 11 +++++------ src/servala/static/js/expert-mode.js | 7 ++++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/servala/frontend/templates/includes/tabbed_fieldset_form.html b/src/servala/frontend/templates/includes/tabbed_fieldset_form.html index d07b4e3..ace05af 100644 --- a/src/servala/frontend/templates/includes/tabbed_fieldset_form.html +++ b/src/servala/frontend/templates/includes/tabbed_fieldset_form.html @@ -7,12 +7,11 @@ {% csrf_token %} {% include "frontend/forms/errors.html" %} {% if form %} -
- + {% endif %}
Date: Sun, 9 Nov 2025 03:01:47 +0000 Subject: [PATCH 140/185] Update dependency pytest to v9 --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 189b20e..ad38e69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ dev = [ "flake8-bugbear>=25.10.21", "flake8-pyproject>=1.2.3", "isort>=7.0.0", - "pytest>=8.4.2", + "pytest>=9.0.0", "pytest-cov>=7.0.0", "pytest-django>=4.11.1", "pytest-mock>=3.15.1", diff --git a/uv.lock b/uv.lock index 2d46a1d..31c062c 100644 --- a/uv.lock +++ b/uv.lock @@ -921,7 +921,7 @@ wheels = [ [[package]] name = "pytest" -version = "8.4.2" +version = "9.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -930,9 +930,9 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/1d/eb34f286b164c5e431a810a38697409cca1112cee04b287bb56ac486730b/pytest-9.0.0.tar.gz", hash = "sha256:8f44522eafe4137b0f35c9ce3072931a788a21ee40a2ed279e817d3cc16ed21e", size = 1562764, upload-time = "2025-11-08T17:25:33.34Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, + { url = "https://files.pythonhosted.org/packages/72/99/cafef234114a3b6d9f3aaed0723b437c40c57bdb7b3e4c3a575bc4890052/pytest-9.0.0-py3-none-any.whl", hash = "sha256:e5ccdf10b0bac554970ee88fc1a4ad0ee5d221f8ef22321f9b7e4584e19d7f96", size = 373364, upload-time = "2025-11-08T17:25:31.811Z" }, ] [[package]] @@ -1327,7 +1327,7 @@ dev = [ { name = "flake8-bugbear", specifier = ">=25.10.21" }, { name = "flake8-pyproject", specifier = ">=1.2.3" }, { name = "isort", specifier = ">=7.0.0" }, - { name = "pytest", specifier = ">=8.4.2" }, + { name = "pytest", specifier = ">=9.0.0" }, { name = "pytest-cov", specifier = ">=7.0.0" }, { name = "pytest-django", specifier = ">=4.11.1" }, { name = "pytest-mock", specifier = ">=3.15.1" }, From 14f813fe25ba0bab3c0fe36015319eab42bf08d8 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 10 Nov 2025 03:01:35 +0000 Subject: [PATCH 141/185] Update dependency coverage to >=7.11.3 --- pyproject.toml | 2 +- uv.lock | 112 ++++++++++++++++++++++++------------------------- 2 files changed, 57 insertions(+), 57 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 189b20e..17d4c03 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ dependencies = [ dev = [ "black>=25.9.0", "bumpver>=2025.1131", - "coverage>=7.11.0", + "coverage>=7.11.3", "djlint>=1.36.4", "flake8>=7.3.0", "flake8-bugbear>=25.10.21", diff --git a/uv.lock b/uv.lock index 2d46a1d..42ceae5 100644 --- a/uv.lock +++ b/uv.lock @@ -258,63 +258,63 @@ wheels = [ [[package]] name = "coverage" -version = "7.11.0" +version = "7.11.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/38/ee22495420457259d2f3390309505ea98f98a5eed40901cf62196abad006/coverage-7.11.0.tar.gz", hash = "sha256:167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050", size = 811905, upload-time = "2025-10-15T15:15:08.542Z" } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/60/7f/85e4dfe65e400645464b25c036a26ac226cf3a69d4a50c3934c532491cdd/coverage-7.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cc3f49e65ea6e0d5d9bd60368684fe52a704d46f9e7fc413918f18d046ec40e1", size = 216129, upload-time = "2025-10-15T15:13:25.371Z" }, - { url = "https://files.pythonhosted.org/packages/96/5d/dc5fa98fea3c175caf9d360649cb1aa3715e391ab00dc78c4c66fabd7356/coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f39ae2f63f37472c17b4990f794035c9890418b1b8cca75c01193f3c8d3e01be", size = 216380, upload-time = "2025-10-15T15:13:26.976Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f5/3da9cc9596708273385189289c0e4d8197d37a386bdf17619013554b3447/coverage-7.11.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7db53b5cdd2917b6eaadd0b1251cf4e7d96f4a8d24e174bdbdf2f65b5ea7994d", size = 247375, upload-time = "2025-10-15T15:13:28.923Z" }, - { url = "https://files.pythonhosted.org/packages/65/6c/f7f59c342359a235559d2bc76b0c73cfc4bac7d61bb0df210965cb1ecffd/coverage-7.11.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10ad04ac3a122048688387828b4537bc9cf60c0bf4869c1e9989c46e45690b82", size = 249978, upload-time = "2025-10-15T15:13:30.525Z" }, - { url = "https://files.pythonhosted.org/packages/e7/8c/042dede2e23525e863bf1ccd2b92689692a148d8b5fd37c37899ba882645/coverage-7.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4036cc9c7983a2b1f2556d574d2eb2154ac6ed55114761685657e38782b23f52", size = 251253, upload-time = "2025-10-15T15:13:32.174Z" }, - { url = "https://files.pythonhosted.org/packages/7b/a9/3c58df67bfa809a7bddd786356d9c5283e45d693edb5f3f55d0986dd905a/coverage-7.11.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ab934dd13b1c5e94b692b1e01bd87e4488cb746e3a50f798cb9464fd128374b", size = 247591, upload-time = "2025-10-15T15:13:34.147Z" }, - { url = "https://files.pythonhosted.org/packages/26/5b/c7f32efd862ee0477a18c41e4761305de6ddd2d49cdeda0c1116227570fd/coverage-7.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59a6e5a265f7cfc05f76e3bb53eca2e0dfe90f05e07e849930fecd6abb8f40b4", size = 249411, upload-time = "2025-10-15T15:13:38.425Z" }, - { url = "https://files.pythonhosted.org/packages/76/b5/78cb4f1e86c1611431c990423ec0768122905b03837e1b4c6a6f388a858b/coverage-7.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:df01d6c4c81e15a7c88337b795bb7595a8596e92310266b5072c7e301168efbd", size = 247303, upload-time = "2025-10-15T15:13:40.464Z" }, - { url = "https://files.pythonhosted.org/packages/87/c9/23c753a8641a330f45f221286e707c427e46d0ffd1719b080cedc984ec40/coverage-7.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8c934bd088eed6174210942761e38ee81d28c46de0132ebb1801dbe36a390dcc", size = 247157, upload-time = "2025-10-15T15:13:42.087Z" }, - { url = "https://files.pythonhosted.org/packages/c5/42/6e0cc71dc8a464486e944a4fa0d85bdec031cc2969e98ed41532a98336b9/coverage-7.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a03eaf7ec24078ad64a07f02e30060aaf22b91dedf31a6b24d0d98d2bba7f48", size = 248921, upload-time = "2025-10-15T15:13:43.715Z" }, - { url = "https://files.pythonhosted.org/packages/e8/1c/743c2ef665e6858cccb0f84377dfe3a4c25add51e8c7ef19249be92465b6/coverage-7.11.0-cp313-cp313-win32.whl", hash = "sha256:695340f698a5f56f795b2836abe6fb576e7c53d48cd155ad2f80fd24bc63a040", size = 218526, upload-time = "2025-10-15T15:13:45.336Z" }, - { url = "https://files.pythonhosted.org/packages/ff/d5/226daadfd1bf8ddbccefbd3aa3547d7b960fb48e1bdac124e2dd13a2b71a/coverage-7.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:2727d47fce3ee2bac648528e41455d1b0c46395a087a229deac75e9f88ba5a05", size = 219317, upload-time = "2025-10-15T15:13:47.401Z" }, - { url = "https://files.pythonhosted.org/packages/97/54/47db81dcbe571a48a298f206183ba8a7ba79200a37cd0d9f4788fcd2af4a/coverage-7.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:0efa742f431529699712b92ecdf22de8ff198df41e43aeaaadf69973eb93f17a", size = 217948, upload-time = "2025-10-15T15:13:49.096Z" }, - { url = "https://files.pythonhosted.org/packages/e5/8b/cb68425420154e7e2a82fd779a8cc01549b6fa83c2ad3679cd6c088ebd07/coverage-7.11.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:587c38849b853b157706407e9ebdca8fd12f45869edb56defbef2daa5fb0812b", size = 216837, upload-time = "2025-10-15T15:13:51.09Z" }, - { url = "https://files.pythonhosted.org/packages/33/55/9d61b5765a025685e14659c8d07037247de6383c0385757544ffe4606475/coverage-7.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b971bdefdd75096163dd4261c74be813c4508477e39ff7b92191dea19f24cd37", size = 217061, upload-time = "2025-10-15T15:13:52.747Z" }, - { url = "https://files.pythonhosted.org/packages/52/85/292459c9186d70dcec6538f06ea251bc968046922497377bf4a1dc9a71de/coverage-7.11.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:269bfe913b7d5be12ab13a95f3a76da23cf147be7fa043933320ba5625f0a8de", size = 258398, upload-time = "2025-10-15T15:13:54.45Z" }, - { url = "https://files.pythonhosted.org/packages/1f/e2/46edd73fb8bf51446c41148d81944c54ed224854812b6ca549be25113ee0/coverage-7.11.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dadbcce51a10c07b7c72b0ce4a25e4b6dcb0c0372846afb8e5b6307a121eb99f", size = 260574, upload-time = "2025-10-15T15:13:56.145Z" }, - { url = "https://files.pythonhosted.org/packages/07/5e/1df469a19007ff82e2ca8fe509822820a31e251f80ee7344c34f6cd2ec43/coverage-7.11.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ed43fa22c6436f7957df036331f8fe4efa7af132054e1844918866cd228af6c", size = 262797, upload-time = "2025-10-15T15:13:58.635Z" }, - { url = "https://files.pythonhosted.org/packages/f9/50/de216b31a1434b94d9b34a964c09943c6be45069ec704bfc379d8d89a649/coverage-7.11.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9516add7256b6713ec08359b7b05aeff8850c98d357784c7205b2e60aa2513fa", size = 257361, upload-time = "2025-10-15T15:14:00.409Z" }, - { url = "https://files.pythonhosted.org/packages/82/1e/3f9f8344a48111e152e0fd495b6fff13cc743e771a6050abf1627a7ba918/coverage-7.11.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb92e47c92fcbcdc692f428da67db33337fa213756f7adb6a011f7b5a7a20740", size = 260349, upload-time = "2025-10-15T15:14:02.188Z" }, - { url = "https://files.pythonhosted.org/packages/65/9b/3f52741f9e7d82124272f3070bbe316006a7de1bad1093f88d59bfc6c548/coverage-7.11.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d06f4fc7acf3cabd6d74941d53329e06bab00a8fe10e4df2714f0b134bfc64ef", size = 258114, upload-time = "2025-10-15T15:14:03.907Z" }, - { url = "https://files.pythonhosted.org/packages/0b/8b/918f0e15f0365d50d3986bbd3338ca01178717ac5678301f3f547b6619e6/coverage-7.11.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:6fbcee1a8f056af07ecd344482f711f563a9eb1c2cad192e87df00338ec3cdb0", size = 256723, upload-time = "2025-10-15T15:14:06.324Z" }, - { url = "https://files.pythonhosted.org/packages/44/9e/7776829f82d3cf630878a7965a7d70cc6ca94f22c7d20ec4944f7148cb46/coverage-7.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dbbf012be5f32533a490709ad597ad8a8ff80c582a95adc8d62af664e532f9ca", size = 259238, upload-time = "2025-10-15T15:14:08.002Z" }, - { url = "https://files.pythonhosted.org/packages/9a/b8/49cf253e1e7a3bedb85199b201862dd7ca4859f75b6cf25ffa7298aa0760/coverage-7.11.0-cp313-cp313t-win32.whl", hash = "sha256:cee6291bb4fed184f1c2b663606a115c743df98a537c969c3c64b49989da96c2", size = 219180, upload-time = "2025-10-15T15:14:09.786Z" }, - { url = "https://files.pythonhosted.org/packages/ac/e1/1a541703826be7ae2125a0fb7f821af5729d56bb71e946e7b933cc7a89a4/coverage-7.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a386c1061bf98e7ea4758e4313c0ab5ecf57af341ef0f43a0bf26c2477b5c268", size = 220241, upload-time = "2025-10-15T15:14:11.471Z" }, - { url = "https://files.pythonhosted.org/packages/d5/d1/5ee0e0a08621140fd418ec4020f595b4d52d7eb429ae6a0c6542b4ba6f14/coverage-7.11.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f9ea02ef40bb83823b2b04964459d281688fe173e20643870bb5d2edf68bc836", size = 218510, upload-time = "2025-10-15T15:14:13.46Z" }, - { url = "https://files.pythonhosted.org/packages/f4/06/e923830c1985ce808e40a3fa3eb46c13350b3224b7da59757d37b6ce12b8/coverage-7.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c770885b28fb399aaf2a65bbd1c12bf6f307ffd112d6a76c5231a94276f0c497", size = 216110, upload-time = "2025-10-15T15:14:15.157Z" }, - { url = "https://files.pythonhosted.org/packages/42/82/cdeed03bfead45203fb651ed756dfb5266028f5f939e7f06efac4041dad5/coverage-7.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3d0e2087dba64c86a6b254f43e12d264b636a39e88c5cc0a01a7c71bcfdab7e", size = 216395, upload-time = "2025-10-15T15:14:16.863Z" }, - { url = "https://files.pythonhosted.org/packages/fc/ba/e1c80caffc3199aa699813f73ff097bc2df7b31642bdbc7493600a8f1de5/coverage-7.11.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73feb83bb41c32811973b8565f3705caf01d928d972b72042b44e97c71fd70d1", size = 247433, upload-time = "2025-10-15T15:14:18.589Z" }, - { url = "https://files.pythonhosted.org/packages/80/c0/5b259b029694ce0a5bbc1548834c7ba3db41d3efd3474489d7efce4ceb18/coverage-7.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c6f31f281012235ad08f9a560976cc2fc9c95c17604ff3ab20120fe480169bca", size = 249970, upload-time = "2025-10-15T15:14:20.307Z" }, - { url = "https://files.pythonhosted.org/packages/8c/86/171b2b5e1aac7e2fd9b43f7158b987dbeb95f06d1fbecad54ad8163ae3e8/coverage-7.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9570ad567f880ef675673992222746a124b9595506826b210fbe0ce3f0499cd", size = 251324, upload-time = "2025-10-15T15:14:22.419Z" }, - { url = "https://files.pythonhosted.org/packages/1a/7e/7e10414d343385b92024af3932a27a1caf75c6e27ee88ba211221ff1a145/coverage-7.11.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8badf70446042553a773547a61fecaa734b55dc738cacf20c56ab04b77425e43", size = 247445, upload-time = "2025-10-15T15:14:24.205Z" }, - { url = "https://files.pythonhosted.org/packages/c4/3b/e4f966b21f5be8c4bf86ad75ae94efa0de4c99c7bbb8114476323102e345/coverage-7.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a09c1211959903a479e389685b7feb8a17f59ec5a4ef9afde7650bd5eabc2777", size = 249324, upload-time = "2025-10-15T15:14:26.234Z" }, - { url = "https://files.pythonhosted.org/packages/00/a2/8479325576dfcd909244d0df215f077f47437ab852ab778cfa2f8bf4d954/coverage-7.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:5ef83b107f50db3f9ae40f69e34b3bd9337456c5a7fe3461c7abf8b75dd666a2", size = 247261, upload-time = "2025-10-15T15:14:28.42Z" }, - { url = "https://files.pythonhosted.org/packages/7b/d8/3a9e2db19d94d65771d0f2e21a9ea587d11b831332a73622f901157cc24b/coverage-7.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f91f927a3215b8907e214af77200250bb6aae36eca3f760f89780d13e495388d", size = 247092, upload-time = "2025-10-15T15:14:30.784Z" }, - { url = "https://files.pythonhosted.org/packages/b3/b1/bbca3c472544f9e2ad2d5116b2379732957048be4b93a9c543fcd0207e5f/coverage-7.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbcd376716d6b7fbfeedd687a6c4be019c5a5671b35f804ba76a4c0a778cba4", size = 248755, upload-time = "2025-10-15T15:14:32.585Z" }, - { url = "https://files.pythonhosted.org/packages/89/49/638d5a45a6a0f00af53d6b637c87007eb2297042186334e9923a61aa8854/coverage-7.11.0-cp314-cp314-win32.whl", hash = "sha256:bab7ec4bb501743edc63609320aaec8cd9188b396354f482f4de4d40a9d10721", size = 218793, upload-time = "2025-10-15T15:14:34.972Z" }, - { url = "https://files.pythonhosted.org/packages/30/cc/b675a51f2d068adb3cdf3799212c662239b0ca27f4691d1fff81b92ea850/coverage-7.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d4ba9a449e9364a936a27322b20d32d8b166553bfe63059bd21527e681e2fad", size = 219587, upload-time = "2025-10-15T15:14:37.047Z" }, - { url = "https://files.pythonhosted.org/packages/93/98/5ac886876026de04f00820e5094fe22166b98dcb8b426bf6827aaf67048c/coverage-7.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:ce37f215223af94ef0f75ac68ea096f9f8e8c8ec7d6e8c346ee45c0d363f0479", size = 218168, upload-time = "2025-10-15T15:14:38.861Z" }, - { url = "https://files.pythonhosted.org/packages/14/d1/b4145d35b3e3ecf4d917e97fc8895bcf027d854879ba401d9ff0f533f997/coverage-7.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f413ce6e07e0d0dc9c433228727b619871532674b45165abafe201f200cc215f", size = 216850, upload-time = "2025-10-15T15:14:40.651Z" }, - { url = "https://files.pythonhosted.org/packages/ca/d1/7f645fc2eccd318369a8a9948acc447bb7c1ade2911e31d3c5620544c22b/coverage-7.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05791e528a18f7072bf5998ba772fe29db4da1234c45c2087866b5ba4dea710e", size = 217071, upload-time = "2025-10-15T15:14:42.755Z" }, - { url = "https://files.pythonhosted.org/packages/54/7d/64d124649db2737ceced1dfcbdcb79898d5868d311730f622f8ecae84250/coverage-7.11.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cacb29f420cfeb9283b803263c3b9a068924474ff19ca126ba9103e1278dfa44", size = 258570, upload-time = "2025-10-15T15:14:44.542Z" }, - { url = "https://files.pythonhosted.org/packages/6c/3f/6f5922f80dc6f2d8b2c6f974835c43f53eb4257a7797727e6ca5b7b2ec1f/coverage-7.11.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314c24e700d7027ae3ab0d95fbf8d53544fca1f20345fd30cd219b737c6e58d3", size = 260738, upload-time = "2025-10-15T15:14:46.436Z" }, - { url = "https://files.pythonhosted.org/packages/0e/5f/9e883523c4647c860b3812b417a2017e361eca5b635ee658387dc11b13c1/coverage-7.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:630d0bd7a293ad2fc8b4b94e5758c8b2536fdf36c05f1681270203e463cbfa9b", size = 262994, upload-time = "2025-10-15T15:14:48.3Z" }, - { url = "https://files.pythonhosted.org/packages/07/bb/43b5a8e94c09c8bf51743ffc65c4c841a4ca5d3ed191d0a6919c379a1b83/coverage-7.11.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e89641f5175d65e2dbb44db15fe4ea48fade5d5bbb9868fdc2b4fce22f4a469d", size = 257282, upload-time = "2025-10-15T15:14:50.236Z" }, - { url = "https://files.pythonhosted.org/packages/aa/e5/0ead8af411411330b928733e1d201384b39251a5f043c1612970310e8283/coverage-7.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c9f08ea03114a637dab06cedb2e914da9dc67fa52c6015c018ff43fdde25b9c2", size = 260430, upload-time = "2025-10-15T15:14:52.413Z" }, - { url = "https://files.pythonhosted.org/packages/ae/66/03dd8bb0ba5b971620dcaac145461950f6d8204953e535d2b20c6b65d729/coverage-7.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce9f3bde4e9b031eaf1eb61df95c1401427029ea1bfddb8621c1161dcb0fa02e", size = 258190, upload-time = "2025-10-15T15:14:54.268Z" }, - { url = "https://files.pythonhosted.org/packages/45/ae/28a9cce40bf3174426cb2f7e71ee172d98e7f6446dff936a7ccecee34b14/coverage-7.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:e4dc07e95495923d6fd4d6c27bf70769425b71c89053083843fd78f378558996", size = 256658, upload-time = "2025-10-15T15:14:56.436Z" }, - { url = "https://files.pythonhosted.org/packages/5c/7c/3a44234a8599513684bfc8684878fd7b126c2760f79712bb78c56f19efc4/coverage-7.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:424538266794db2861db4922b05d729ade0940ee69dcf0591ce8f69784db0e11", size = 259342, upload-time = "2025-10-15T15:14:58.538Z" }, - { url = "https://files.pythonhosted.org/packages/e1/e6/0108519cba871af0351725ebdb8660fd7a0fe2ba3850d56d32490c7d9b4b/coverage-7.11.0-cp314-cp314t-win32.whl", hash = "sha256:4c1eeb3fb8eb9e0190bebafd0462936f75717687117339f708f395fe455acc73", size = 219568, upload-time = "2025-10-15T15:15:00.382Z" }, - { url = "https://files.pythonhosted.org/packages/c9/76/44ba876e0942b4e62fdde23ccb029ddb16d19ba1bef081edd00857ba0b16/coverage-7.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b56efee146c98dbf2cf5cffc61b9829d1e94442df4d7398b26892a53992d3547", size = 220687, upload-time = "2025-10-15T15:15:02.322Z" }, - { url = "https://files.pythonhosted.org/packages/b9/0c/0df55ecb20d0d0ed5c322e10a441775e1a3a5d78c60f0c4e1abfe6fcf949/coverage-7.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b5c2705afa83f49bd91962a4094b6b082f94aef7626365ab3f8f4bd159c5acf3", size = 218711, upload-time = "2025-10-15T15:15:04.575Z" }, - { url = "https://files.pythonhosted.org/packages/5f/04/642c1d8a448ae5ea1369eac8495740a79eb4e581a9fb0cbdce56bbf56da1/coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68", size = 207761, upload-time = "2025-10-15T15:15:06.439Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f6/d8572c058211c7d976f24dab71999a565501fb5b3cdcb59cf782f19c4acb/coverage-7.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84b892e968164b7a0498ddc5746cdf4e985700b902128421bb5cec1080a6ee36", size = 216694, upload-time = "2025-11-10T00:11:34.296Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f6/b6f9764d90c0ce1bce8d995649fa307fff21f4727b8d950fa2843b7b0de5/coverage-7.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f761dbcf45e9416ec4698e1a7649248005f0064ce3523a47402d1bff4af2779e", size = 217065, upload-time = "2025-11-10T00:11:36.281Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8d/a12cb424063019fd077b5be474258a0ed8369b92b6d0058e673f0a945982/coverage-7.11.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1410bac9e98afd9623f53876fae7d8a5db9f5a0ac1c9e7c5188463cb4b3212e2", size = 248062, upload-time = "2025-11-10T00:11:37.903Z" }, + { url = "https://files.pythonhosted.org/packages/7f/9c/dab1a4e8e75ce053d14259d3d7485d68528a662e286e184685ea49e71156/coverage-7.11.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:004cdcea3457c0ea3233622cd3464c1e32ebba9b41578421097402bee6461b63", size = 250657, upload-time = "2025-11-10T00:11:39.509Z" }, + { url = "https://files.pythonhosted.org/packages/3f/89/a14f256438324f33bae36f9a1a7137729bf26b0a43f5eda60b147ec7c8c7/coverage-7.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f067ada2c333609b52835ca4d4868645d3b63ac04fb2b9a658c55bba7f667d3", size = 251900, upload-time = "2025-11-10T00:11:41.372Z" }, + { url = "https://files.pythonhosted.org/packages/04/07/75b0d476eb349f1296486b1418b44f2d8780cc8db47493de3755e5340076/coverage-7.11.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:07bc7745c945a6d95676953e86ba7cebb9f11de7773951c387f4c07dc76d03f5", size = 248254, upload-time = "2025-11-10T00:11:43.27Z" }, + { url = "https://files.pythonhosted.org/packages/5a/4b/0c486581fa72873489ca092c52792d008a17954aa352809a7cbe6cf0bf07/coverage-7.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bba7e4743e37484ae17d5c3b8eb1ce78b564cb91b7ace2e2182b25f0f764cb5", size = 250041, upload-time = "2025-11-10T00:11:45.274Z" }, + { url = "https://files.pythonhosted.org/packages/af/a3/0059dafb240ae3e3291f81b8de00e9c511d3dd41d687a227dd4b529be591/coverage-7.11.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbffc22d80d86fbe456af9abb17f7a7766e7b2101f7edaacc3535501691563f7", size = 248004, upload-time = "2025-11-10T00:11:46.93Z" }, + { url = "https://files.pythonhosted.org/packages/83/93/967d9662b1eb8c7c46917dcc7e4c1875724ac3e73c3cb78e86d7a0ac719d/coverage-7.11.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0dba4da36730e384669e05b765a2c49f39514dd3012fcc0398dd66fba8d746d5", size = 247828, upload-time = "2025-11-10T00:11:48.563Z" }, + { url = "https://files.pythonhosted.org/packages/4c/1c/5077493c03215701e212767e470b794548d817dfc6247a4718832cc71fac/coverage-7.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ae12fe90b00b71a71b69f513773310782ce01d5f58d2ceb2b7c595ab9d222094", size = 249588, upload-time = "2025-11-10T00:11:50.581Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a5/77f64de461016e7da3e05d7d07975c89756fe672753e4cf74417fc9b9052/coverage-7.11.3-cp313-cp313-win32.whl", hash = "sha256:12d821de7408292530b0d241468b698bce18dd12ecaf45316149f53877885f8c", size = 219223, upload-time = "2025-11-10T00:11:52.184Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1c/ec51a3c1a59d225b44bdd3a4d463135b3159a535c2686fac965b698524f4/coverage-7.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:6bb599052a974bb6cedfa114f9778fedfad66854107cf81397ec87cb9b8fbcf2", size = 220033, upload-time = "2025-11-10T00:11:53.871Z" }, + { url = "https://files.pythonhosted.org/packages/01/ec/e0ce39746ed558564c16f2cc25fa95ce6fc9fa8bfb3b9e62855d4386b886/coverage-7.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:bb9d7efdb063903b3fdf77caec7b77c3066885068bdc0d44bc1b0c171033f944", size = 218661, upload-time = "2025-11-10T00:11:55.597Z" }, + { url = "https://files.pythonhosted.org/packages/46/cb/483f130bc56cbbad2638248915d97b185374d58b19e3cc3107359715949f/coverage-7.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:fb58da65e3339b3dbe266b607bb936efb983d86b00b03eb04c4ad5b442c58428", size = 217389, upload-time = "2025-11-10T00:11:57.59Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ae/81f89bae3afef75553cf10e62feb57551535d16fd5859b9ee5a2a97ddd27/coverage-7.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8d16bbe566e16a71d123cd66382c1315fcd520c7573652a8074a8fe281b38c6a", size = 217742, upload-time = "2025-11-10T00:11:59.519Z" }, + { url = "https://files.pythonhosted.org/packages/db/6e/a0fb897041949888191a49c36afd5c6f5d9f5fd757e0b0cd99ec198a324b/coverage-7.11.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8258f10059b5ac837232c589a350a2df4a96406d6d5f2a09ec587cbdd539655", size = 259049, upload-time = "2025-11-10T00:12:01.592Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/d13acc67eb402d91eb94b9bd60593411799aed09ce176ee8d8c0e39c94ca/coverage-7.11.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4c5627429f7fbff4f4131cfdd6abd530734ef7761116811a707b88b7e205afd7", size = 261113, upload-time = "2025-11-10T00:12:03.639Z" }, + { url = "https://files.pythonhosted.org/packages/ea/07/a6868893c48191d60406df4356aa7f0f74e6de34ef1f03af0d49183e0fa1/coverage-7.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:465695268414e149bab754c54b0c45c8ceda73dd4a5c3ba255500da13984b16d", size = 263546, upload-time = "2025-11-10T00:12:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/24/e5/28598f70b2c1098332bac47925806353b3313511d984841111e6e760c016/coverage-7.11.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4ebcddfcdfb4c614233cff6e9a3967a09484114a8b2e4f2c7a62dc83676ba13f", size = 258260, upload-time = "2025-11-10T00:12:07.137Z" }, + { url = "https://files.pythonhosted.org/packages/0e/58/58e2d9e6455a4ed746a480c4b9cf96dc3cb2a6b8f3efbee5efd33ae24b06/coverage-7.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:13b2066303a1c1833c654d2af0455bb009b6e1727b3883c9964bc5c2f643c1d0", size = 261121, upload-time = "2025-11-10T00:12:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/17/57/38803eefb9b0409934cbc5a14e3978f0c85cb251d2b6f6a369067a7105a0/coverage-7.11.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d8750dd20362a1b80e3cf84f58013d4672f89663aee457ea59336df50fab6739", size = 258736, upload-time = "2025-11-10T00:12:11.195Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/f94683167156e93677b3442be1d4ca70cb33718df32a2eea44a5898f04f6/coverage-7.11.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ab6212e62ea0e1006531a2234e209607f360d98d18d532c2fa8e403c1afbdd71", size = 257625, upload-time = "2025-11-10T00:12:12.843Z" }, + { url = "https://files.pythonhosted.org/packages/87/ed/42d0bf1bc6bfa7d65f52299a31daaa866b4c11000855d753857fe78260ac/coverage-7.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a6b17c2b5e0b9bb7702449200f93e2d04cb04b1414c41424c08aa1e5d352da76", size = 259827, upload-time = "2025-11-10T00:12:15.128Z" }, + { url = "https://files.pythonhosted.org/packages/d3/76/5682719f5d5fbedb0c624c9851ef847407cae23362deb941f185f489c54e/coverage-7.11.3-cp313-cp313t-win32.whl", hash = "sha256:426559f105f644b69290ea414e154a0d320c3ad8a2bb75e62884731f69cf8e2c", size = 219897, upload-time = "2025-11-10T00:12:17.274Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/1da511d0ac3d39e6676fa6cc5ec35320bbf1cebb9b24e9ee7548ee4e931a/coverage-7.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:90a96fcd824564eae6137ec2563bd061d49a32944858d4bdbae5c00fb10e76ac", size = 220959, upload-time = "2025-11-10T00:12:19.292Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9d/e255da6a04e9ec5f7b633c54c0fdfa221a9e03550b67a9c83217de12e96c/coverage-7.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:1e33d0bebf895c7a0905fcfaff2b07ab900885fc78bba2a12291a2cfbab014cc", size = 219234, upload-time = "2025-11-10T00:12:21.251Z" }, + { 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" }, ] [[package]] @@ -1321,7 +1321,7 @@ requires-dist = [ dev = [ { name = "black", specifier = ">=25.9.0" }, { name = "bumpver", specifier = ">=2025.1131" }, - { name = "coverage", specifier = ">=7.11.0" }, + { name = "coverage", specifier = ">=7.11.3" }, { name = "djlint", specifier = ">=1.36.4" }, { name = "flake8", specifier = ">=7.3.0" }, { name = "flake8-bugbear", specifier = ">=25.10.21" }, From eb8176d446214c31e0a5d5119b9188c032c848ec Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 10 Nov 2025 03:01:40 +0000 Subject: [PATCH 142/185] Update dependency black to >=25.11.0 --- pyproject.toml | 2 +- uv.lock | 26 +++++++++++++++----------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 189b20e..445bfb8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ [dependency-groups] dev = [ - "black>=25.9.0", + "black>=25.11.0", "bumpver>=2025.1131", "coverage>=7.11.0", "djlint>=1.36.4", diff --git a/uv.lock b/uv.lock index 2d46a1d..f4339cd 100644 --- a/uv.lock +++ b/uv.lock @@ -69,7 +69,7 @@ wheels = [ [[package]] name = "black" -version = "25.9.0" +version = "25.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -79,13 +79,17 @@ dependencies = [ { name = "platformdirs" }, { name = "pytokens" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4b/43/20b5c90612d7bdb2bdbcceeb53d588acca3bb8f0e4c5d5c751a2c8fdd55a/black-25.9.0.tar.gz", hash = "sha256:0474bca9a0dd1b51791fcc507a4e02078a1c63f6d4e4ae5544b9848c7adfb619", size = 648393, upload-time = "2025-09-19T00:27:37.758Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/ad/33adf4708633d047950ff2dfdea2e215d84ac50ef95aff14a614e4b6e9b2/black-25.11.0.tar.gz", hash = "sha256:9a323ac32f5dc75ce7470501b887250be5005a01602e931a15e45593f70f6e08", size = 655669, upload-time = "2025-11-10T01:53:50.558Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/99/3acfea65f5e79f45472c45f87ec13037b506522719cd9d4ac86484ff51ac/black-25.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0172a012f725b792c358d57fe7b6b6e8e67375dd157f64fa7a3097b3ed3e2175", size = 1742165, upload-time = "2025-09-19T00:34:10.402Z" }, - { url = "https://files.pythonhosted.org/packages/3a/18/799285282c8236a79f25d590f0222dbd6850e14b060dfaa3e720241fd772/black-25.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3bec74ee60f8dfef564b573a96b8930f7b6a538e846123d5ad77ba14a8d7a64f", size = 1581259, upload-time = "2025-09-19T00:32:49.685Z" }, - { url = "https://files.pythonhosted.org/packages/f1/ce/883ec4b6303acdeca93ee06b7622f1fa383c6b3765294824165d49b1a86b/black-25.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b756fc75871cb1bcac5499552d771822fd9db5a2bb8db2a7247936ca48f39831", size = 1655583, upload-time = "2025-09-19T00:30:44.505Z" }, - { url = "https://files.pythonhosted.org/packages/21/17/5c253aa80a0639ccc427a5c7144534b661505ae2b5a10b77ebe13fa25334/black-25.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:846d58e3ce7879ec1ffe816bb9df6d006cd9590515ed5d17db14e17666b2b357", size = 1343428, upload-time = "2025-09-19T00:32:13.839Z" }, - { url = "https://files.pythonhosted.org/packages/1b/46/863c90dcd3f9d41b109b7f19032ae0db021f0b2a81482ba0a1e28c84de86/black-25.9.0-py3-none-any.whl", hash = "sha256:474b34c1342cdc157d307b56c4c65bce916480c4a8f6551fdc6bf9b486a7c4ae", size = 203363, upload-time = "2025-09-19T00:27:35.724Z" }, + { url = "https://files.pythonhosted.org/packages/ad/47/3378d6a2ddefe18553d1115e36aea98f4a90de53b6a3017ed861ba1bd3bc/black-25.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a1d40348b6621cc20d3d7530a5b8d67e9714906dfd7346338249ad9c6cedf2b", size = 1772446, upload-time = "2025-11-10T02:02:16.181Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4b/0f00bfb3d1f7e05e25bfc7c363f54dc523bb6ba502f98f4ad3acf01ab2e4/black-25.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51c65d7d60bb25429ea2bf0731c32b2a2442eb4bd3b2afcb47830f0b13e58bfd", size = 1607983, upload-time = "2025-11-10T02:02:52.502Z" }, + { url = "https://files.pythonhosted.org/packages/99/fe/49b0768f8c9ae57eb74cc10a1f87b4c70453551d8ad498959721cc345cb7/black-25.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:936c4dd07669269f40b497440159a221ee435e3fddcf668e0c05244a9be71993", size = 1682481, upload-time = "2025-11-10T01:57:12.35Z" }, + { url = "https://files.pythonhosted.org/packages/55/17/7e10ff1267bfa950cc16f0a411d457cdff79678fbb77a6c73b73a5317904/black-25.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:f42c0ea7f59994490f4dccd64e6b2dd49ac57c7c84f38b8faab50f8759db245c", size = 1363869, upload-time = "2025-11-10T01:58:24.608Z" }, + { url = "https://files.pythonhosted.org/packages/67/c0/cc865ce594d09e4cd4dfca5e11994ebb51604328489f3ca3ae7bb38a7db5/black-25.11.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:35690a383f22dd3e468c85dc4b915217f87667ad9cce781d7b42678ce63c4170", size = 1771358, upload-time = "2025-11-10T02:03:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/37/77/4297114d9e2fd2fc8ab0ab87192643cd49409eb059e2940391e7d2340e57/black-25.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dae49ef7369c6caa1a1833fd5efb7c3024bb7e4499bf64833f65ad27791b1545", size = 1612902, upload-time = "2025-11-10T01:59:33.382Z" }, + { url = "https://files.pythonhosted.org/packages/de/63/d45ef97ada84111e330b2b2d45e1dd163e90bd116f00ac55927fb6bf8adb/black-25.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bd4a22a0b37401c8e492e994bce79e614f91b14d9ea911f44f36e262195fdda", size = 1680571, upload-time = "2025-11-10T01:57:04.239Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4b/5604710d61cdff613584028b4cb4607e56e148801ed9b38ee7970799dab6/black-25.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:aa211411e94fdf86519996b7f5f05e71ba34835d8f0c0f03c00a26271da02664", size = 1382599, upload-time = "2025-11-10T01:57:57.427Z" }, + { url = "https://files.pythonhosted.org/packages/00/5d/aed32636ed30a6e7f9efd6ad14e2a0b0d687ae7c8c7ec4e4a557174b895c/black-25.11.0-py3-none-any.whl", hash = "sha256:e3f562da087791e96cefcd9dda058380a442ab322a02e222add53736451f604b", size = 204918, upload-time = "2025-11-10T01:53:48.917Z" }, ] [[package]] @@ -987,11 +991,11 @@ wheels = [ [[package]] name = "pytokens" -version = "0.2.0" +version = "0.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d4/c2/dbadcdddb412a267585459142bfd7cc241e6276db69339353ae6e241ab2b/pytokens-0.2.0.tar.gz", hash = "sha256:532d6421364e5869ea57a9523bf385f02586d4662acbcc0342afd69511b4dd43", size = 15368, upload-time = "2025-10-15T08:02:42.738Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/8d/a762be14dae1c3bf280202ba3172020b2b0b4c537f94427435f19c413b72/pytokens-0.3.0.tar.gz", hash = "sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a", size = 17644, upload-time = "2025-11-05T13:36:35.34Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/5a/c269ea6b348b6f2c32686635df89f32dbe05df1088dd4579302a6f8f99af/pytokens-0.2.0-py3-none-any.whl", hash = "sha256:74d4b318c67f4295c13782ddd9abcb7e297ec5630ad060eb90abf7ebbefe59f8", size = 12038, upload-time = "2025-10-15T08:02:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/84/25/d9db8be44e205a124f6c98bc0324b2bb149b7431c53877fc6d1038dddaf5/pytokens-0.3.0-py3-none-any.whl", hash = "sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3", size = 12195, upload-time = "2025-11-05T13:36:33.183Z" }, ] [[package]] @@ -1319,7 +1323,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ - { name = "black", specifier = ">=25.9.0" }, + { name = "black", specifier = ">=25.11.0" }, { name = "bumpver", specifier = ">=2025.1131" }, { name = "coverage", specifier = ">=7.11.0" }, { name = "djlint", specifier = ">=1.36.4" }, From 0806523f1837769df6c42abd22259065e5872283 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 10 Nov 2025 03:01:51 +0000 Subject: [PATCH 143/185] Update https://github.com/renovatebot/github-action action to v44 --- .forgejo/workflows/renovate.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/renovate.yaml b/.forgejo/workflows/renovate.yaml index 851664f..1a0bbe9 100644 --- a/.forgejo/workflows/renovate.yaml +++ b/.forgejo/workflows/renovate.yaml @@ -19,7 +19,7 @@ jobs: node-version: "24" - name: Renovate - uses: https://github.com/renovatebot/github-action@v43.0.20 + uses: https://github.com/renovatebot/github-action@v44.0.1 with: token: ${{ secrets.RENOVATE_TOKEN }} env: From 228ab9bc0d8fa1642d038c781d1f74008b13ced4 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Mon, 10 Nov 2025 13:36:09 +0100 Subject: [PATCH 144/185] Make fqdn generator fail silently --- src/servala/static/js/fqdn.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/servala/static/js/fqdn.js b/src/servala/static/js/fqdn.js index 178c586..ec70dad 100644 --- a/src/servala/static/js/fqdn.js +++ b/src/servala/static/js/fqdn.js @@ -1,7 +1,9 @@ const initializeFqdnGeneration = (prefix) => { const nameField = document.querySelector(`input#id_${prefix}-name`); - const fqdnField = document.getElementById(`${prefix}-spec.parameters.service.fqdn_container`).querySelector('input.array-item-input'); + const fqdnFieldContainer = document.getElementById(`${prefix}-spec.parameters.service.fqdn_container`) + if (!nameField || !fqdnFieldContainer) return + const fqdnField = fqdnFieldContainer.querySelector('input.array-item-input'); if (nameField && fqdnField) { const generateFqdn = (instanceName) => { From c7c22aa265b7e234e0799816d98c78f79e86911b Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Mon, 10 Nov 2025 14:02:34 +0100 Subject: [PATCH 145/185] Add default config --- src/servala/core/crd/forms.py | 33 ++++++- src/servala/core/forms.py | 36 +++++-- src/tests/test_form_config.py | 180 ++++++++++++++++++++++++++++++++++ 3 files changed, 238 insertions(+), 11 deletions(-) diff --git a/src/servala/core/crd/forms.py b/src/servala/core/crd/forms.py index 99e2737..bdb8ed6 100644 --- a/src/servala/core/crd/forms.py +++ b/src/servala/core/crd/forms.py @@ -6,6 +6,27 @@ from servala.core.crd.utils import deslugify from servala.core.models import ControlPlaneCRD from servala.frontend.forms.widgets import DynamicArrayWidget +# Fields that must be present in every form +MANDATORY_FIELDS = ["name"] + +# Default field configurations - fields that can be included with just a mapping +# to avoid administrators having to duplicate common information +DEFAULT_FIELD_CONFIGS = { + "name": { + "type": "text", + "label": "Instance Name", + "help_text": "Unique name for the new instance", + "required": True, + "max_length": 63, + }, + "spec.parameters.service.fqdn": { + "type": "array", + "label": "FQDNs", + "help_text": "Domain names for accessing this service", + "required": False, + }, +} + class FormGeneratorMixin: """Shared base class for ModelForm classes based on our generated CRD models. @@ -278,12 +299,20 @@ class CustomFormMixin(FormGeneratorMixin): def _apply_field_config(self): for fieldset in self.form_config.get("fieldsets", []): - for field_config in fieldset.get("fields", []): - field_name = field_config.get("controlplane_field_mapping") + for fc in fieldset.get("fields", []): + field_name = fc.get("controlplane_field_mapping") if field_name not in self.fields: continue + field_config = fc.copy() + # Merge with defaults if field has default config + if field_name in DEFAULT_FIELD_CONFIGS: + field_config = DEFAULT_FIELD_CONFIGS[field_name].copy() + for key, value in fc.items(): + if value or (value is False): + field_config[key] = value + field = self.fields[field_name] field_type = field_config.get("type") diff --git a/src/servala/core/forms.py b/src/servala/core/forms.py index 076a9d9..69132fc 100644 --- a/src/servala/core/forms.py +++ b/src/servala/core/forms.py @@ -6,6 +6,7 @@ from django import forms from django.utils.translation import gettext_lazy as _ from django_jsonform.widgets import JSONFormWidget +from servala.core.crd.forms import DEFAULT_FIELD_CONFIGS, MANDATORY_FIELDS from servala.core.models import ControlPlane, ServiceDefinition CONTROL_PLANE_USER_INFO_SCHEMA = { @@ -244,8 +245,26 @@ class ServiceDefinitionAdminForm(forms.ModelForm): for field in fieldset.get("fields", []): mapping = field.get("controlplane_field_mapping") included_mappings.add(mapping) + + # Validate that fields without defaults have required properties + if mapping not in DEFAULT_FIELD_CONFIGS: + if not field.get("label"): + errors.append( + _( + "Field with mapping '{}' must have a 'label' property " + "(or use a mapping with default config)" + ).format(mapping) + ) + if not field.get("type"): + errors.append( + _( + "Field with mapping '{}' must have a 'type' property " + "(or use a mapping with default config)" + ).format(mapping) + ) + if mapping and mapping not in valid_paths: - field_name = field.get("name", mapping) + field_name = field.get("label", field.get("name", mapping)) errors.append( _( "Field '{}' has invalid mapping '{}'. Valid paths are: {}" @@ -262,14 +281,13 @@ class ServiceDefinitionAdminForm(forms.ModelForm): field, mapping, spec_schema, "spec", errors ) - if "name" not in included_mappings: - raise forms.ValidationError( - { - "form_config": _( - "You must include a `name` field in the custom form config." - ) - } - ) + for mandatory_field in MANDATORY_FIELDS: + if mandatory_field not in included_mappings: + errors.append( + _( + "Required field '{}' must be included in the form configuration" + ).format(mandatory_field) + ) if errors: raise forms.ValidationError({"form_config": errors}) diff --git a/src/tests/test_form_config.py b/src/tests/test_form_config.py index ba44d72..7188d61 100644 --- a/src/tests/test_form_config.py +++ b/src/tests/test_form_config.py @@ -5,6 +5,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from servala.core.crd import generate_custom_form_class +from servala.core.crd.forms import DEFAULT_FIELD_CONFIGS, MANDATORY_FIELDS from servala.core.forms import ServiceDefinitionAdminForm from servala.core.models import ControlPlaneCRD @@ -904,3 +905,182 @@ def test_three_plus_element_choices_fail_validation(): assert len(errors) > 0 assert "must have 1 or 2 elements" in str(errors[0]) + + +def test_field_with_default_config_only_needs_mapping(): + + class TestModel(models.Model): + name = models.CharField(max_length=100) + + class Meta: + app_label = "test" + + minimal_config = { + "fieldsets": [ + { + "fields": [ + { + "controlplane_field_mapping": "name", + }, + ] + } + ] + } + + form_class = generate_custom_form_class(minimal_config, TestModel) + form = form_class() + + name_field = form.fields["name"] + assert name_field.label == DEFAULT_FIELD_CONFIGS["name"]["label"] + assert name_field.help_text == DEFAULT_FIELD_CONFIGS["name"]["help_text"] + assert name_field.required == DEFAULT_FIELD_CONFIGS["name"]["required"] + + +def test_field_with_default_config_can_override_defaults(): + + class TestModel(models.Model): + name = models.CharField(max_length=100) + + class Meta: + app_label = "test" + + override_config = { + "fieldsets": [ + { + "fields": [ + { + "controlplane_field_mapping": "name", + "label": "Custom Name Label", + "required": False, + }, + ] + } + ] + } + + form_class = generate_custom_form_class(override_config, TestModel) + form = form_class() + + name_field = form.fields["name"] + assert name_field.label == "Custom Name Label" + assert name_field.required is False + assert name_field.help_text == DEFAULT_FIELD_CONFIGS["name"]["help_text"] + + +def test_admin_form_validates_mandatory_fields_present(): + + mock_crd = Mock() + mock_crd.resource_schema = { + "properties": { + "spec": { + "properties": { + "environment": { + "type": "string", + "enum": ["dev", "prod"], + } + } + } + } + } + + config_without_name = { + "fieldsets": [ + { + "fields": [ + { + "type": "choice", + "label": "Environment", + "controlplane_field_mapping": "spec.environment", + "choices": [["dev", "Development"]], + }, + ] + } + ] + } + + errors = [] + included_mappings = set() + for fieldset in config_without_name.get("fieldsets", []): + for field in fieldset.get("fields", []): + mapping = field.get("controlplane_field_mapping") + included_mappings.add(mapping) + + for mandatory_field in MANDATORY_FIELDS: + if mandatory_field not in included_mappings: + errors.append(f"Required field '{mandatory_field}' must be included") + + assert len(errors) > 0 + assert "name" in str(errors[0]).lower() + + +def test_admin_form_validates_fields_without_defaults_need_label_and_type(): + config_with_incomplete_field = { + "fieldsets": [ + { + "fields": [ + {"controlplane_field_mapping": "name"}, # Has defaults - OK + { + "controlplane_field_mapping": "spec.unknown", # No defaults + # Missing label and type + }, + ] + } + ] + } + + errors = [] + + for fieldset in config_with_incomplete_field.get("fieldsets", []): + for field in fieldset.get("fields", []): + mapping = field.get("controlplane_field_mapping") + + if mapping not in DEFAULT_FIELD_CONFIGS: + if not field.get("label"): + errors.append( + f"Field with mapping '{mapping}' must have a 'label' property" + ) + if not field.get("type"): + errors.append( + f"Field with mapping '{mapping}' must have a 'type' property" + ) + + assert len(errors) == 2 + assert any("label" in str(e) for e in errors) + assert any("type" in str(e) for e in errors) + + +def test_empty_values_dont_override_default_configs(): + + class TestModel(models.Model): + name = models.CharField(max_length=100) + + class Meta: + app_label = "test" + + admin_form_config = { + "fieldsets": [ + { + "fields": [ + { + "controlplane_field_mapping": "name", + "type": "", + "label": "", + "help_text": None, + "max_length": None, + "required": False, + }, + ] + } + ] + } + + form_class = generate_custom_form_class(admin_form_config, TestModel) + form = form_class() + + name_field = form.fields["name"] + + assert name_field.label == DEFAULT_FIELD_CONFIGS["name"]["label"] + assert name_field.help_text == DEFAULT_FIELD_CONFIGS["name"]["help_text"] + assert name_field.max_length == DEFAULT_FIELD_CONFIGS["name"]["max_length"] + + assert name_field.required is False # Was overridden by explicit False From 7a8dc91afe48e0d7efe966975409713503cf8fe3 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Mon, 10 Nov 2025 14:02:40 +0100 Subject: [PATCH 146/185] Make sure max length applies to textarea --- src/servala/core/crd/forms.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/servala/core/crd/forms.py b/src/servala/core/crd/forms.py index bdb8ed6..659684e 100644 --- a/src/servala/core/crd/forms.py +++ b/src/servala/core/crd/forms.py @@ -350,6 +350,13 @@ class CustomFormMixin(FormGeneratorMixin): if "default_value" in field_config and field.initial is None: field.initial = field_config["default_value"] + if field_type in ("text", "textarea") and field_config.get( + "max_length" + ): + field.max_length = field_config.get("max_length") + if hasattr(field.widget, "attrs"): + field.widget.attrs["maxlength"] = field_config.get("max_length") + field.controlplane_field_mapping = field_name def get_fieldsets(self): From 9ac9f5e1c944f32e8d3962f41594118d137d9fd5 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Mon, 10 Nov 2025 14:04:42 +0100 Subject: [PATCH 147/185] Add print debugging to tab setup --- src/servala/static/js/bootstrap-tabs.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/servala/static/js/bootstrap-tabs.js b/src/servala/static/js/bootstrap-tabs.js index d382475..1d9eb89 100644 --- a/src/servala/static/js/bootstrap-tabs.js +++ b/src/servala/static/js/bootstrap-tabs.js @@ -3,15 +3,22 @@ (function() { 'use strict'; + console.log("Loading bootstrap tab setup") + const initBootstrapTabs = () => { + console.log("setting up bootstrap tabs") const customTabList = document.querySelectorAll('#myTab button[data-bs-toggle="tab"]'); + console.log("found custom tabs", customTabList) customTabList.forEach(function(tabButton) { new bootstrap.Tab(tabButton); + console.log("setting up custom tab", tabButton) }); const expertTabList = document.querySelectorAll('#expertTab button[data-bs-toggle="tab"]'); + console.log("found expert tabs", expertTabList) expertTabList.forEach(function(tabButton) { new bootstrap.Tab(tabButton); + console.log("setting up expert tab", tabButton) }); } From f3e14b4c85ceabc321ec251227b4983acd00488a Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Mon, 10 Nov 2025 14:15:22 +0100 Subject: [PATCH 148/185] Fix tabs for custom fieldsets --- .../frontend/templates/includes/tabbed_fieldset_form.html | 8 ++++---- src/servala/static/js/bootstrap-tabs.js | 7 ------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/servala/frontend/templates/includes/tabbed_fieldset_form.html b/src/servala/frontend/templates/includes/tabbed_fieldset_form.html index ace05af..009ad94 100644 --- a/src/servala/frontend/templates/includes/tabbed_fieldset_form.html +++ b/src/servala/frontend/templates/includes/tabbed_fieldset_form.html @@ -45,10 +45,10 @@
- {% if form.instance.billing_entity and form.instance.billing_entity.odoo_data.invoice_address %} + {% if not form.instance.origin.hide_billing_address %} + {% if form.instance.billing_entity and form.instance.billing_entity.odoo_data.invoice_address %} +
+
+

{% translate "Billing Address" %}

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

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

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

{% translate "Billing Address" %}

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

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

- {% endif %} +

{% translate "Billing Information" %}

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

{{ form.instance.origin.billing_message }}

From b3ecae6fd995e71bb20be94f9e39d1507dd180dd Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 12 Nov 2025 10:34:55 +0100 Subject: [PATCH 156/185] Add missing migration --- .../migrations/0014_hide_billing_address.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/servala/core/migrations/0014_hide_billing_address.py diff --git a/src/servala/core/migrations/0014_hide_billing_address.py b/src/servala/core/migrations/0014_hide_billing_address.py new file mode 100644 index 0000000..7295f3a --- /dev/null +++ b/src/servala/core/migrations/0014_hide_billing_address.py @@ -0,0 +1,41 @@ +# Generated by Django 5.2.8 on 2025-11-12 09:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0013_add_form_config"), + ] + + operations = [ + migrations.AddField( + model_name="organizationorigin", + name="billing_message", + field=models.TextField( + blank=True, + help_text="Optional message to display instead of billing address (e.g., 'You will be invoiced by Exoscale').", + verbose_name="Billing Message", + ), + ), + migrations.AddField( + model_name="organizationorigin", + name="hide_billing_address", + field=models.BooleanField( + default=False, + help_text="If enabled, the billing address will not be shown in the organization details view.", + verbose_name="Hide Billing Address", + ), + ), + migrations.AlterField( + model_name="controlplane", + name="user_info", + field=models.JSONField( + blank=True, + help_text='Array of info objects: [{"title": "…", "content": "…", "help_text": "…"}]. The help_text field is optional and will be shown as a hover popover on an info icon.', + null=True, + verbose_name="User Information", + ), + ), + ] From 830ebfb890eb5356042a075c8444ff2fa1bfada0 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 12 Nov 2025 12:16:52 +0100 Subject: [PATCH 157/185] Add "open" button to instances with FQDN closes #256 --- .../frontend/organizations/service_instance_detail.html | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html b/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html index 4aaef14..7333838 100644 --- a/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html +++ b/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html @@ -7,6 +7,15 @@ {% endblock html_title %} {% block page_title_extra %}
+ {% if instance.spec.parameters.service.fqdn %} + + + {% translate "Open" %} + + {% endif %} {% if has_change_permission %} {% translate "Edit" %} {% endif %} From 37875349a0e5632b888e1e8680093d16648629a1 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 13 Nov 2025 03:01:13 +0000 Subject: [PATCH 158/185] Update dependency pytest to >=9.0.1 --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9a22700..bd84e17 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ dev = [ "flake8-bugbear>=25.10.21", "flake8-pyproject>=1.2.3", "isort>=7.0.0", - "pytest>=9.0.0", + "pytest>=9.0.1", "pytest-cov>=7.0.0", "pytest-django>=4.11.1", "pytest-mock>=3.15.1", diff --git a/uv.lock b/uv.lock index ad4ea46..5b887be 100644 --- a/uv.lock +++ b/uv.lock @@ -823,7 +823,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.0" +version = "9.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -832,9 +832,9 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/1d/eb34f286b164c5e431a810a38697409cca1112cee04b287bb56ac486730b/pytest-9.0.0.tar.gz", hash = "sha256:8f44522eafe4137b0f35c9ce3072931a788a21ee40a2ed279e817d3cc16ed21e", size = 1562764, upload-time = "2025-11-08T17:25:33.34Z" } +sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/72/99/cafef234114a3b6d9f3aaed0723b437c40c57bdb7b3e4c3a575bc4890052/pytest-9.0.0-py3-none-any.whl", hash = "sha256:e5ccdf10b0bac554970ee88fc1a4ad0ee5d221f8ef22321f9b7e4584e19d7f96", size = 373364, upload-time = "2025-11-08T17:25:31.811Z" }, + { url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" }, ] [[package]] @@ -1162,7 +1162,7 @@ dev = [ { name = "flake8-bugbear", specifier = ">=25.10.21" }, { name = "flake8-pyproject", specifier = ">=1.2.3" }, { name = "isort", specifier = ">=7.0.0" }, - { name = "pytest", specifier = ">=9.0.0" }, + { name = "pytest", specifier = ">=9.0.1" }, { name = "pytest-cov", specifier = ">=7.0.0" }, { name = "pytest-django", specifier = ">=4.11.1" }, { name = "pytest-mock", specifier = ">=3.15.1" }, From db590e011581500e7e01b67c42adae36094aaaf6 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 13 Nov 2025 10:59:32 +0100 Subject: [PATCH 159/185] view availability is now get it which is a much better call to action --- src/servala/frontend/templates/includes/service_card.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/servala/frontend/templates/includes/service_card.html b/src/servala/frontend/templates/includes/service_card.html index 0dae3ae..dcf739f 100644 --- a/src/servala/frontend/templates/includes/service_card.html +++ b/src/servala/frontend/templates/includes/service_card.html @@ -26,6 +26,6 @@ {% else %} {% endif %} - {% translate "View Availability" %} + {% translate "Get It" %}
From 325d274ceaa0191df8fdaf6b8a1826acf9c01afd Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 13 Nov 2025 11:05:43 +0100 Subject: [PATCH 160/185] bump version 2025.10.27-0 -> 2025.11.13-0 --- README.md | 2 +- .../ROOT/pages/web-portal-changelog.adoc | 41 +++++++++++++++++++ pyproject.toml | 2 +- src/servala/__about__.py | 2 +- 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d814ce4..fa0e56a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ The Servala Self-Service Portal -Latest release: 2025.10.27-0 +Latest release: 2025.11.13-0 ## Documentation diff --git a/docs/modules/ROOT/pages/web-portal-changelog.adoc b/docs/modules/ROOT/pages/web-portal-changelog.adoc index bf2d3aa..3eef510 100644 --- a/docs/modules/ROOT/pages/web-portal-changelog.adoc +++ b/docs/modules/ROOT/pages/web-portal-changelog.adoc @@ -1,5 +1,46 @@ = Portal Changelog +== 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 diff --git a/pyproject.toml b/pyproject.toml index bd84e17..ef073ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ testpaths = "src/tests" pythonpath = "src" [tool.bumpver] -current_version = "2025.10.27-0" +current_version = "2025.11.13-0" version_pattern = "YYYY.0M.0D-INC0" commit_message = "bump version {old_version} -> {new_version}" tag_message = "{new_version}" diff --git a/src/servala/__about__.py b/src/servala/__about__.py index 6ac27d0..acfbf71 100644 --- a/src/servala/__about__.py +++ b/src/servala/__about__.py @@ -1 +1 @@ -__version__ = "2025.10.27-0" +__version__ = "2025.11.13-0" From 63f598235013891f3876f1806600cd0e0b1be636 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 13 Nov 2025 14:11:46 +0100 Subject: [PATCH 161/185] improve release workflow with tags --- .forgejo/workflows/build-deploy-prod.yaml | 3 +++ hack/bumpver-post-commit-hook.sh | 8 ++++++++ pyproject.toml | 2 +- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.forgejo/workflows/build-deploy-prod.yaml b/.forgejo/workflows/build-deploy-prod.yaml index 53d1e32..aa9315d 100644 --- a/.forgejo/workflows/build-deploy-prod.yaml +++ b/.forgejo/workflows/build-deploy-prod.yaml @@ -12,6 +12,9 @@ on: - "pyproject.toml" - "uv.lock" workflow_dispatch: + release: + types: + - published jobs: build: diff --git a/hack/bumpver-post-commit-hook.sh b/hack/bumpver-post-commit-hook.sh index ca175bd..fdda006 100755 --- a/hack/bumpver-post-commit-hook.sh +++ b/hack/bumpver-post-commit-hook.sh @@ -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 diff --git a/pyproject.toml b/pyproject.toml index ef073ca..385c305 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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] From 4097261efea3aa3fd9182757a727f1f3df1fff6f Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Fri, 14 Nov 2025 16:41:44 +0100 Subject: [PATCH 162/185] Support single-value FQDN in "open" button --- src/servala/core/models/service.py | 16 ++++++++++++++++ .../organizations/service_instance_detail.html | 4 ++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index 1535703..29bae9f 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -961,5 +961,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) diff --git a/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html b/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html index 7333838..948a2df 100644 --- a/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html +++ b/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html @@ -7,8 +7,8 @@ {% endblock html_title %} {% block page_title_extra %}
- {% if instance.spec.parameters.service.fqdn %} - From 3f9fdd0e48fc6183be43a6cbc1eee455c20276fd Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Fri, 14 Nov 2025 16:52:14 +0100 Subject: [PATCH 163/185] Make sure fqdn generation works with single values --- src/servala/static/js/fqdn.js | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/servala/static/js/fqdn.js b/src/servala/static/js/fqdn.js index ec70dad..0996bda 100644 --- a/src/servala/static/js/fqdn.js +++ b/src/servala/static/js/fqdn.js @@ -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); + } } } } From 1dbeb31985b74768d716b0c1a542831538dd7358 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Fri, 14 Nov 2025 17:30:29 +0100 Subject: [PATCH 164/185] Allow admins to disable the expert mode form closes #289 --- src/servala/core/admin.py | 2 +- .../migrations/0014_hide_billing_address.py | 5 +++- ..._hide_expert_mode_to_service_definition.py | 25 ++++++++++++++++ src/servala/core/models/service.py | 8 +++++ .../service_instance_update.html | 4 +-- .../includes/tabbed_fieldset_form.html | 6 ++-- src/servala/frontend/views/service.py | 29 ++++++++++++++++++- src/tests/test_views.py | 1 + 8 files changed, 72 insertions(+), 8 deletions(-) create mode 100644 src/servala/core/migrations/0015_add_hide_expert_mode_to_service_definition.py diff --git a/src/servala/core/admin.py b/src/servala/core/admin.py index 39fbab9..60fe147 100644 --- a/src/servala/core/admin.py +++ b/src/servala/core/admin.py @@ -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." ), diff --git a/src/servala/core/migrations/0014_hide_billing_address.py b/src/servala/core/migrations/0014_hide_billing_address.py index 7295f3a..1e7f3bf 100644 --- a/src/servala/core/migrations/0014_hide_billing_address.py +++ b/src/servala/core/migrations/0014_hide_billing_address.py @@ -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", ), diff --git a/src/servala/core/migrations/0015_add_hide_expert_mode_to_service_definition.py b/src/servala/core/migrations/0015_add_hide_expert_mode_to_service_definition.py new file mode 100644 index 0000000..cc88c9d --- /dev/null +++ b/src/servala/core/migrations/0015_add_hide_expert_mode_to_service_definition.py @@ -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", + ), + ), + ] diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index 1535703..4f89063 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -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, diff --git a/src/servala/frontend/templates/frontend/organizations/service_instance_update.html b/src/servala/frontend/templates/frontend/organizations/service_instance_update.html index 74259e6..17b9a51 100644 --- a/src/servala/frontend/templates/frontend/organizations/service_instance_update.html +++ b/src/servala/frontend/templates/frontend/organizations/service_instance_update.html @@ -13,7 +13,7 @@ {% translate "Back" %} {% endblock page_title_extra %} {% partialdef service-form %} -{% if form %} +{% if form or custom_form %}
@@ -31,7 +31,7 @@ {% block content %}
- {% if not form %} + {% if not form and not custom_form %} diff --git a/src/servala/frontend/templates/includes/tabbed_fieldset_form.html b/src/servala/frontend/templates/includes/tabbed_fieldset_form.html index 009ad94..5f289b7 100644 --- a/src/servala/frontend/templates/includes/tabbed_fieldset_form.html +++ b/src/servala/frontend/templates/includes/tabbed_fieldset_form.html @@ -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 %} - {% if expert_form %} + {% if expert_form and not hide_expert_mode %}
@@ -144,6 +144,6 @@
-{% if form %} +{% if form and not hide_expert_mode %} {% endif %} diff --git a/src/servala/frontend/views/service.py b/src/servala/frontend/views/service.py index 1aeac59..c26194d 100644 --- a/src/servala/frontend/views/service.py +++ b/src/servala/frontend/views/service.py @@ -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() diff --git a/src/tests/test_views.py b/src/tests/test_views.py index a97ecd1..0a39444 100644 --- a/src/tests/test_views.py +++ b/src/tests/test_views.py @@ -2,6 +2,7 @@ import pytest from servala.core.models.service import CloudProvider, ServiceOffering + @pytest.mark.parametrize( "url,redirect", ( From e8d6c1acd59eaefb493a36b00997bf5c3254c89d Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 15 Nov 2025 03:01:04 +0000 Subject: [PATCH 165/185] Update dependency django-template-partials to >=25.3 --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 385c305..a9f80c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ dependencies = [ "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", diff --git a/uv.lock b/uv.lock index 5b887be..8b546d7 100644 --- a/uv.lock +++ b/uv.lock @@ -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]] @@ -1140,7 +1140,7 @@ requires-dist = [ { 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" }, From 6996e56de73d0ca33d1f31c11c9f759d72fcf0f4 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 17 Nov 2025 03:01:02 +0000 Subject: [PATCH 166/185] Lock file maintenance --- uv.lock | 164 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 82 insertions(+), 82 deletions(-) diff --git a/uv.lock b/uv.lock index 5b887be..59eceaa 100644 --- a/uv.lock +++ b/uv.lock @@ -86,30 +86,30 @@ wheels = [ [[package]] name = "boto3" -version = "1.40.64" +version = "1.40.74" 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/a2/37/0db5fc46548b347255310893f1a47971a1d8eb0dbc46dfb5ace8a1e7d45e/boto3-1.40.74.tar.gz", hash = "sha256:484e46bf394b03a7c31b34f90945ebe1390cb1e2ac61980d128a9079beac87d4", size = 111592, upload-time = "2025-11-14T20:29:10.991Z" } 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/d2/08/c52751748762901c0ca3c3019e3aa950010217f0fdf9940ebe68e6bb2f5a/boto3-1.40.74-py3-none-any.whl", hash = "sha256:41fc8844b37ae27b24bcabf8369769df246cc12c09453988d0696ad06d6aa9ef", size = 139360, upload-time = "2025-11-14T20:29:09.477Z" }, ] [[package]] name = "botocore" -version = "1.40.64" +version = "1.40.74" 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/81/dc/0412505f05286f282a75bb0c650e525ddcfaf3f6f1a05cd8e99d32a2db06/botocore-1.40.74.tar.gz", hash = "sha256:57de0b9ffeada06015b3c7e5186c77d0692b210d9e5efa294f3214df97e2f8ee", size = 14452479, upload-time = "2025-11-14T20:29:00.949Z" } 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/7d/a2/306dec16e3c84f3ca7aaead0084358c1c7fbe6501f6160844cbc93bc871e/botocore-1.40.74-py3-none-any.whl", hash = "sha256:f39f5763e35e75f0bd91212b7b36120b1536203e8003cd952ef527db79702b15", size = 14117911, upload-time = "2025-11-14T20:28:58.153Z" }, ] [[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]] @@ -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]] @@ -513,16 +513,16 @@ wheels = [ [[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.29.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/98/33/23b3b3419b6a3e0f559c7c0d2ca8fc1b9448382b25245033788785921332/rpds_py-0.29.0.tar.gz", hash = "sha256:fe55fe686908f50154d1dc599232016e50c243b438c3b7432f24e2895b0e5359", size = 69359, upload-time = "2025-11-16T14:50:39.532Z" } 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/89/b1/0b1474e7899371d9540d3bbb2a499a3427ae1fc39c998563fe9035a1073b/rpds_py-0.29.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:394d27e4453d3b4d82bb85665dc1fcf4b0badc30fc84282defed71643b50e1a1", size = 363731, upload-time = "2025-11-16T14:49:26.683Z" }, + { url = "https://files.pythonhosted.org/packages/28/12/3b7cf2068d0a334ed1d7b385a9c3c8509f4c2bcba3d4648ea71369de0881/rpds_py-0.29.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55d827b2ae95425d3be9bc9a5838b6c29d664924f98146557f7715e331d06df8", size = 354343, upload-time = "2025-11-16T14:49:28.24Z" }, + { url = "https://files.pythonhosted.org/packages/eb/73/5afcf8924bc02a749416eda64e17ac9c9b28f825f4737385295a0e99b0c1/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc31a07ed352e5462d3ee1b22e89285f4ce97d5266f6d1169da1142e78045626", size = 385406, upload-time = "2025-11-16T14:49:29.943Z" }, + { url = "https://files.pythonhosted.org/packages/c8/37/5db736730662508535221737a21563591b6f43c77f2e388951c42f143242/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c4695dd224212f6105db7ea62197144230b808d6b2bba52238906a2762f1d1e7", size = 396162, upload-time = "2025-11-16T14:49:31.833Z" }, + { url = "https://files.pythonhosted.org/packages/70/0d/491c1017d14f62ce7bac07c32768d209a50ec567d76d9f383b4cfad19b80/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcae1770b401167f8b9e1e3f566562e6966ffa9ce63639916248a9e25fa8a244", size = 517719, upload-time = "2025-11-16T14:49:33.804Z" }, + { url = "https://files.pythonhosted.org/packages/d7/25/b11132afcb17cd5d82db173f0c8dab270ffdfaba43e5ce7a591837ae9649/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:90f30d15f45048448b8da21c41703b31c61119c06c216a1bf8c245812a0f0c17", size = 409498, upload-time = "2025-11-16T14:49:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7d/e6543cedfb2e6403a1845710a5ab0e0ccf8fc288e0b5af9a70bfe2c12053/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44a91e0ab77bdc0004b43261a4b8cd6d6b451e8d443754cfda830002b5745b32", size = 382743, upload-time = "2025-11-16T14:49:36.704Z" }, + { url = "https://files.pythonhosted.org/packages/75/11/a4ebc9f654293ae9fefb83b2b6be7f3253e85ea42a5db2f77d50ad19aaeb/rpds_py-0.29.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:4aa195e5804d32c682e453b34474f411ca108e4291c6a0f824ebdc30a91c973c", size = 400317, upload-time = "2025-11-16T14:49:39.132Z" }, + { url = "https://files.pythonhosted.org/packages/52/18/97677a60a81c7f0e5f64e51fb3f8271c5c8fcabf3a2df18e97af53d7c2bf/rpds_py-0.29.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7971bdb7bf4ee0f7e6f67fa4c7fbc6019d9850cc977d126904392d363f6f8318", size = 416979, upload-time = "2025-11-16T14:49:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/f0/69/28ab391a9968f6c746b2a2db181eaa4d16afaa859fedc9c2f682d19f7e18/rpds_py-0.29.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8ae33ad9ce580c7a47452c3b3f7d8a9095ef6208e0a0c7e4e2384f9fc5bf8212", size = 567288, upload-time = "2025-11-16T14:49:42.24Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d3/0c7afdcdb830eee94f5611b64e71354ffe6ac8df82d00c2faf2bfffd1d4e/rpds_py-0.29.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c661132ab2fb4eeede2ef69670fd60da5235209874d001a98f1542f31f2a8a94", size = 593157, upload-time = "2025-11-16T14:49:43.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ac/a0fcbc2feed4241cf26d32268c195eb88ddd4bd862adfc9d4b25edfba535/rpds_py-0.29.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bb78b3a0d31ac1bde132c67015a809948db751cb4e92cdb3f0b242e430b6ed0d", size = 554741, upload-time = "2025-11-16T14:49:45.557Z" }, + { url = "https://files.pythonhosted.org/packages/0f/f1/fcc24137c470df8588674a677f33719d5800ec053aaacd1de8a5d5d84d9e/rpds_py-0.29.0-cp314-cp314-win32.whl", hash = "sha256:f475f103488312e9bd4000bc890a95955a07b2d0b6e8884aef4be56132adbbf1", size = 215508, upload-time = "2025-11-16T14:49:47.562Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c7/1d169b2045512eac019918fc1021ea07c30e84a4343f9f344e3e0aa8c788/rpds_py-0.29.0-cp314-cp314-win_amd64.whl", hash = "sha256:b9cf2359a4fca87cfb6801fae83a76aedf66ee1254a7a151f1341632acf67f1b", size = 228125, upload-time = "2025-11-16T14:49:49.064Z" }, + { url = "https://files.pythonhosted.org/packages/be/36/0cec88aaba70ec4a6e381c444b0d916738497d27f0c30406e3d9fcbd3bc2/rpds_py-0.29.0-cp314-cp314-win_arm64.whl", hash = "sha256:9ba8028597e824854f0f1733d8b964e914ae3003b22a10c2c664cb6927e0feb9", size = 221992, upload-time = "2025-11-16T14:49:50.777Z" }, + { url = "https://files.pythonhosted.org/packages/b1/fa/a2e524631717c9c0eb5d90d30f648cfba6b731047821c994acacb618406c/rpds_py-0.29.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:e71136fd0612556b35c575dc2726ae04a1669e6a6c378f2240312cf5d1a2ab10", size = 366425, upload-time = "2025-11-16T14:49:52.691Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a4/6d43ebe0746ff694a30233f63f454aed1677bd50ab7a59ff6b2bb5ac61f2/rpds_py-0.29.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:76fe96632d53f3bf0ea31ede2f53bbe3540cc2736d4aec3b3801b0458499ef3a", size = 355282, upload-time = "2025-11-16T14:49:54.292Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a7/52fd8270e0320b09eaf295766ae81dd175f65394687906709b3e75c71d06/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9459a33f077130dbb2c7c3cea72ee9932271fb3126404ba2a2661e4fe9eb7b79", size = 384968, upload-time = "2025-11-16T14:49:55.857Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/e6bc526b7a14e1ef80579a52c1d4ad39260a058a51d66c6039035d14db9d/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5c9546cfdd5d45e562cc0444b6dddc191e625c62e866bf567a2c69487c7ad28a", size = 394714, upload-time = "2025-11-16T14:49:57.343Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3f/f0ade3954e7db95c791e7eaf978aa7e08a756d2046e8bdd04d08146ed188/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12597d11d97b8f7e376c88929a6e17acb980e234547c92992f9f7c058f1a7310", size = 520136, upload-time = "2025-11-16T14:49:59.162Z" }, + { url = "https://files.pythonhosted.org/packages/87/b3/07122ead1b97009715ab9d4082be6d9bd9546099b2b03fae37c3116f72be/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28de03cf48b8a9e6ec10318f2197b83946ed91e2891f651a109611be4106ac4b", size = 409250, upload-time = "2025-11-16T14:50:00.698Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/dcbee61fd1dc892aedcb1b489ba661313101aa82ec84b1a015d4c63ebfda/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd7951c964069039acc9d67a8ff1f0a7f34845ae180ca542b17dc1456b1f1808", size = 384940, upload-time = "2025-11-16T14:50:02.312Z" }, + { url = "https://files.pythonhosted.org/packages/47/11/914ecb6f3574cf9bf8b38aced4063e0f787d6e1eb30b181a7efbc6c1da9a/rpds_py-0.29.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:c07d107b7316088f1ac0177a7661ca0c6670d443f6fe72e836069025e6266761", size = 399392, upload-time = "2025-11-16T14:50:03.829Z" }, + { url = "https://files.pythonhosted.org/packages/f5/fd/2f4bd9433f58f816434bb934313584caa47dbc6f03ce5484df8ac8980561/rpds_py-0.29.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1de2345af363d25696969befc0c1688a6cb5e8b1d32b515ef84fc245c6cddba3", size = 416796, upload-time = "2025-11-16T14:50:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/79/a5/449f0281af33efa29d5c71014399d74842342ae908d8cd38260320167692/rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:00e56b12d2199ca96068057e1ae7f9998ab6e99cda82431afafd32f3ec98cca9", size = 566843, upload-time = "2025-11-16T14:50:07.243Z" }, + { url = "https://files.pythonhosted.org/packages/ab/32/0a6a1ccee2e37fcb1b7ba9afde762b77182dbb57937352a729c6cd3cf2bb/rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3919a3bbecee589300ed25000b6944174e07cd20db70552159207b3f4bbb45b8", size = 593956, upload-time = "2025-11-16T14:50:09.029Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3d/eb820f95dce4306f07a495ede02fb61bef36ea201d9137d4fcd5ab94ec1e/rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7fa2ccc312bbd91e43aa5e0869e46bc03278a3dddb8d58833150a18b0f0283a", size = 557288, upload-time = "2025-11-16T14:50:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f8/b8ff786f40470462a252918e0836e0db903c28e88e3eec66bc4a7856ee5d/rpds_py-0.29.0-cp314-cp314t-win32.whl", hash = "sha256:97c817863ffc397f1e6a6e9d2d89fe5408c0a9922dac0329672fb0f35c867ea5", size = 211382, upload-time = "2025-11-16T14:50:12.827Z" }, + { url = "https://files.pythonhosted.org/packages/c9/7f/1a65ae870bc9d0576aebb0c501ea5dccf1ae2178fe2821042150ebd2e707/rpds_py-0.29.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2023473f444752f0f82a58dfcbee040d0a1b3d1b3c2ec40e884bd25db6d117d2", size = 225919, upload-time = "2025-11-16T14:50:14.734Z" }, ] [[package]] From 61f1065bc6fbc748f4569246ee293d6cfce38365 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 12 Nov 2025 11:42:10 +0100 Subject: [PATCH 167/185] Start work on implementing Exoscale offboarding rel #262 --- src/servala/api/views.py | 173 ++++++++++++++++++++- src/tests/test_api_exoscale.py | 276 +++++++++++++++++++++++++++++++++ 2 files changed, 445 insertions(+), 4 deletions(-) diff --git a/src/servala/api/views.py b/src/servala/api/views.py index 456f4b2..c4857e5 100644 --- a/src/servala/api/views.py +++ b/src/servala/api/views.py @@ -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 CLIENT 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,168 @@ 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) + 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="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, 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 + # TODO: we do not currently match instance IDs from exoscale yet, this + # will likely not work at all yet + instances = ( + ServiceInstance.objects.filter( + name=instance_id, + context__service_offering=service_offering, + ) + .select_related("organization") + .first() + ) + + if instances: + organization = service_instance.organization + except Exception: + 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}") + + 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("\nUsers:") + 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 = "\n".join(description_parts) + ticket_data = { + "name": f"Exoscale OSB {action} - {service.name} - {instance_id}", + "team_id": settings.ODOO["HELPDESK_TEAM_ID"], + "description": description, + } + + CLIENT.execute("helpdesk.ticket", "create", [ticket_data]) + logger.info( + f"Created {action} helpdesk ticket for instance {instance_id}, service {service.name}" + ) + + except Exception as e: + logger.error( + f"Error creating Exoscale {action} helpdesk ticket for instance {instance_id}: {e}" + ) diff --git a/src/tests/test_api_exoscale.py b/src/tests/test_api_exoscale.py index 6d10deb..283e604 100644 --- a/src/tests/test_api_exoscale.py +++ b/src/tests/test_api_exoscale.py @@ -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,273 @@ 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_api_client = mocker.patch("servala.api.views.CLIENT") + + 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 + + ticket_call = mock_api_client.execute.call_args_list[-1] + ticket_data = ticket_call[0][2][0] + + assert "admin/core/serviceoffering" in ticket_data["description"] + assert f"/{test_service_offering.pk}/" in ticket_data["description"] + assert ( + ticket_data["name"] + == 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_api_client = mocker.patch("servala.api.views.CLIENT") + 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 + ticket_call = mock_api_client.execute.call_args_list[-1] + ticket_data = ticket_call[0][2][0] + assert "admin/core/serviceoffering" in ticket_data["description"] + assert "admin/core/user" in ticket_data["description"] + assert f"/{org_owner.pk}/" in ticket_data["description"] + assert ( + ticket_data["name"] + == 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_api_client = mocker.patch("servala.api.views.CLIENT") + 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 + ticket_call = mock_api_client.execute.call_args_list[-1] + ticket_data = ticket_call[0][2][0] + assert f"Organization: {organization.name}" in ticket_data["description"] + assert "admin/core/organization" in ticket_data["description"] + assert f"/{organization.pk}/" in ticket_data["description"] + assert f"Instance: {service_instance.name}" in ticket_data["description"] + assert "admin/core/serviceinstance" in ticket_data["description"] + assert f"/{service_instance.pk}/" in ticket_data["description"] From e4c64c4a1777c6211b5b65c204dd6af655617827 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 12 Nov 2025 11:52:54 +0100 Subject: [PATCH 168/185] Extract odoo helpdesk ticket creation --- src/servala/api/views.py | 18 +++----- src/servala/core/odoo.py | 16 +++++++ src/servala/frontend/views/support.py | 20 +++------ src/servala/settings_test.py | 1 + src/tests/test_api_exoscale.py | 61 +++++++++++++++++---------- 5 files changed, 70 insertions(+), 46 deletions(-) diff --git a/src/servala/api/views.py b/src/servala/api/views.py index c4857e5..7d8ebfa 100644 --- a/src/servala/api/views.py +++ b/src/servala/api/views.py @@ -21,7 +21,7 @@ from servala.core.models import ( User, ) from servala.core.models.service import Service, ServiceInstance, ServiceOffering -from servala.core.odoo import CLIENT +from servala.core.odoo import create_helpdesk_ticket logger = logging.getLogger(__name__) @@ -269,9 +269,7 @@ The Servala Team""" organization = None try: # Look for instances with this name in the service offering's context - # TODO: we do not currently match instance IDs from exoscale yet, this - # will likely not work at all yet - instances = ( + service_instance = ( ServiceInstance.objects.filter( name=instance_id, context__service_offering=service_offering, @@ -280,7 +278,7 @@ The Servala Team""" .first() ) - if instances: + if service_instance: organization = service_instance.organization except Exception: pass @@ -327,13 +325,11 @@ The Servala Team""" description_parts.append(f" - {full_name} ({user_link}) - {role}") description = "\n".join(description_parts) - ticket_data = { - "name": f"Exoscale OSB {action} - {service.name} - {instance_id}", - "team_id": settings.ODOO["HELPDESK_TEAM_ID"], - "description": description, - } - CLIENT.execute("helpdesk.ticket", "create", [ticket_data]) + 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}" ) diff --git a/src/servala/core/odoo.py b/src/servala/core/odoo.py index ba91dc7..517829a 100644 --- a/src/servala/core/odoo.py +++ b/src/servala/core/odoo.py @@ -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]) diff --git a/src/servala/frontend/views/support.py b/src/servala/frontend/views/support.py index 6f4c4aa..2cd4cf3 100644 --- a/src/servala/frontend/views/support.py +++ b/src/servala/frontend/views/support.py @@ -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, _( diff --git a/src/servala/settings_test.py b/src/servala/settings_test.py index 477ecb2..fec1bfb 100644 --- a/src/servala/settings_test.py +++ b/src/servala/settings_test.py @@ -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", ] diff --git a/src/tests/test_api_exoscale.py b/src/tests/test_api_exoscale.py index 283e604..19f8b93 100644 --- a/src/tests/test_api_exoscale.py +++ b/src/tests/test_api_exoscale.py @@ -614,7 +614,8 @@ def test_delete_creates_ticket_with_admin_links( test_service_offering, instance_id, ): - mock_api_client = mocker.patch("servala.api.views.CLIENT") + # 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}" @@ -623,13 +624,15 @@ def test_delete_creates_ticket_with_admin_links( assert response.status_code == 200 - ticket_call = mock_api_client.execute.call_args_list[-1] - ticket_data = ticket_call[0][2][0] + # Verify the ticket was created with admin URL + mock_create_ticket.assert_called_once() + call_kwargs = mock_create_ticket.call_args[1] - assert "admin/core/serviceoffering" in ticket_data["description"] - assert f"/{test_service_offering.pk}/" in ticket_data["description"] + # 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 ( - ticket_data["name"] + call_kwargs["title"] == f"Exoscale OSB Offboard - {test_service.name} - {instance_id}" ) @@ -644,7 +647,9 @@ def test_patch_creates_ticket_with_user_admin_links( instance_id, org_owner, ): - mock_api_client = mocker.patch("servala.api.views.CLIENT") + # 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, @@ -666,13 +671,17 @@ def test_patch_creates_ticket_with_user_admin_links( ) assert response.status_code == 200 - ticket_call = mock_api_client.execute.call_args_list[-1] - ticket_data = ticket_call[0][2][0] - assert "admin/core/serviceoffering" in ticket_data["description"] - assert "admin/core/user" in ticket_data["description"] - assert f"/{org_owner.pk}/" in ticket_data["description"] + + # 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 ( - ticket_data["name"] + call_kwargs["title"] == f"Exoscale OSB Suspend - {test_service.name} - {instance_id}" ) @@ -686,7 +695,9 @@ def test_ticket_includes_organization_and_instance_when_found( test_service_offering, organization, ): - mock_api_client = mocker.patch("servala.api.views.CLIENT") + # 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, @@ -719,11 +730,17 @@ def test_ticket_includes_organization_and_instance_when_found( ) assert response.status_code == 200 - ticket_call = mock_api_client.execute.call_args_list[-1] - ticket_data = ticket_call[0][2][0] - assert f"Organization: {organization.name}" in ticket_data["description"] - assert "admin/core/organization" in ticket_data["description"] - assert f"/{organization.pk}/" in ticket_data["description"] - assert f"Instance: {service_instance.name}" in ticket_data["description"] - assert "admin/core/serviceinstance" in ticket_data["description"] - assert f"/{service_instance.pk}/" in ticket_data["description"] + + # 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"] From 208f3c357d1a956ceb981a98c6e4e00e816be0a5 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 17 Nov 2025 11:38:02 +0100 Subject: [PATCH 169/185] odoo wants html for linebreaks --- src/servala/api/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/servala/api/views.py b/src/servala/api/views.py index 7d8ebfa..b2991aa 100644 --- a/src/servala/api/views.py +++ b/src/servala/api/views.py @@ -308,7 +308,7 @@ The Servala Team""" description_parts.append(f"Service Offering: {offering_url}") if users: - description_parts.append("\nUsers:") + description_parts.append("
Users:") for user_data in users: email = user_data.get("email", "N/A") full_name = user_data.get("full_name", "N/A") @@ -324,7 +324,7 @@ The Servala Team""" description_parts.append(f" - {full_name} ({user_link}) - {role}") - description = "\n".join(description_parts) + description = "
".join(description_parts) create_helpdesk_ticket( title=f"Exoscale OSB {action} - {service.name} - {instance_id}", From 3e17e03da9f290aeed89a68343d2ca98fa2c266b Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 17 Nov 2025 11:48:19 +0100 Subject: [PATCH 170/185] support hardcoded suspend plan for exoscale --- src/servala/api/views.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/servala/api/views.py b/src/servala/api/views.py index b2991aa..5fdb91a 100644 --- a/src/servala/api/views.py +++ b/src/servala/api/views.py @@ -233,9 +233,12 @@ The Servala Team""" try: service = Service.objects.get(osb_service_id=service_id) - service_offering = ServiceOffering.objects.get( - osb_plan_id=plan_id, service=service - ) + # 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: return self._error(f"Unknown service_id: {service_id}") except ServiceOffering.DoesNotExist: @@ -258,7 +261,7 @@ The Servala Team""" return self.request.build_absolute_uri(admin_path) def _create_action_helpdesk_ticket( - self, request, action, instance_id, service, service_offering, users=None + self, request, action, instance_id, service, service_offering=None, users=None ): """ Create an Odoo helpdesk ticket for offboarding or suspension actions. @@ -269,11 +272,12 @@ The Servala Team""" 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( - name=instance_id, - context__service_offering=service_offering, - ) + ServiceInstance.objects.filter(**filter_kwargs) .select_related("organization") .first() ) @@ -302,10 +306,11 @@ The Servala Team""" else: description_parts.append(f"Instance: {instance_id}") - offering_url = self._get_admin_url( - "core_serviceoffering_change", service_offering.pk - ) - description_parts.append(f"Service Offering: {offering_url}") + if service_offering: + offering_url = self._get_admin_url( + "core_serviceoffering_change", service_offering.pk + ) + description_parts.append(f"Service Offering: {offering_url}") if users: description_parts.append("
Users:") From f99f8511bb99ed567e13580d9ee81746b538f0be Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 17 Nov 2025 11:50:15 +0100 Subject: [PATCH 171/185] bump version 2025.11.13-0 -> 2025.11.17-0 --- README.md | 2 +- .../ROOT/pages/web-portal-changelog.adoc | 36 +++++++++++++++++++ pyproject.toml | 2 +- src/servala/__about__.py | 2 +- 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fa0e56a..eaa1cdd 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ The Servala Self-Service Portal -Latest release: 2025.11.13-0 +Latest release: 2025.11.17-0 ## Documentation diff --git a/docs/modules/ROOT/pages/web-portal-changelog.adoc b/docs/modules/ROOT/pages/web-portal-changelog.adoc index 3eef510..0c5de9c 100644 --- a/docs/modules/ROOT/pages/web-portal-changelog.adoc +++ b/docs/modules/ROOT/pages/web-portal-changelog.adoc @@ -1,5 +1,41 @@ = 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 diff --git a/pyproject.toml b/pyproject.toml index a9f80c3..1a8696d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ testpaths = "src/tests" pythonpath = "src" [tool.bumpver] -current_version = "2025.11.13-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}" diff --git a/src/servala/__about__.py b/src/servala/__about__.py index acfbf71..d6db270 100644 --- a/src/servala/__about__.py +++ b/src/servala/__about__.py @@ -1 +1 @@ -__version__ = "2025.11.13-0" +__version__ = "2025.11.17-0" From def01771c4ab30e85e2430c8c966e7fd4b7f1c4e Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 18 Nov 2025 03:00:49 +0000 Subject: [PATCH 172/185] Update https://github.com/renovatebot/github-action action to v44.0.3 --- .forgejo/workflows/renovate.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/renovate.yaml b/.forgejo/workflows/renovate.yaml index 7dbe5e0..6083841 100644 --- a/.forgejo/workflows/renovate.yaml +++ b/.forgejo/workflows/renovate.yaml @@ -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.3 with: token: ${{ secrets.RENOVATE_TOKEN }} env: From ae925167477b92ab02d0c9c3ea4b521161da0355 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 19 Nov 2025 03:00:55 +0000 Subject: [PATCH 173/185] Update dependency coverage to >=7.12.0 --- pyproject.toml | 2 +- uv.lock | 60 +++++++++++++++++++++++++------------------------- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1a8696d..6ce820e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ 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", diff --git a/uv.lock b/uv.lock index 5ff3cfa..5255a0c 100644 --- a/uv.lock +++ b/uv.lock @@ -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]] @@ -1156,7 +1156,7 @@ 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" }, From aa40027f0a8bd1b35abc2032e0929574a7fda51a Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 19 Nov 2025 03:01:01 +0000 Subject: [PATCH 174/185] Update dependency sentry-sdk to >=2.45.0 --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1a8696d..b397d18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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.45.0", "urlman>=2.0.2", ] diff --git a/uv.lock b/uv.lock index 5ff3cfa..95ecb4d 100644 --- a/uv.lock +++ b/uv.lock @@ -1071,15 +1071,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.44.0" +version = "2.45.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/61/89/1561b3dc8e28bf7978d031893297e89be266f53650c87bb14a29406a9791/sentry_sdk-2.45.0.tar.gz", hash = "sha256:e9bbfe69d5f6742f48bad22452beffb525bbc5b797d817c7f1b1f7d210cdd271", size = 373631, upload-time = "2025-11-18T13:23:22.475Z" } 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/94/c6/039121a0355bc1b5bcceef0dabf211b021fd435d0ee5c46393717bb1c09f/sentry_sdk-2.45.0-py2.py3-none-any.whl", hash = "sha256:86c8ab05dc3e8666aece77a5c747b45b25aa1d5f35f06cde250608f495d50f23", size = 404791, upload-time = "2025-11-18T13:23:20.533Z" }, ] [package.optional-dependencies] @@ -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.45.0" }, { name = "urlman", specifier = ">=2.0.2" }, ] From a268625d808bb014f0fb1508dc546dd33bca3f4b Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Thu, 20 Nov 2025 11:56:10 +0100 Subject: [PATCH 175/185] Add number addon widget --- src/servala/frontend/forms/widgets.py | 19 +++++++++++++++++++ .../forms/number_input_with_addon.html | 11 +++++++++++ 2 files changed, 30 insertions(+) create mode 100644 src/servala/frontend/templates/frontend/forms/number_input_with_addon.html diff --git a/src/servala/frontend/forms/widgets.py b/src/servala/frontend/forms/widgets.py index d67030f..99b7a59 100644 --- a/src/servala/frontend/forms/widgets.py +++ b/src/servala/frontend/forms/widgets.py @@ -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 diff --git a/src/servala/frontend/templates/frontend/forms/number_input_with_addon.html b/src/servala/frontend/templates/frontend/forms/number_input_with_addon.html new file mode 100644 index 0000000..4fe3b54 --- /dev/null +++ b/src/servala/frontend/templates/frontend/forms/number_input_with_addon.html @@ -0,0 +1,11 @@ +
+ + {{ widget.addon_text }} +
From de6794046d77bf58a87154b0a726ecddd52d5fc5 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Thu, 20 Nov 2025 12:06:17 +0100 Subject: [PATCH 176/185] Implement unit fields for numeric fields --- src/servala/api/views.py | 6 +-- src/servala/core/crd/forms.py | 20 +++++++++ src/tests/test_form_config.py | 78 ++++++++++++++++++++--------------- 3 files changed, 67 insertions(+), 37 deletions(-) diff --git a/src/servala/api/views.py b/src/servala/api/views.py index 5fdb91a..015e091 100644 --- a/src/servala/api/views.py +++ b/src/servala/api/views.py @@ -239,9 +239,9 @@ The Servala Team""" service_offering = ServiceOffering.objects.get( osb_plan_id=plan_id, service=service ) - except Service.DoesNotExist: + except Service.DoesNotExist: # pragma: no-cover return self._error(f"Unknown service_id: {service_id}") - except ServiceOffering.DoesNotExist: + except ServiceOffering.DoesNotExist: # pragma: no-cover return self._error( f"Unknown plan_id: {plan_id} for service_id: {service_id}" ) @@ -284,7 +284,7 @@ The Servala Team""" if service_instance: organization = service_instance.organization - except Exception: + except Exception: # pragma: no cover pass description_parts = [f"Action: {action}", f"Service: {service.name}"] diff --git a/src/servala/core/crd/forms.py b/src/servala/core/crd/forms.py index 659684e..6ef44e2 100644 --- a/src/servala/core/crd/forms.py +++ b/src/servala/core/crd/forms.py @@ -1,3 +1,5 @@ +from contextlib import suppress + from django import forms from django.core.validators import MaxValueValidator, MinValueValidator from django.forms.models import ModelForm, ModelFormMetaclass @@ -335,6 +337,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 +421,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]: diff --git a/src/tests/test_form_config.py b/src/tests/test_form_config.py index 7188d61..5b33729 100644 --- a/src/tests/test_form_config.py +++ b/src/tests/test_form_config.py @@ -8,28 +8,7 @@ from servala.core.crd import generate_custom_form_class from servala.core.crd.forms import DEFAULT_FIELD_CONFIGS, MANDATORY_FIELDS from servala.core.forms import ServiceDefinitionAdminForm from servala.core.models import ControlPlaneCRD - - -def test_custom_model_form_class_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 +from servala.frontend.forms.widgets import NumberInputWithAddon def test_custom_model_form_class_returns_class_when_form_config_exists(): @@ -60,18 +39,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 +1054,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" From 0a621f637284f2bf1b9b023718d6bbf294a1b52d Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Thu, 20 Nov 2025 12:06:28 +0100 Subject: [PATCH 177/185] Add default unit for disk size field --- src/servala/core/crd/forms.py | 7 ++++++- src/tests/test_form_config.py | 1 - 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/servala/core/crd/forms.py b/src/servala/core/crd/forms.py index 6ef44e2..0f825ce 100644 --- a/src/servala/core/crd/forms.py +++ b/src/servala/core/crd/forms.py @@ -6,7 +6,7 @@ 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"] @@ -27,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", + }, } diff --git a/src/tests/test_form_config.py b/src/tests/test_form_config.py index 5b33729..c93f3fb 100644 --- a/src/tests/test_form_config.py +++ b/src/tests/test_form_config.py @@ -8,7 +8,6 @@ from servala.core.crd import generate_custom_form_class from servala.core.crd.forms import DEFAULT_FIELD_CONFIGS, MANDATORY_FIELDS from servala.core.forms import ServiceDefinitionAdminForm from servala.core.models import ControlPlaneCRD -from servala.frontend.forms.widgets import NumberInputWithAddon def test_custom_model_form_class_returns_class_when_form_config_exists(): From 1ca02bfb4d8b2914102cbf74093ed1cb04e857e9 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 21 Nov 2025 03:01:01 +0000 Subject: [PATCH 178/185] Update dependency django-allauth to >=65.13.1 --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 51897da..0b3a632 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ 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", diff --git a/uv.lock b/uv.lock index 91e0cdb..6f83304 100644 --- a/uv.lock +++ b/uv.lock @@ -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]] @@ -1134,7 +1134,7 @@ 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" }, From ea313eca1d7722b309e3dca23eeb5b288a1a6678 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 21 Nov 2025 03:01:10 +0000 Subject: [PATCH 179/185] Update actions/checkout action to v6 --- .forgejo/workflows/build-deploy-prod.yaml | 4 ++-- .forgejo/workflows/build-deploy-staging.yaml | 4 ++-- .forgejo/workflows/docs.yaml | 4 ++-- .forgejo/workflows/renovate.yaml | 2 +- .forgejo/workflows/tests.yaml | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.forgejo/workflows/build-deploy-prod.yaml b/.forgejo/workflows/build-deploy-prod.yaml index aa9315d..ddaeb1c 100644 --- a/.forgejo/workflows/build-deploy-prod.yaml +++ b/.forgejo/workflows/build-deploy-prod.yaml @@ -26,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 @@ -72,7 +72,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Determine image tag id: determine-tag diff --git a/.forgejo/workflows/build-deploy-staging.yaml b/.forgejo/workflows/build-deploy-staging.yaml index 93c77b2..8f438cd 100644 --- a/.forgejo/workflows/build-deploy-staging.yaml +++ b/.forgejo/workflows/build-deploy-staging.yaml @@ -22,7 +22,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@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 diff --git a/.forgejo/workflows/docs.yaml b/.forgejo/workflows/docs.yaml index 0b6c77c..b1e5fe5 100644 --- a/.forgejo/workflows/docs.yaml +++ b/.forgejo/workflows/docs.yaml @@ -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 diff --git a/.forgejo/workflows/renovate.yaml b/.forgejo/workflows/renovate.yaml index 6083841..82a9f0e 100644 --- a/.forgejo/workflows/renovate.yaml +++ b/.forgejo/workflows/renovate.yaml @@ -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 diff --git a/.forgejo/workflows/tests.yaml b/.forgejo/workflows/tests.yaml index e3900b3..b2cddd0 100644 --- a/.forgejo/workflows/tests.yaml +++ b/.forgejo/workflows/tests.yaml @@ -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 From 870acfa81b31138e09cf48d68baafe7cf79422c7 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 24 Nov 2025 03:00:48 +0000 Subject: [PATCH 180/185] Lock file maintenance --- uv.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/uv.lock b/uv.lock index 6f83304..1c2a7b7 100644 --- a/uv.lock +++ b/uv.lock @@ -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.74" +version = "1.41.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a2/37/0db5fc46548b347255310893f1a47971a1d8eb0dbc46dfb5ace8a1e7d45e/boto3-1.40.74.tar.gz", hash = "sha256:484e46bf394b03a7c31b34f90945ebe1390cb1e2ac61980d128a9079beac87d4", size = 111592, upload-time = "2025-11-14T20:29:10.991Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/81/2600e83ddd7cb1dac43d28fd39774434afcda0d85d730402192b1a9266a3/boto3-1.41.2.tar.gz", hash = "sha256:7054fbc61cadab383f40ea6d725013ba6c8f569641dddb14c0055e790280ad6c", size = 111593, upload-time = "2025-11-21T20:32:08.622Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/08/c52751748762901c0ca3c3019e3aa950010217f0fdf9940ebe68e6bb2f5a/boto3-1.40.74-py3-none-any.whl", hash = "sha256:41fc8844b37ae27b24bcabf8369769df246cc12c09453988d0696ad06d6aa9ef", size = 139360, upload-time = "2025-11-14T20:29:09.477Z" }, + { url = "https://files.pythonhosted.org/packages/48/41/1ed7fdc3f124c1cf2df78e605588fa78a182410b832f5b71944a69436171/boto3-1.41.2-py3-none-any.whl", hash = "sha256:edcde82fdae4201aa690e3683f8e5b1a846cf1bbf79d03db4fa8a2f6f46dba9c", size = 139343, upload-time = "2025-11-21T20:32:07.147Z" }, ] [[package]] name = "botocore" -version = "1.40.74" +version = "1.41.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/dc/0412505f05286f282a75bb0c650e525ddcfaf3f6f1a05cd8e99d32a2db06/botocore-1.40.74.tar.gz", hash = "sha256:57de0b9ffeada06015b3c7e5186c77d0692b210d9e5efa294f3214df97e2f8ee", size = 14452479, upload-time = "2025-11-14T20:29:00.949Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/0b/6eb5dc752b240dd0b76d7e3257ae25b70683896d1789e7bfb78fba7c7c99/botocore-1.41.2.tar.gz", hash = "sha256:49a3e8f4c1a1759a687941fef8b36efd7bafcf63c1ef74aa75d6497eb4887c9c", size = 14660558, upload-time = "2025-11-21T20:31:58.785Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/a2/306dec16e3c84f3ca7aaead0084358c1c7fbe6501f6160844cbc93bc871e/botocore-1.40.74-py3-none-any.whl", hash = "sha256:f39f5763e35e75f0bd91212b7b36120b1536203e8003cd952ef527db79702b15", size = 14117911, upload-time = "2025-11-14T20:28:58.153Z" }, + { url = "https://files.pythonhosted.org/packages/77/4d/516ee2157c0686fbe48ca8b94dffc17a0c35040d4626761d74b1a43215c8/botocore-1.41.2-py3-none-any.whl", hash = "sha256:154052dfaa7292212f01c8fab822c76cd10a15a7e164e4c45e4634eb40214b90", size = 14324839, upload-time = "2025-11-21T20:31:56.236Z" }, ] [[package]] @@ -1059,14 +1059,14 @@ wheels = [ [[package]] name = "s3transfer" -version = "0.14.0" +version = "0.15.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/ca/bb/940d6af975948c1cc18f44545ffb219d3c35d78ec972b42ae229e8e37e08/s3transfer-0.15.0.tar.gz", hash = "sha256:d36fac8d0e3603eff9b5bfa4282c7ce6feb0301a633566153cbd0b93d11d8379", size = 152185, upload-time = "2025-11-20T20:28:56.327Z" } 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/5f/e1/5ef25f52973aa12a19cf4e1375d00932d7fb354ffd310487ba7d44225c1a/s3transfer-0.15.0-py3-none-any.whl", hash = "sha256:6f8bf5caa31a0865c4081186689db1b2534cef721d104eb26101de4b9d6a5852", size = 85984, upload-time = "2025-11-20T20:28:55.046Z" }, ] [[package]] From 8e725932c4f682a6d4726a4d6e8fb4a1859267ea Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 25 Nov 2025 03:00:34 +0000 Subject: [PATCH 181/185] Update https://github.com/renovatebot/github-action action to v44.0.4 --- .forgejo/workflows/renovate.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/renovate.yaml b/.forgejo/workflows/renovate.yaml index 82a9f0e..3c643b1 100644 --- a/.forgejo/workflows/renovate.yaml +++ b/.forgejo/workflows/renovate.yaml @@ -19,7 +19,7 @@ jobs: node-version: "24" - name: Renovate - uses: https://github.com/renovatebot/github-action@v44.0.3 + uses: https://github.com/renovatebot/github-action@v44.0.4 with: token: ${{ secrets.RENOVATE_TOKEN }} env: From 0c09ae8eca4ee795a3e47e3098d92e60c6c1f7bb Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 25 Nov 2025 03:00:52 +0000 Subject: [PATCH 182/185] Update dependency sentry-sdk to >=2.46.0 --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0b3a632..35bb517 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ "pyjwt>=2.10.1", "requests>=2.32.5", "rules>=3.5", - "sentry-sdk[django]>=2.45.0", + "sentry-sdk[django]>=2.46.0", "urlman>=2.0.2", ] diff --git a/uv.lock b/uv.lock index 6f83304..b5c5039 100644 --- a/uv.lock +++ b/uv.lock @@ -1071,15 +1071,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.45.0" +version = "2.46.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/89/1561b3dc8e28bf7978d031893297e89be266f53650c87bb14a29406a9791/sentry_sdk-2.45.0.tar.gz", hash = "sha256:e9bbfe69d5f6742f48bad22452beffb525bbc5b797d817c7f1b1f7d210cdd271", size = 373631, upload-time = "2025-11-18T13:23:22.475Z" } +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/94/c6/039121a0355bc1b5bcceef0dabf211b021fd435d0ee5c46393717bb1c09f/sentry_sdk-2.45.0-py2.py3-none-any.whl", hash = "sha256:86c8ab05dc3e8666aece77a5c747b45b25aa1d5f35f06cde250608f495d50f23", size = 404791, upload-time = "2025-11-18T13:23:20.533Z" }, + { 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] @@ -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.45.0" }, + { name = "sentry-sdk", extras = ["django"], specifier = ">=2.46.0" }, { name = "urlman", specifier = ">=2.0.2" }, ] From c60a69a305b5d06748ad6fe5edb34349ecb201c9 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sun, 30 Nov 2025 03:01:16 +0000 Subject: [PATCH 183/185] Update dependency flake8-bugbear to >=25.11.29 --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 35bb517..4548d9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ dev = [ "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", diff --git a/uv.lock b/uv.lock index 7359b8a..c5c4af9 100644 --- a/uv.lock +++ b/uv.lock @@ -489,15 +489,15 @@ 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]] @@ -1159,7 +1159,7 @@ dev = [ { 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" }, From 0a47eedc9e980cdde39c40450a2eb6ad7bcbbb72 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 1 Dec 2025 03:01:20 +0000 Subject: [PATCH 184/185] Lock file maintenance --- uv.lock | 96 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/uv.lock b/uv.lock index 7359b8a..40172f4 100644 --- a/uv.lock +++ b/uv.lock @@ -86,30 +86,30 @@ wheels = [ [[package]] name = "boto3" -version = "1.41.2" +version = "1.42.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fa/81/2600e83ddd7cb1dac43d28fd39774434afcda0d85d730402192b1a9266a3/boto3-1.41.2.tar.gz", hash = "sha256:7054fbc61cadab383f40ea6d725013ba6c8f569641dddb14c0055e790280ad6c", size = 111593, upload-time = "2025-11-21T20:32:08.622Z" } +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/48/41/1ed7fdc3f124c1cf2df78e605588fa78a182410b832f5b71944a69436171/boto3-1.41.2-py3-none-any.whl", hash = "sha256:edcde82fdae4201aa690e3683f8e5b1a846cf1bbf79d03db4fa8a2f6f46dba9c", size = 139343, upload-time = "2025-11-21T20:32:07.147Z" }, + { 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.41.2" +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/c5/0b/6eb5dc752b240dd0b76d7e3257ae25b70683896d1789e7bfb78fba7c7c99/botocore-1.41.2.tar.gz", hash = "sha256:49a3e8f4c1a1759a687941fef8b36efd7bafcf63c1ef74aa75d6497eb4887c9c", size = 14660558, upload-time = "2025-11-21T20:31:58.785Z" } +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/77/4d/516ee2157c0686fbe48ca8b94dffc17a0c35040d4626761d74b1a43215c8/botocore-1.41.2-py3-none-any.whl", hash = "sha256:154052dfaa7292212f01c8fab822c76cd10a15a7e164e4c45e4634eb40214b90", size = 14324839, upload-time = "2025-11-21T20:31:56.236Z" }, + { 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]] @@ -489,26 +489,26 @@ 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]] @@ -1001,39 +1001,39 @@ wheels = [ [[package]] name = "rpds-py" -version = "0.29.0" +version = "0.30.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/33/23b3b3419b6a3e0f559c7c0d2ca8fc1b9448382b25245033788785921332/rpds_py-0.29.0.tar.gz", hash = "sha256:fe55fe686908f50154d1dc599232016e50c243b438c3b7432f24e2895b0e5359", size = 69359, upload-time = "2025-11-16T14:50:39.532Z" } +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/89/b1/0b1474e7899371d9540d3bbb2a499a3427ae1fc39c998563fe9035a1073b/rpds_py-0.29.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:394d27e4453d3b4d82bb85665dc1fcf4b0badc30fc84282defed71643b50e1a1", size = 363731, upload-time = "2025-11-16T14:49:26.683Z" }, - { url = "https://files.pythonhosted.org/packages/28/12/3b7cf2068d0a334ed1d7b385a9c3c8509f4c2bcba3d4648ea71369de0881/rpds_py-0.29.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55d827b2ae95425d3be9bc9a5838b6c29d664924f98146557f7715e331d06df8", size = 354343, upload-time = "2025-11-16T14:49:28.24Z" }, - { url = "https://files.pythonhosted.org/packages/eb/73/5afcf8924bc02a749416eda64e17ac9c9b28f825f4737385295a0e99b0c1/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc31a07ed352e5462d3ee1b22e89285f4ce97d5266f6d1169da1142e78045626", size = 385406, upload-time = "2025-11-16T14:49:29.943Z" }, - { url = "https://files.pythonhosted.org/packages/c8/37/5db736730662508535221737a21563591b6f43c77f2e388951c42f143242/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c4695dd224212f6105db7ea62197144230b808d6b2bba52238906a2762f1d1e7", size = 396162, upload-time = "2025-11-16T14:49:31.833Z" }, - { url = "https://files.pythonhosted.org/packages/70/0d/491c1017d14f62ce7bac07c32768d209a50ec567d76d9f383b4cfad19b80/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcae1770b401167f8b9e1e3f566562e6966ffa9ce63639916248a9e25fa8a244", size = 517719, upload-time = "2025-11-16T14:49:33.804Z" }, - { url = "https://files.pythonhosted.org/packages/d7/25/b11132afcb17cd5d82db173f0c8dab270ffdfaba43e5ce7a591837ae9649/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:90f30d15f45048448b8da21c41703b31c61119c06c216a1bf8c245812a0f0c17", size = 409498, upload-time = "2025-11-16T14:49:35.222Z" }, - { url = "https://files.pythonhosted.org/packages/0f/7d/e6543cedfb2e6403a1845710a5ab0e0ccf8fc288e0b5af9a70bfe2c12053/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44a91e0ab77bdc0004b43261a4b8cd6d6b451e8d443754cfda830002b5745b32", size = 382743, upload-time = "2025-11-16T14:49:36.704Z" }, - { url = "https://files.pythonhosted.org/packages/75/11/a4ebc9f654293ae9fefb83b2b6be7f3253e85ea42a5db2f77d50ad19aaeb/rpds_py-0.29.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:4aa195e5804d32c682e453b34474f411ca108e4291c6a0f824ebdc30a91c973c", size = 400317, upload-time = "2025-11-16T14:49:39.132Z" }, - { url = "https://files.pythonhosted.org/packages/52/18/97677a60a81c7f0e5f64e51fb3f8271c5c8fcabf3a2df18e97af53d7c2bf/rpds_py-0.29.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7971bdb7bf4ee0f7e6f67fa4c7fbc6019d9850cc977d126904392d363f6f8318", size = 416979, upload-time = "2025-11-16T14:49:40.575Z" }, - { url = "https://files.pythonhosted.org/packages/f0/69/28ab391a9968f6c746b2a2db181eaa4d16afaa859fedc9c2f682d19f7e18/rpds_py-0.29.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8ae33ad9ce580c7a47452c3b3f7d8a9095ef6208e0a0c7e4e2384f9fc5bf8212", size = 567288, upload-time = "2025-11-16T14:49:42.24Z" }, - { url = "https://files.pythonhosted.org/packages/3b/d3/0c7afdcdb830eee94f5611b64e71354ffe6ac8df82d00c2faf2bfffd1d4e/rpds_py-0.29.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c661132ab2fb4eeede2ef69670fd60da5235209874d001a98f1542f31f2a8a94", size = 593157, upload-time = "2025-11-16T14:49:43.782Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ac/a0fcbc2feed4241cf26d32268c195eb88ddd4bd862adfc9d4b25edfba535/rpds_py-0.29.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bb78b3a0d31ac1bde132c67015a809948db751cb4e92cdb3f0b242e430b6ed0d", size = 554741, upload-time = "2025-11-16T14:49:45.557Z" }, - { url = "https://files.pythonhosted.org/packages/0f/f1/fcc24137c470df8588674a677f33719d5800ec053aaacd1de8a5d5d84d9e/rpds_py-0.29.0-cp314-cp314-win32.whl", hash = "sha256:f475f103488312e9bd4000bc890a95955a07b2d0b6e8884aef4be56132adbbf1", size = 215508, upload-time = "2025-11-16T14:49:47.562Z" }, - { url = "https://files.pythonhosted.org/packages/7b/c7/1d169b2045512eac019918fc1021ea07c30e84a4343f9f344e3e0aa8c788/rpds_py-0.29.0-cp314-cp314-win_amd64.whl", hash = "sha256:b9cf2359a4fca87cfb6801fae83a76aedf66ee1254a7a151f1341632acf67f1b", size = 228125, upload-time = "2025-11-16T14:49:49.064Z" }, - { url = "https://files.pythonhosted.org/packages/be/36/0cec88aaba70ec4a6e381c444b0d916738497d27f0c30406e3d9fcbd3bc2/rpds_py-0.29.0-cp314-cp314-win_arm64.whl", hash = "sha256:9ba8028597e824854f0f1733d8b964e914ae3003b22a10c2c664cb6927e0feb9", size = 221992, upload-time = "2025-11-16T14:49:50.777Z" }, - { url = "https://files.pythonhosted.org/packages/b1/fa/a2e524631717c9c0eb5d90d30f648cfba6b731047821c994acacb618406c/rpds_py-0.29.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:e71136fd0612556b35c575dc2726ae04a1669e6a6c378f2240312cf5d1a2ab10", size = 366425, upload-time = "2025-11-16T14:49:52.691Z" }, - { url = "https://files.pythonhosted.org/packages/a2/a4/6d43ebe0746ff694a30233f63f454aed1677bd50ab7a59ff6b2bb5ac61f2/rpds_py-0.29.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:76fe96632d53f3bf0ea31ede2f53bbe3540cc2736d4aec3b3801b0458499ef3a", size = 355282, upload-time = "2025-11-16T14:49:54.292Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a7/52fd8270e0320b09eaf295766ae81dd175f65394687906709b3e75c71d06/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9459a33f077130dbb2c7c3cea72ee9932271fb3126404ba2a2661e4fe9eb7b79", size = 384968, upload-time = "2025-11-16T14:49:55.857Z" }, - { url = "https://files.pythonhosted.org/packages/f4/7d/e6bc526b7a14e1ef80579a52c1d4ad39260a058a51d66c6039035d14db9d/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5c9546cfdd5d45e562cc0444b6dddc191e625c62e866bf567a2c69487c7ad28a", size = 394714, upload-time = "2025-11-16T14:49:57.343Z" }, - { url = "https://files.pythonhosted.org/packages/c0/3f/f0ade3954e7db95c791e7eaf978aa7e08a756d2046e8bdd04d08146ed188/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12597d11d97b8f7e376c88929a6e17acb980e234547c92992f9f7c058f1a7310", size = 520136, upload-time = "2025-11-16T14:49:59.162Z" }, - { url = "https://files.pythonhosted.org/packages/87/b3/07122ead1b97009715ab9d4082be6d9bd9546099b2b03fae37c3116f72be/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28de03cf48b8a9e6ec10318f2197b83946ed91e2891f651a109611be4106ac4b", size = 409250, upload-time = "2025-11-16T14:50:00.698Z" }, - { url = "https://files.pythonhosted.org/packages/c9/c6/dcbee61fd1dc892aedcb1b489ba661313101aa82ec84b1a015d4c63ebfda/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd7951c964069039acc9d67a8ff1f0a7f34845ae180ca542b17dc1456b1f1808", size = 384940, upload-time = "2025-11-16T14:50:02.312Z" }, - { url = "https://files.pythonhosted.org/packages/47/11/914ecb6f3574cf9bf8b38aced4063e0f787d6e1eb30b181a7efbc6c1da9a/rpds_py-0.29.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:c07d107b7316088f1ac0177a7661ca0c6670d443f6fe72e836069025e6266761", size = 399392, upload-time = "2025-11-16T14:50:03.829Z" }, - { url = "https://files.pythonhosted.org/packages/f5/fd/2f4bd9433f58f816434bb934313584caa47dbc6f03ce5484df8ac8980561/rpds_py-0.29.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1de2345af363d25696969befc0c1688a6cb5e8b1d32b515ef84fc245c6cddba3", size = 416796, upload-time = "2025-11-16T14:50:05.558Z" }, - { url = "https://files.pythonhosted.org/packages/79/a5/449f0281af33efa29d5c71014399d74842342ae908d8cd38260320167692/rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:00e56b12d2199ca96068057e1ae7f9998ab6e99cda82431afafd32f3ec98cca9", size = 566843, upload-time = "2025-11-16T14:50:07.243Z" }, - { url = "https://files.pythonhosted.org/packages/ab/32/0a6a1ccee2e37fcb1b7ba9afde762b77182dbb57937352a729c6cd3cf2bb/rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3919a3bbecee589300ed25000b6944174e07cd20db70552159207b3f4bbb45b8", size = 593956, upload-time = "2025-11-16T14:50:09.029Z" }, - { url = "https://files.pythonhosted.org/packages/4a/3d/eb820f95dce4306f07a495ede02fb61bef36ea201d9137d4fcd5ab94ec1e/rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7fa2ccc312bbd91e43aa5e0869e46bc03278a3dddb8d58833150a18b0f0283a", size = 557288, upload-time = "2025-11-16T14:50:10.73Z" }, - { url = "https://files.pythonhosted.org/packages/e9/f8/b8ff786f40470462a252918e0836e0db903c28e88e3eec66bc4a7856ee5d/rpds_py-0.29.0-cp314-cp314t-win32.whl", hash = "sha256:97c817863ffc397f1e6a6e9d2d89fe5408c0a9922dac0329672fb0f35c867ea5", size = 211382, upload-time = "2025-11-16T14:50:12.827Z" }, - { url = "https://files.pythonhosted.org/packages/c9/7f/1a65ae870bc9d0576aebb0c501ea5dccf1ae2178fe2821042150ebd2e707/rpds_py-0.29.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2023473f444752f0f82a58dfcbee040d0a1b3d1b3c2ec40e884bd25db6d117d2", size = 225919, upload-time = "2025-11-16T14:50:14.734Z" }, + { 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,14 +1059,14 @@ wheels = [ [[package]] name = "s3transfer" -version = "0.15.0" +version = "0.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ca/bb/940d6af975948c1cc18f44545ffb219d3c35d78ec972b42ae229e8e37e08/s3transfer-0.15.0.tar.gz", hash = "sha256:d36fac8d0e3603eff9b5bfa4282c7ce6feb0301a633566153cbd0b93d11d8379", size = 152185, upload-time = "2025-11-20T20:28:56.327Z" } +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/5f/e1/5ef25f52973aa12a19cf4e1375d00932d7fb354ffd310487ba7d44225c1a/s3transfer-0.15.0-py3-none-any.whl", hash = "sha256:6f8bf5caa31a0865c4081186689db1b2534cef721d104eb26101de4b9d6a5852", size = 85984, upload-time = "2025-11-20T20:28:55.046Z" }, + { 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]] @@ -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]] From 0aebbc42f0b515e94467af35e5552955764874ba Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 2 Dec 2025 03:01:04 +0000 Subject: [PATCH 185/185] Update https://github.com/renovatebot/github-action action to v44.0.5 --- .forgejo/workflows/renovate.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/renovate.yaml b/.forgejo/workflows/renovate.yaml index 3c643b1..a2577d9 100644 --- a/.forgejo/workflows/renovate.yaml +++ b/.forgejo/workflows/renovate.yaml @@ -19,7 +19,7 @@ jobs: node-version: "24" - name: Renovate - uses: https://github.com/renovatebot/github-action@v44.0.4 + uses: https://github.com/renovatebot/github-action@v44.0.5 with: token: ${{ secrets.RENOVATE_TOKEN }} env: