diff --git a/.github/workflows/containers-openstack.yaml b/.github/workflows/containers-openstack.yaml index d31689eef..fc56555ec 100644 --- a/.github/workflows/containers-openstack.yaml +++ b/.github/workflows/containers-openstack.yaml @@ -36,6 +36,7 @@ jobs: - cinder - glance - horizon + - neutron - octavia - openstack-client - skyline @@ -44,51 +45,5 @@ jobs: with: container_name: ${{ matrix.project }} dockerfile_path: containers/${{ matrix.project }}/Dockerfile - build_args: OPENSTACK_VERSION=2025.2 - latest_name: "2025.2" - - placement: - uses: ./.github/workflows/build-container-reuse.yaml - secrets: inherit - with: - container_name: placement - dockerfile_path: containers/placement/Dockerfile - build_args: OPENSTACK_VERSION=2026.1 - latest_name: "2026.1" - - nova: - uses: ./.github/workflows/build-container-reuse.yaml - secrets: inherit - with: - container_name: nova - dockerfile_path: containers/nova/Dockerfile - build_args: OPENSTACK_VERSION=2026.1 - latest_name: "2026.1" - - neutron: - uses: ./.github/workflows/build-container-reuse.yaml - secrets: inherit - with: - container_name: neutron - dockerfile_path: containers/neutron/Dockerfile - build_args: OPENSTACK_VERSION=2026.1 - latest_name: "2026.1" - - ironic: - uses: ./.github/workflows/build-container-reuse.yaml - secrets: inherit - with: - container_name: ironic - dockerfile_path: containers/ironic/Dockerfile - build_args: OPENSTACK_VERSION=2026.1 - latest_name: "2026.1" - target: final - - keystone: - uses: ./.github/workflows/build-container-reuse.yaml - secrets: inherit - with: - container_name: keystone - dockerfile_path: containers/keystone/Dockerfile build_args: OPENSTACK_VERSION=2026.1 latest_name: "2026.1" diff --git a/ansible/roles/keystone_bootstrap/defaults/main.yml b/ansible/roles/keystone_bootstrap/defaults/main.yml index 4b68e7e3e..46791bb75 100644 --- a/ansible/roles/keystone_bootstrap/defaults/main.yml +++ b/ansible/roles/keystone_bootstrap/defaults/main.yml @@ -68,3 +68,32 @@ keystone_bootstrap_groups: # role: member # - project: shared-services # role: reader + +# domain in which k8s service account groups are created +keystone_bootstrap_k8s_group_domain: service + +# k8s identity providers; override with your cluster issuers +keystone_k8s_identity: + providers: [] + +# k8s service account groups and their project role assignments +# Each group can have project_roles with an optional domain to scope the project lookup +keystone_bootstrap_k8s_groups: + - name: k8s-serviceaccounts + desc: 'Kubernetes Service Accounts' + project_roles: + - project: service + domain: service + role: admin + - project: service + domain: service + role: service + - project: baremetal + domain: infra + role: admin + - project: baremetal + domain: infra + role: service + - project: admin + domain: Default + role: admin diff --git a/ansible/roles/keystone_bootstrap/tasks/k8s.yml b/ansible/roles/keystone_bootstrap/tasks/k8s.yml new file mode 100644 index 000000000..a8f05671a --- /dev/null +++ b/ansible/roles/keystone_bootstrap/tasks/k8s.yml @@ -0,0 +1,54 @@ +--- +# Copyright (c) 2026 Rackspace Technology, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +- name: Create k8s mapping + openstack.cloud.federation_mapping: + name: k8s-mapping + rules: + - local: + - user: + name: '{0}' + group: + domain: + name: service + name: k8s-serviceaccounts + remote: + - type: HTTP_OIDC_SUB + +- name: Create k8s identity providers + openstack.cloud.federation_idp: + name: "{{ item.name }}" + is_enabled: true + remote_ids: + - "{{ item.issuer }}" + loop: "{{ keystone_k8s_identity.providers }}" + +- name: Create k8s openid protocols + openstack.cloud.keystone_federation_protocol: + name: openid + idp: "{{ item.name }}" + mapping: k8s-mapping + loop: "{{ keystone_k8s_identity.providers }}" + +- name: Get k8s group domain info + openstack.cloud.identity_domain_info: + name: "{{ keystone_bootstrap_k8s_group_domain }}" + register: _k8s_group_domain + +- name: Create k8s service account group mappings + ansible.builtin.include_tasks: k8s_group.yml + loop: "{{ keystone_bootstrap_k8s_groups }}" + loop_control: + loop_var: group_item diff --git a/ansible/roles/keystone_bootstrap/tasks/k8s_group.yml b/ansible/roles/keystone_bootstrap/tasks/k8s_group.yml new file mode 100644 index 000000000..7049950ef --- /dev/null +++ b/ansible/roles/keystone_bootstrap/tasks/k8s_group.yml @@ -0,0 +1,34 @@ +--- +# Copyright (c) 2026 Rackspace Technology, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +- name: Create k8s group + openstack.cloud.identity_group: + name: "{{ group_item.name }}" + domain_id: "{{ _k8s_group_domain.domains[0].id }}" + description: "{{ group_item.desc }}" + state: present + register: _k8s_group + +- name: Assign role to k8s group for project + openstack.cloud.role_assignment: + group: "{{ _k8s_group.group.id }}" + project: "{{ role_item.project }}" + project_domain: "{{ role_item.domain | default(omit) }}" + role: "{{ role_item.role }}" + state: present + loop: "{{ group_item.project_roles | default([]) }}" + loop_control: + loop_var: role_item + when: dont_set_roles is not defined diff --git a/ansible/roles/keystone_bootstrap/tasks/main.yml b/ansible/roles/keystone_bootstrap/tasks/main.yml index 82c1caf8f..4365796fc 100644 --- a/ansible/roles/keystone_bootstrap/tasks/main.yml +++ b/ansible/roles/keystone_bootstrap/tasks/main.yml @@ -10,5 +10,8 @@ - name: Define SSO ansible.builtin.include_tasks: sso.yml +- name: Define k8s service account groups + ansible.builtin.include_tasks: k8s.yml + - name: Define misc keystone ansible.builtin.include_tasks: misc.yml diff --git a/charts/argocd-understack/templates/application-oidc-rbac.yaml b/charts/argocd-understack/templates/application-oidc-rbac.yaml new file mode 100644 index 000000000..0e0db3b8a --- /dev/null +++ b/charts/argocd-understack/templates/application-oidc-rbac.yaml @@ -0,0 +1,30 @@ +{{- if or (eq (include "understack.isEnabled" (list $.Values.global "oidc_rbac")) "true") (eq (include "understack.isEnabled" (list $.Values.site "oidc_rbac")) "true") }} +--- +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: {{ printf "%s-%s" $.Release.Name "oidc-rbac" }} + finalizers: + - resources-finalizer.argocd.argoproj.io + annotations: + argocd.argoproj.io/compare-options: ServerSideDiff=true,IncludeMutationWebhook=true +{{- include "understack.appLabelsBlock" $ | nindent 2 }} +spec: + destination: + namespace: kube-system + server: {{ $.Values.cluster_server }} + project: understack-infra + sources: + - path: components/oidc-rbac + ref: understack + repoURL: {{ include "understack.understack_url" $ }} + targetRevision: {{ include "understack.understack_ref" $ }} + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - ServerSideApply=true + - RespectIgnoreDifferences=true + - ApplyOutOfSyncOnly=true +{{- end }} diff --git a/charts/argocd-understack/values.yaml b/charts/argocd-understack/values.yaml index abf3a677f..50bfc7ced 100644 --- a/charts/argocd-understack/values.yaml +++ b/charts/argocd-understack/values.yaml @@ -146,7 +146,7 @@ global: enabled: false # -- config file to use for Nautobot scoped to either $understack or $deploy repo # @default -- $understack/components/nautobot/nautobot_config.py - nautobot_config: '$understack/components/nautobot/nautobot_config.py' + nautobot_config: "$understack/components/nautobot/nautobot_config.py" # -- Nautobot API token generation jobs nautobot_api_tokens: @@ -169,6 +169,12 @@ global: # @default -- 0.0.3 chartVersion: 0.0.3 + # -- OIDC RBAC (ClusterRoleBindings for OIDC service account issuer discovery) + oidc_rbac: + # -- Enable/disable deploying OIDC RBAC + # @default -- false + enabled: false + # -- OpenEBS openebs: # -- Enable/disable deploying OpenEBS @@ -229,7 +235,6 @@ global: # @default -- false enabled: false - # -- This block is for setting up the UnderStack site specific ArgoCD Applications site: # -- Enable/disable deploying the site specific applications @@ -341,9 +346,8 @@ site: wave: 1 # -- Chart version for Keystone # renovate: datasource=helm depName=keystone registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.5+a2a343968 + chartVersion: 2026.1.10+a659ab8a2 # -- Glance (Image Service) - # -- Glance (Image Service) glance: # -- Enable/disable deploying Glance # @default -- false @@ -353,7 +357,7 @@ site: wave: 2 # -- Chart version for Glance # renovate: datasource=helm depName=glance registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.5+bad57716b + chartVersion: 2026.1.11+a659ab8a2 # -- Cinder (Block Storage Service) cinder: @@ -365,8 +369,7 @@ site: wave: 2 # -- Chart version for Cinder # renovate: datasource=helm depName=cinder registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2025.1.3+abd55b4a7 - + chartVersion: 2026.1.14+a659ab8a2 # -- Ironic (Bare Metal Service) ironic: # -- Enable/disable deploying Ironic @@ -377,8 +380,7 @@ site: wave: 2 # -- Chart version for Ironic # renovate: datasource=helm depName=ironic registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2025.2.23+ea0d1ecda - + chartVersion: 2026.1.10+a659ab8a2 # -- Neutron (Networking Service) neutron: # -- Enable/disable deploying Neutron @@ -389,7 +391,7 @@ site: wave: 2 # -- Chart version for Neutron # renovate: datasource=helm depName=neutron registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.8+872fd69e7 + chartVersion: 2026.1.15+a659ab8a2 # -- Placement (Placement Service) placement: @@ -401,7 +403,7 @@ site: wave: 2 # -- Chart version for Placement # renovate: datasource=helm depName=placement registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.4+a2a343968 + chartVersion: 2026.1.7+a659ab8a2 # -- Nova (Compute Service) nova: @@ -413,7 +415,7 @@ site: wave: 3 # -- Chart version for Nova # renovate: datasource=helm depName=nova registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.8+a2a343968 + chartVersion: 2026.1.13+a659ab8a2 # -- Octavia (Load Balancer Service) octavia: @@ -425,7 +427,7 @@ site: wave: 3 # -- Chart version for Octavia # renovate: datasource=helm depName=octavia registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2025.2.8+01c93d867 + chartVersion: 2026.1.7+a659ab8a2 # -- Horizon (Dashboard) horizon: @@ -437,7 +439,7 @@ site: wave: 4 # -- Chart version for Horizon # renovate: datasource=helm depName=horizon registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2025.2.4+01c93d867 + chartVersion: 2026.1.8+a659ab8a2 # -- Skyline (Dashboard) skyline: @@ -449,7 +451,7 @@ site: wave: 4 # -- Chart version for Skyline # renovate: datasource=helm depName=skyline registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2025.2.5+01c93d867 + chartVersion: 2026.1.3+a659ab8a2 # -- Open vSwitch (Networking) openvswitch: @@ -508,6 +510,12 @@ site: # @default -- false enabled: false + # -- OIDC RBAC (ClusterRoleBindings for OIDC service account issuer discovery) + oidc_rbac: + # -- Enable/disable deploying OIDC RBAC + # @default -- false + enabled: false + # -- OpenEBS openebs: # -- Enable/disable deploying OpenEBS @@ -575,7 +583,7 @@ site: enabled: false # -- config file to use for Nautobot scoped to either $understack or $deploy repo # @default -- $understack/components/nautobot/nautobot_config.py - nautobot_config: '$understack/components/nautobot/nautobot_config.py' + nautobot_config: "$understack/components/nautobot/nautobot_config.py" # -- Ironic hardware exporter for bare-metal hardware metrics via RabbitMQ ironic_hardware_exporter: diff --git a/components/cinder/values.yaml b/components/cinder/values.yaml index 3a5099a75..43f420a0f 100644 --- a/components/cinder/values.yaml +++ b/components/cinder/values.yaml @@ -73,32 +73,11 @@ pod: volumeMounts: - mountPath: /var/lib/cinder name: var-lib-cinder - - name: cinder-etc-snippets - mountPath: /etc/cinder/cinder.conf.d/ - readOnly: true volumes: - name: var-lib-cinder emptyDir: {} - - name: cinder-etc-snippets - projected: - sources: - - secret: - name: cinder-ks-etc - - secret: - name: cinder-netapp-config - optional: true - cinder_volume_usage_audit: - cinder_volume_usage_audit: - volumeMounts: - - name: cinder-etc-snippets - mountPath: /etc/cinder/cinder.conf.d/ - readOnly: true - volumes: - - name: cinder-etc-snippets - projected: - sources: - - secret: - name: cinder-ks-etc + # cinder_db_sync has no native etcSources support in the chart, so the + # cinder-etc-snippets mount still has to be injected manually here. cinder_db_sync: cinder_db_sync: volumeMounts: @@ -111,54 +90,30 @@ pod: sources: - secret: name: cinder-ks-etc - cinder_backup: - cinder_backup: - volumeMounts: - - name: cinder-etc-snippets - mountPath: /etc/cinder/cinder.conf.d/ - readOnly: true - volumes: - - name: cinder-etc-snippets - projected: - sources: - - secret: - name: cinder-ks-etc + # cinder-ks-etc mounting for the other components is handled natively via + # pod.etcSources below, rather than duplicating it through pod.mounts. + etcSources: + cinder_api: + - secret: + name: cinder-ks-etc cinder_scheduler: - cinder_scheduler: - volumeMounts: - - name: cinder-etc-snippets - mountPath: /etc/cinder/cinder.conf.d/ - readOnly: true - volumes: - - name: cinder-etc-snippets - projected: - sources: - - secret: - name: cinder-ks-etc + - secret: + name: cinder-ks-etc + cinder_volume: + - secret: + name: cinder-ks-etc + - secret: + name: cinder-netapp-config + optional: true + cinder_volume_usage_audit: + - secret: + name: cinder-ks-etc + cinder_backup: + - secret: + name: cinder-ks-etc cinder_db_purge: - cinder_db_purge: - volumeMounts: - - name: cinder-etc-snippets - mountPath: /etc/cinder/cinder.conf.d/ - readOnly: true - volumes: - - name: cinder-etc-snippets - projected: - sources: - - secret: - name: cinder-ks-etc - cinder_api: - cinder_api: - volumeMounts: - - name: cinder-etc-snippets - mountPath: /etc/cinder/cinder.conf.d/ - readOnly: true - volumes: - - name: cinder-etc-snippets - projected: - sources: - - secret: - name: cinder-ks-etc + - secret: + name: cinder-ks-etc lifecycle: disruption_budget: deployments: diff --git a/components/images-openstack.yaml b/components/images-openstack.yaml index d63aa6968..7fae0af94 100644 --- a/components/images-openstack.yaml +++ b/components/images-openstack.yaml @@ -6,66 +6,67 @@ images: tags: # these are common across all these OpenStack Helm installations bootstrap: "ghcr.io/rackerlabs/understack/ansible:latest" - db_init: "ghcr.io/rackerlabs/understack/openstack-client:2025.2" - db_drop: "ghcr.io/rackerlabs/understack/openstack-client:2025.2" - ks_user: "ghcr.io/rackerlabs/understack/openstack-client:2025.2" - ks_service: "ghcr.io/rackerlabs/understack/openstack-client:2025.2" - ks_endpoints: "ghcr.io/rackerlabs/understack/openstack-client:2025.2" + db_init: "ghcr.io/rackerlabs/understack/openstack-client:pr-1876" + db_drop: "ghcr.io/rackerlabs/understack/openstack-client:pr-1876" + ks_user: "ghcr.io/rackerlabs/understack/openstack-client:pr-1876" + ks_service: "ghcr.io/rackerlabs/understack/openstack-client:pr-1876" + ks_endpoints: "ghcr.io/rackerlabs/understack/openstack-client:pr-1876" # keystone - keystone_api: "ghcr.io/rackerlabs/understack/keystone:2026.1" - keystone_credential_rotate: "ghcr.io/rackerlabs/understack/keystone:2026.1" - keystone_credential_setup: "ghcr.io/rackerlabs/understack/keystone:2026.1" - keystone_db_sync: "ghcr.io/rackerlabs/understack/keystone:2026.1" - keystone_domain_manage: "ghcr.io/rackerlabs/understack/keystone:2026.1" - keystone_fernet_rotate: "ghcr.io/rackerlabs/understack/keystone:2026.1" - keystone_fernet_setup: "ghcr.io/rackerlabs/understack/keystone:2026.1" + keystone_api: "ghcr.io/rackerlabs/understack/keystone:pr-1876" + keystone_credential_rotate: "ghcr.io/rackerlabs/understack/keystone:pr-1876" + keystone_credential_setup: "ghcr.io/rackerlabs/understack/keystone:pr-1876" + keystone_db_sync: "ghcr.io/rackerlabs/understack/keystone:pr-1876" + keystone_domain_manage: "ghcr.io/rackerlabs/understack/keystone:pr-1876" + keystone_fernet_rotate: "ghcr.io/rackerlabs/understack/keystone:pr-1876" + keystone_fernet_setup: "ghcr.io/rackerlabs/understack/keystone:pr-1876" # ironic - ironic_api: "ghcr.io/rackerlabs/understack/ironic:2026.1" - ironic_conductor: "ghcr.io/rackerlabs/understack/ironic:2026.1" - ironic_pxe: "ghcr.io/rackerlabs/understack/ironic:2026.1" - ironic_pxe_init: "ghcr.io/rackerlabs/understack/ironic:2026.1" + ironic_api: "ghcr.io/rackerlabs/understack/ironic:pr-1876" + ironic_conductor: "ghcr.io/rackerlabs/understack/ironic:pr-1876" + ironic_pxe: "ghcr.io/rackerlabs/understack/ironic:pr-1876" + ironic_pxe_init: "ghcr.io/rackerlabs/understack/ironic:pr-1876" ironic_pxe_http: "docker.io/nginx:1.29.8" - ironic_db_sync: "ghcr.io/rackerlabs/understack/ironic:2026.1" + ironic_db_sync: "ghcr.io/rackerlabs/understack/ironic:pr-1876" # these want curl which apparently is in the openstack-client image - ironic_manage_cleaning_network: "ghcr.io/rackerlabs/understack/openstack-client:2025.2" - ironic_retrive_cleaning_network: "ghcr.io/rackerlabs/understack/openstack-client:2025.2" - ironic_retrive_swift_config: "ghcr.io/rackerlabs/understack/openstack-client:2025.2" + ironic_manage_cleaning_network: "ghcr.io/rackerlabs/understack/openstack-client:pr-1876" + ironic_retrive_cleaning_network: "ghcr.io/rackerlabs/understack/openstack-client:pr-1876" + ironic_retrive_swift_config: "ghcr.io/rackerlabs/understack/openstack-client:pr-1876" # neutron - neutron_db_sync: "ghcr.io/rackerlabs/understack/neutron:2026.1" - neutron_dhcp: "ghcr.io/rackerlabs/understack/neutron:2026.1" - neutron_l3: "ghcr.io/rackerlabs/understack/neutron:2026.1" - neutron_l2gw: "ghcr.io/rackerlabs/understack/neutron:2026.1" - neutron_linuxbridge_agent: "ghcr.io/rackerlabs/understack/neutron:2026.1" - neutron_metadata: "ghcr.io/rackerlabs/understack/neutron:2026.1" - neutron_ovn_metadata: "ghcr.io/rackerlabs/understack/neutron:2026.1" - neutron_openvswitch_agent: "ghcr.io/rackerlabs/understack/neutron:2026.1" - neutron_server: "ghcr.io/rackerlabs/understack/neutron:2026.1" - neutron_rpc_server: "ghcr.io/rackerlabs/understack/neutron:2026.1" - neutron_bagpipe_bgp: "ghcr.io/rackerlabs/understack/neutron:2026.1" - neutron_netns_cleanup_cron: "ghcr.io/rackerlabs/understack/neutron:2026.1" + neutron_db_sync: "ghcr.io/rackerlabs/understack/neutron:pr-1876" + neutron_dhcp: "ghcr.io/rackerlabs/understack/neutron:pr-1876" + neutron_l3: "ghcr.io/rackerlabs/understack/neutron:pr-1876" + neutron_l2gw: "ghcr.io/rackerlabs/understack/neutron:pr-1876" + neutron_linuxbridge_agent: "ghcr.io/rackerlabs/understack/neutron:pr-1876" + neutron_ironic_agent: "ghcr.io/rackerlabs/understack/neutron:pr-1876" + neutron_metadata: "ghcr.io/rackerlabs/understack/neutron:pr-1876" + neutron_ovn_metadata: "ghcr.io/rackerlabs/understack/neutron:pr-1876" + neutron_openvswitch_agent: "ghcr.io/rackerlabs/understack/neutron:pr-1876" + neutron_server: "ghcr.io/rackerlabs/understack/neutron:pr-1876" + neutron_rpc_server: "ghcr.io/rackerlabs/understack/neutron:pr-1876" + neutron_bagpipe_bgp: "ghcr.io/rackerlabs/understack/neutron:pr-1876" + neutron_netns_cleanup_cron: "ghcr.io/rackerlabs/understack/neutron:pr-1876" # nova - nova_api: "ghcr.io/rackerlabs/understack/nova:2026.1" - nova_cell_setup: "ghcr.io/rackerlabs/understack/nova:2026.1" - nova_cell_setup_init: "ghcr.io/rackerlabs/understack/openstack-client:2025.2" - nova_compute: "ghcr.io/rackerlabs/understack/nova:2026.1" - nova_compute_ironic: "ghcr.io/rackerlabs/understack/nova:2026.1" - nova_compute_ssh: "ghcr.io/rackerlabs/understack/nova:2026.1" - nova_conductor: "ghcr.io/rackerlabs/understack/nova:2026.1" - nova_db_sync: "ghcr.io/rackerlabs/understack/nova:2026.1" - nova_novncproxy: "ghcr.io/rackerlabs/understack/nova:2026.1" - nova_novncproxy_assets: "ghcr.io/rackerlabs/understack/nova:2026.1" - nova_scheduler: "ghcr.io/rackerlabs/understack/nova:2026.1" - nova_spiceproxy: "ghcr.io/rackerlabs/understack/nova:2026.1" - nova_spiceproxy_assets: "ghcr.io/rackerlabs/understack/nova:2026.1" + nova_api: "ghcr.io/rackerlabs/understack/nova:pr-1876" + nova_cell_setup: "ghcr.io/rackerlabs/understack/nova:pr-1876" + nova_cell_setup_init: "ghcr.io/rackerlabs/understack/openstack-client:pr-1876" + nova_compute: "ghcr.io/rackerlabs/understack/nova:pr-1876" + nova_compute_ironic: "ghcr.io/rackerlabs/understack/nova:pr-1876" + nova_compute_ssh: "ghcr.io/rackerlabs/understack/nova:pr-1876" + nova_conductor: "ghcr.io/rackerlabs/understack/nova:pr-1876" + nova_db_sync: "ghcr.io/rackerlabs/understack/nova:pr-1876" + nova_novncproxy: "ghcr.io/rackerlabs/understack/nova:pr-1876" + nova_novncproxy_assets: "ghcr.io/rackerlabs/understack/nova:pr-1876" + nova_scheduler: "ghcr.io/rackerlabs/understack/nova:pr-1876" + nova_spiceproxy: "ghcr.io/rackerlabs/understack/nova:pr-1876" + nova_spiceproxy_assets: "ghcr.io/rackerlabs/understack/nova:pr-1876" nova_service_cleaner: "docker.io/openstackhelm/ceph-config-helper:latest-ubuntu_jammy" # placement - placement: "ghcr.io/rackerlabs/understack/placement:2026.1" - placement_db_sync: "ghcr.io/rackerlabs/understack/placement:2026.1" + placement: "ghcr.io/rackerlabs/understack/placement:pr-1876" + placement_db_sync: "ghcr.io/rackerlabs/understack/placement:pr-1876" # openvswitch openvswitch_db_server: "docker.io/openstackhelm/openvswitch:ubuntu_jammy-dpdk-20250127" @@ -78,36 +79,36 @@ images: ovn_controller: "docker.io/openstackhelm/ovn:ubuntu_jammy-20250111" # horizon - horizon: "ghcr.io/rackerlabs/understack/horizon:2025.2" - horizon_db_sync: "ghcr.io/rackerlabs/understack/horizon:2025.2" + horizon: "ghcr.io/rackerlabs/understack/horizon:pr-1876" + horizon_db_sync: "ghcr.io/rackerlabs/understack/horizon:pr-1876" # glance - glance_api: "ghcr.io/rackerlabs/understack/glance:2026.1" - glance_db_sync: "ghcr.io/rackerlabs/understack/glance:2026.1" - glance_metadefs_load: "ghcr.io/rackerlabs/understack/glance:2026.1" + glance_api: "ghcr.io/rackerlabs/understack/glance:pr-1876" + glance_db_sync: "ghcr.io/rackerlabs/understack/glance:pr-1876" + glance_metadefs_load: "ghcr.io/rackerlabs/understack/glance:pr-1876" glance_storage_init: "docker.io/openstackhelm/ceph-config-helper:latest-ubuntu_jammy" # skyline - skyline: "ghcr.io/rackerlabs/understack/skyline:2025.2" - skyline_db_sync: "ghcr.io/rackerlabs/understack/skyline:2025.2" - skyline_nginx: "ghcr.io/rackerlabs/understack/skyline:2025.2" + skyline: "ghcr.io/rackerlabs/understack/skyline:pr-1876" + skyline_db_sync: "ghcr.io/rackerlabs/understack/skyline:pr-1876" + skyline_nginx: "ghcr.io/rackerlabs/understack/skyline:pr-1876" # cinder - cinder_api: "ghcr.io/rackerlabs/understack/cinder:2025.2" - cinder_db_sync: "ghcr.io/rackerlabs/understack/cinder:2025.2" - cinder_scheduler: "ghcr.io/rackerlabs/understack/cinder:2025.2" - cinder_volume: "ghcr.io/rackerlabs/understack/cinder:2025.2" - cinder_volume_usage_audit: "ghcr.io/rackerlabs/understack/cinder:2025.2" - cinder_db_purge: "ghcr.io/rackerlabs/understack/cinder:2025.2" - cinder_backup: "ghcr.io/rackerlabs/understack/cinder:2025.2" + cinder_api: "ghcr.io/rackerlabs/understack/cinder:pr-1876" + cinder_db_sync: "ghcr.io/rackerlabs/understack/cinder:pr-1876" + cinder_scheduler: "ghcr.io/rackerlabs/understack/cinder:pr-1876" + cinder_volume: "ghcr.io/rackerlabs/understack/cinder:pr-1876" + cinder_volume_usage_audit: "ghcr.io/rackerlabs/understack/cinder:pr-1876" + cinder_db_purge: "ghcr.io/rackerlabs/understack/cinder:pr-1876" + cinder_backup: "ghcr.io/rackerlabs/understack/cinder:pr-1876" cinder_storage_init: "docker.io/openstackhelm/ceph-config-helper:latest-ubuntu_jammy" cinder_backup_storage_init: "docker.io/openstackhelm/ceph-config-helper:latest-ubuntu_jammy" # octavia - octavia_api: "ghcr.io/rackerlabs/understack/octavia:2025.2" - octavia_db_sync: "ghcr.io/rackerlabs/understack/octavia:2025.2" - octavia_worker: "ghcr.io/rackerlabs/understack/octavia:2025.2" - octavia_housekeeping: "ghcr.io/rackerlabs/understack/octavia:2025.2" - octavia_health_manager: "ghcr.io/rackerlabs/understack/octavia:2025.2" - octavia_health_manager_init: "ghcr.io/rackerlabs/understack/openstack-client:2025.2" + octavia_api: "ghcr.io/rackerlabs/understack/octavia:pr-1876" + octavia_db_sync: "ghcr.io/rackerlabs/understack/octavia:pr-1876" + octavia_worker: "ghcr.io/rackerlabs/understack/octavia:pr-1876" + octavia_housekeeping: "ghcr.io/rackerlabs/understack/octavia:pr-1876" + octavia_health_manager: "ghcr.io/rackerlabs/understack/octavia:pr-1876" + octavia_health_manager_init: "ghcr.io/rackerlabs/understack/openstack-client:pr-1876" ... diff --git a/components/keystone/values.yaml b/components/keystone/values.yaml index 1eff67a9e..d1df1abbe 100644 --- a/components/keystone/values.yaml +++ b/components/keystone/values.yaml @@ -142,6 +142,8 @@ pod: - name: oidc-secret mountPath: /etc/oidc-secret readOnly: true + - name: oidc-metadata + readOnly: true volumes: - name: keystone-sso secret: @@ -149,6 +151,10 @@ pod: - name: oidc-secret secret: secretName: sso-passphrase + - name: oidc-metadata + configMap: + name: keystone-oidc-metadata + defaultMode: 0555 keystone_bootstrap: keystone_bootstrap: volumeMounts: @@ -402,3 +408,12 @@ annotations: # relies on services to be up so it can remain post argocd.argoproj.io/hook-delete-policy: BeforeHookCreation argocd.argoproj.io/sync-options: Force=true + +extraObjects: + - apiVersion: v1 + kind: ConfigMap + metadata: + name: keystone-oidc-metadata + namespace: openstack + data: + # Define your OIDC providers metadata in deploy repo diff --git a/components/oidc-rbac/kustomization.yaml b/components/oidc-rbac/kustomization.yaml new file mode 100644 index 000000000..51b635bc8 --- /dev/null +++ b/components/oidc-rbac/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - oidc-reviewer.yaml diff --git a/components/oidc-rbac/oidc-reviewer.yaml b/components/oidc-rbac/oidc-reviewer.yaml new file mode 100644 index 000000000..360f13ea3 --- /dev/null +++ b/components/oidc-rbac/oidc-reviewer.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: oidc-reviewer +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:service-account-issuer-discovery +subjects: +- apiGroup: rbac.authorization.k8s.io + kind: Group + name: system:unauthenticated diff --git a/components/openstack/templates/keystone-service-user.yaml.tpl b/components/openstack/templates/keystone-service-user.yaml.tpl index b4b2e0a3b..28d878d77 100644 --- a/components/openstack/templates/keystone-service-user.yaml.tpl +++ b/components/openstack/templates/keystone-service-user.yaml.tpl @@ -2,10 +2,62 @@ {{- range $serviceName, $users := .Values.keystoneServiceUsers.services }} {{- range $_, $user := $users }} --- +apiVersion: v1 +kind: Secret +metadata: + name: {{ (printf "%s-keystone-%s" $serviceName $user.usage) | quote }} +stringData: + OS_AUTH_TYPE: v3oidcaccesstokenfile + OS_AUTH_URL: {{ $.Values.keystoneUrl | quote }} + OS_DEFAULT_DOMAIN: "default" + OS_INTERFACE: {{ $.Values.keystoneServiceUsers.keystoneInterface | quote }} + OS_PROJECT_DOMAIN_NAME: {{ include "openstack.serviceuser.project_domain_name" $user | quote }} + OS_PROJECT_NAME: {{ include "openstack.serviceuser.project_name" $user | quote }} + OS_USER_DOMAIN_NAME: {{ include "openstack.serviceuser.user_domain_name" $user | quote }} + OS_REGION_NAME: {{ $.Values.regionName | quote }} + OS_IDENTITY_PROVIDER: {{ $.Values.keystoneServiceUsers.identityProvider }} + OS_PROTOCOL: openid + OS_ACCESS_TOKEN_FILE: {{ $.Values.keystoneServiceUsers.accessTokenFile }} +{{- end }} +{{- end }} +{{- range $serviceName, $users := .Values.keystoneServiceUsers.services }} +{{- if not (eq $serviceName "keystone") }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ (printf "%s-ks-etc" $serviceName) | quote }} +stringData: + {{ (printf "%s_auth.conf" $serviceName) | quote }}: | + {{- range $_, $user := $users }} + {{- $section := $user.section | default $user.usage }} + {{- $section := eq $section "user" | ternary "keystone_authtoken" $section -}} + {{- $shouldSkip := or (eq $user.usage "test") (eq $user.usage "admin") }} + {{- if not $shouldSkip }} + [{{ $section }}] + auth_type=v3oidcaccesstokenfile + auth_url={{ $.Values.keystoneUrl }} + identity_provider={{ $.Values.keystoneServiceUsers.keystoneIdentityProvider }} + protocol=openid + access_token_file=/var/run/secrets/kubernetes.io/serviceaccount/token + region_name={{ $.Values.regionName }} + project_domain_name={{ include "openstack.serviceuser.project_domain_name" $user }} + project_name={{ include "openstack.serviceuser.project_name" $user }} + {{- end }} + {{- end }} +{{- end }} +{{- end }} +{{- end }} +--- +# Keystone admin must still be placed in ES +{{- range $serviceName, $users := .Values.keystoneServiceUsers.services }} +{{- range $_, $user := $users }} + +{{- if eq $serviceName "keystone" }} apiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: - name: {{ (printf "%s-keystone-%s" $serviceName $user.usage) | quote }} + name: keystone-keystone-admin {{- if $.Values.keystoneServiceUsers.externalLinkAnnotationTemplate }} annotations: link.argocd.argoproj.io/external-link: {{ tpl $.Values.keystoneServiceUsers.externalLinkAnnotationTemplate (dict "remoteRef" $user.remoteRef) | quote }} @@ -34,50 +86,4 @@ spec: key: {{ $user.remoteRef | quote }} {{- end }} {{- end }} -{{- range $serviceName, $users := .Values.keystoneServiceUsers.services }} -{{- if not (eq $serviceName "keystone") }} ---- -apiVersion: external-secrets.io/v1 -kind: ExternalSecret -metadata: - name: {{ (printf "%s-ks-etc" $serviceName) | quote }} -spec: - refreshInterval: {{ $.Values.externalSecretsRefreshInterval | default "1h" }} - secretStoreRef: - kind: {{ $.Values.keystoneServiceUsers.secretStore.kind | quote }} - name: {{ $.Values.keystoneServiceUsers.secretStore.name | quote }} - target: - name: {{ (printf "%s-ks-etc" $serviceName) | quote }} - template: - engineVersion: v2 - data: - {{ (printf "%s_auth.conf" $serviceName) | quote }}: | - {{- range $_, $user := $users }} - {{- $section := $user.section | default $user.usage }} - {{- $section := eq $section "user" | ternary "keystone_authtoken" $section -}} - {{- $shouldSkip := or (eq $user.usage "test") (eq $user.usage "admin") }} - {{- if not $shouldSkip }} - [{{ $section }}] - auth_url={{ $.Values.keystoneUrl }} - project_domain_name={{ include "openstack.serviceuser.project_domain_name" $user }} - project_name={{ include "openstack.serviceuser.project_name" $user }} - user_domain_name={{ include "openstack.serviceuser.user_domain_name" $user }} - username={{ printf "{{ (fromJson .%s).username }}" $user.usage }} - password={{ printf "{{ (fromJson .%s).password }}" $user.usage }} - region_name={{ $.Values.regionName }} - {{- end }} - {{- end }} - data: - {{/* default section name is the usage field */}} - {{/* usage test and admin are special in OpenStack Helm and not added to the config file */}} - {{- range $_, $user := $users -}} - {{- $shouldSkip := or (eq $user.usage "test") (eq $user.usage "admin") -}} - {{- if not $shouldSkip }} - - secretKey: {{ $user.usage }} - remoteRef: - key: {{ $user.remoteRef | quote }} - {{- end }} - {{- end }} -{{- end }} -{{- end }} {{- end }} diff --git a/components/openstack/values.schema.json b/components/openstack/values.schema.json index 5f0025ef2..0dea6f9ad 100644 --- a/components/openstack/values.schema.json +++ b/components/openstack/values.schema.json @@ -317,6 +317,16 @@ "default": "internal", "description": "OpenStack service catalog interface to use" }, + "keystoneIdentityProvider": { + "type": "string", + "default": "undefined", + "description": "Keystone Identity provider name" + }, + "accessTokenFile": { + "type": "string", + "default": "/var/run/secrets/kubernetes.io/serviceaccount/token", + "description": "Path to a file with Kubernetes token" + }, "services": { "type": "object", "description": "Service users organized by service name", diff --git a/components/openstack/values.yaml b/components/openstack/values.yaml index 809c33941..fced7aca2 100644 --- a/components/openstack/values.yaml +++ b/components/openstack/values.yaml @@ -83,6 +83,10 @@ keystoneServiceUsers: externalLinkAnnotationTemplate: "" # -- OpenStack service catalog interface to use keystoneInterface: "internal" + # -- Identity provider name (as created in Keystone) + keystoneIdentityProvider: "k8s-site-a" + # -- Path to Kubernetes service account token file + accessTokenFile: "/var/run/secrets/kubernetes.io/serviceaccount/token" # -- OpenStack services with a definition of each of their service accounts services: keystone: diff --git a/containers/cinder/Dockerfile b/containers/cinder/Dockerfile index 1b76fff31..c6f9b3b13 100644 --- a/containers/cinder/Dockerfile +++ b/containers/cinder/Dockerfile @@ -12,12 +12,14 @@ RUN apt-get update && \ && apt-get clean && rm -rf /var/lib/apt/lists/* COPY python/cinder-understack /src/cinder-understack +COPY python/keystoneauth-kubeservicetoken /src/keystoneauth-kubeservicetoken ARG OPENSTACK_VERSION="required_argument" RUN --mount=type=cache,target=/root/.cache/uv \ uv pip install \ --constraint https://releases.openstack.org/constraints/upper/${OPENSTACK_VERSION} \ - /src/cinder-understack + /src/cinder-understack \ + /src/keystoneauth-kubeservicetoken COPY containers/cinder/patches /tmp/patches/ RUN cd /var/lib/openstack/lib/python3.12/site-packages && \ diff --git a/containers/glance/Dockerfile b/containers/glance/Dockerfile index 85d6b3a6f..434350af1 100644 --- a/containers/glance/Dockerfile +++ b/containers/glance/Dockerfile @@ -1,4 +1,19 @@ # syntax=docker/dockerfile:1 +ARG OPENSTACK_VERSION="required_argument" +FROM quay.io/airshipit/glance:${OPENSTACK_VERSION}-ubuntu_noble AS build + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +COPY python/keystoneauth-kubeservicetoken /src/keystoneauth-kubeservicetoken + +ARG OPENSTACK_VERSION="required_argument" +RUN --mount=type=cache,target=/root/.cache/uv \ + uv pip install \ + --constraint https://releases.openstack.org/constraints/upper/${OPENSTACK_VERSION} \ + /src/keystoneauth-kubeservicetoken + ARG OPENSTACK_VERSION="required_argument" FROM quay.io/airshipit/glance:${OPENSTACK_VERSION}-ubuntu_noble AS final + +COPY --from=build --link /var/lib/openstack /var/lib/openstack diff --git a/containers/horizon/Dockerfile b/containers/horizon/Dockerfile index f8b18402c..bc46b99ff 100644 --- a/containers/horizon/Dockerfile +++ b/containers/horizon/Dockerfile @@ -1,4 +1,19 @@ # syntax=docker/dockerfile:1 +ARG OPENSTACK_VERSION="required_argument" +FROM quay.io/airshipit/horizon:${OPENSTACK_VERSION}-ubuntu_noble AS build + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +COPY python/keystoneauth-kubeservicetoken /src/keystoneauth-kubeservicetoken + +ARG OPENSTACK_VERSION="required_argument" +RUN --mount=type=cache,target=/root/.cache/uv \ + uv pip install \ + --constraint https://releases.openstack.org/constraints/upper/${OPENSTACK_VERSION} \ + /src/keystoneauth-kubeservicetoken + ARG OPENSTACK_VERSION="required_argument" FROM quay.io/airshipit/horizon:${OPENSTACK_VERSION}-ubuntu_noble AS final + +COPY --from=build --link /var/lib/openstack /var/lib/openstack diff --git a/containers/ironic/Dockerfile b/containers/ironic/Dockerfile index e669eb74c..929758463 100644 --- a/containers/ironic/Dockerfile +++ b/containers/ironic/Dockerfile @@ -23,6 +23,7 @@ RUN git -C /src/sushy fetch --unshallow --tags && \ sed -i '/sushy==.*/d' /upper-constraints.txt COPY python/ironic-understack /src/understack/ironic-understack +COPY python/keystoneauth-kubeservicetoken /src/keystoneauth-kubeservicetoken ARG OPENSTACK_VERSION="required_argument" RUN --mount=type=cache,target=/root/.cache/uv \ @@ -32,7 +33,8 @@ RUN --mount=type=cache,target=/root/.cache/uv \ /src/ironic \ /src/sushy \ /src/understack/ironic-understack \ - proliantutils==2.16.3 + /src/keystoneauth-kubeservicetoken \ + proliantutils==2.16.3 COPY containers/ironic/patches /tmp/patches/ RUN cd /var/lib/openstack/lib/python3.12/site-packages && \ diff --git a/containers/keystone/Dockerfile b/containers/keystone/Dockerfile index dd9e62d5c..66df8e795 100644 --- a/containers/keystone/Dockerfile +++ b/containers/keystone/Dockerfile @@ -11,4 +11,7 @@ ADD --checksum=sha256:${MOD_AUTH_OPENIDC_SHA256} https://github.com/OpenIDC/mod_ RUN apt-get update && \ apt-get install -y --no-install-recommends \ /tmp/libapache2-mod-auth-openidc_${MOD_AUTH_OPENIDC_VERSION}-1.noble_amd64.deb \ + libapache2-mod-oauth2 \ && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/*.deb + +RUN a2enmod oauth2 diff --git a/containers/neutron/Dockerfile b/containers/neutron/Dockerfile index da7fd1a0c..619998379 100644 --- a/containers/neutron/Dockerfile +++ b/containers/neutron/Dockerfile @@ -16,6 +16,7 @@ ADD --keep-git-dir=true https://github.com/rackerlabs/neutron-lib.git#${NEUTRON_ RUN git -C /src/neutron-lib fetch --unshallow --tags COPY python/neutron-understack /src/neutron-understack +COPY python/keystoneauth-kubeservicetoken /src/keystoneauth-kubeservicetoken ARG OPENSTACK_VERSION="required_argument" ADD https://releases.openstack.org/constraints/upper/${OPENSTACK_VERSION} /upper-constraints.txt @@ -27,7 +28,8 @@ RUN --mount=type=cache,target=/root/.cache/uv \ --constraint /upper-constraints.txt \ /src/neutron \ /src/neutron-lib \ - /src/neutron-understack + /src/neutron-understack \ + /src/keystoneauth-kubeservicetoken ARG OPENSTACK_VERSION="required_argument" FROM quay.io/airshipit/neutron:${OPENSTACK_VERSION}-ubuntu_noble AS final diff --git a/containers/nova/Dockerfile b/containers/nova/Dockerfile index 0c6ba3fae..4a4bc8e1a 100644 --- a/containers/nova/Dockerfile +++ b/containers/nova/Dockerfile @@ -1,7 +1,9 @@ # syntax=docker/dockerfile:1 ARG OPENSTACK_VERSION="required_argument" -FROM quay.io/airshipit/nova:${OPENSTACK_VERSION}-ubuntu_noble +FROM quay.io/airshipit/nova:${OPENSTACK_VERSION}-ubuntu_noble AS build + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ RUN apt-get update && \ apt-get install -y --no-install-recommends \ @@ -12,4 +14,18 @@ RUN apt-get update && \ COPY containers/nova/patches /tmp/patches/ RUN cd /var/lib/openstack/lib/python3.12/site-packages && \ QUILT_PATCHES=/tmp/patches quilt push -a -COPY python/nova-understack/ironic_understack /var/lib/openstack/lib/python3.12/site-packages/nova/virt/ironic_understack + +COPY python/nova-understack/ironic_understack /var/lib/openstack/lib/python3.12/site-packages/nova/virt/ironic_understack + +COPY python/keystoneauth-kubeservicetoken /src/keystoneauth-kubeservicetoken + +ARG OPENSTACK_VERSION="required_argument" +RUN --mount=type=cache,target=/root/.cache/uv \ + uv pip install \ + --constraint https://releases.openstack.org/constraints/upper/${OPENSTACK_VERSION} \ + /src/keystoneauth-kubeservicetoken + +ARG OPENSTACK_VERSION="required_argument" +FROM quay.io/airshipit/nova:${OPENSTACK_VERSION}-ubuntu_noble AS final + +COPY --from=build --link /var/lib/openstack /var/lib/openstack diff --git a/containers/octavia/Dockerfile b/containers/octavia/Dockerfile index 873a3b13d..0f4e2f10d 100644 --- a/containers/octavia/Dockerfile +++ b/containers/octavia/Dockerfile @@ -5,12 +5,15 @@ FROM quay.io/airshipit/octavia:${OPENSTACK_VERSION}-ubuntu_noble AS build COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ +COPY python/keystoneauth-kubeservicetoken /src/keystoneauth-kubeservicetoken + ARG OPENSTACK_VERSION="required_argument" RUN --mount=type=cache,target=/root/.cache/uv \ uv pip install \ --constraint https://releases.openstack.org/constraints/upper/${OPENSTACK_VERSION} \ ovn-octavia-provider \ - ovsdbapp + ovsdbapp \ + /src/keystoneauth-kubeservicetoken ARG OPENSTACK_VERSION="required_argument" FROM quay.io/airshipit/octavia:${OPENSTACK_VERSION}-ubuntu_noble AS final diff --git a/containers/openstack-client/Dockerfile b/containers/openstack-client/Dockerfile index bd8e9dc12..eb80195af 100644 --- a/containers/openstack-client/Dockerfile +++ b/containers/openstack-client/Dockerfile @@ -1,4 +1,19 @@ # syntax=docker/dockerfile:1 ARG OPENSTACK_VERSION="required_argument" -FROM quay.io/airshipit/openstack-client:${OPENSTACK_VERSION}-ubuntu_noble +FROM quay.io/airshipit/openstack-client:${OPENSTACK_VERSION}-ubuntu_noble AS build + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +COPY python/keystoneauth-kubeservicetoken /src/keystoneauth-kubeservicetoken + +ARG OPENSTACK_VERSION="required_argument" +RUN --mount=type=cache,target=/root/.cache/uv \ + uv pip install \ + --upgrade \ + --constraint https://releases.openstack.org/constraints/upper/${OPENSTACK_VERSION} \ + /src/keystoneauth-kubeservicetoken + +ARG OPENSTACK_VERSION="required_argument" +FROM quay.io/airshipit/openstack-client:${OPENSTACK_VERSION}-ubuntu_noble AS final +COPY --from=build --link /var/lib/openstack /var/lib/openstack diff --git a/containers/placement/Dockerfile b/containers/placement/Dockerfile index 110f69393..12130e482 100644 --- a/containers/placement/Dockerfile +++ b/containers/placement/Dockerfile @@ -1,4 +1,19 @@ # syntax=docker/dockerfile:1 +ARG OPENSTACK_VERSION="required_argument" +FROM quay.io/airshipit/placement:${OPENSTACK_VERSION}-ubuntu_noble AS build + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +COPY python/keystoneauth-kubeservicetoken /src/keystoneauth-kubeservicetoken + +ARG OPENSTACK_VERSION="required_argument" +RUN --mount=type=cache,target=/root/.cache/uv \ + uv pip install \ + --constraint https://releases.openstack.org/constraints/upper/${OPENSTACK_VERSION} \ + /src/keystoneauth-kubeservicetoken + ARG OPENSTACK_VERSION="required_argument" FROM quay.io/airshipit/placement:${OPENSTACK_VERSION}-ubuntu_noble AS final + +COPY --from=build --link /var/lib/openstack /var/lib/openstack diff --git a/containers/skyline/Dockerfile b/containers/skyline/Dockerfile index 3e01e5e31..620babb41 100644 --- a/containers/skyline/Dockerfile +++ b/containers/skyline/Dockerfile @@ -1,4 +1,19 @@ # syntax=docker/dockerfile:1 +ARG OPENSTACK_VERSION="required_argument" +FROM quay.io/airshipit/skyline:${OPENSTACK_VERSION}-ubuntu_noble AS build + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +COPY python/keystoneauth-kubeservicetoken /src/keystoneauth-kubeservicetoken + +ARG OPENSTACK_VERSION="required_argument" +RUN --mount=type=cache,target=/root/.cache/uv \ + uv pip install \ + --constraint https://releases.openstack.org/constraints/upper/${OPENSTACK_VERSION} \ + /src/keystoneauth-kubeservicetoken + ARG OPENSTACK_VERSION="required_argument" FROM quay.io/airshipit/skyline:${OPENSTACK_VERSION}-ubuntu_noble AS final + +COPY --from=build --link /var/lib/openstack /var/lib/openstack diff --git a/docs/deploy-guide/components/oidc-rbac.md b/docs/deploy-guide/components/oidc-rbac.md new file mode 100644 index 000000000..bce9280d6 --- /dev/null +++ b/docs/deploy-guide/components/oidc-rbac.md @@ -0,0 +1,44 @@ +--- +kustomize_paths: +- components/oidc-rbac +deploy_overrides: + helm: + mode: none + kustomize: + mode: none +--- + +# oidc-rbac + +OIDC RBAC configuration for Kubernetes service account issuer discovery. + +## Deployment Scope + +- Cluster scope: global, site +- Values key: `global.oidc_rbac`, `site.oidc_rbac` +- ArgoCD Application template: `charts/argocd-understack/templates/application-oidc-rbac.yaml` + +## How ArgoCD Builds It + +{{ component_argocd_builds() }} + +## How to Enable + +This component is enabled by default. To disable it: + +```yaml title="$CLUSTER_NAME/deploy.yaml" +global: + oidc_rbac: + enabled: false +site: + oidc_rbac: + enabled: false +``` + +## Deployment Repo Content + +{{ secrets_disclaimer }} + +Required or commonly required items: + +- None for this Application today. It deploys the shared `components/oidc-rbac` base directly and does not consume deploy-repo values or overlay manifests. diff --git a/properdocs.yml b/properdocs.yml index 23b373a76..66c4e65a3 100644 --- a/properdocs.yml +++ b/properdocs.yml @@ -199,6 +199,7 @@ nav: - deploy-guide/components/neutron.md - deploy-guide/components/nova.md - deploy-guide/components/octavia.md + - deploy-guide/components/oidc-rbac.md - deploy-guide/components/openebs.md - deploy-guide/components/openstack-exporter.md - deploy-guide/components/openstack-helm.md diff --git a/python/keystoneauth-kubeservicetoken/README.md b/python/keystoneauth-kubeservicetoken/README.md new file mode 100644 index 000000000..1bb4350d6 --- /dev/null +++ b/python/keystoneauth-kubeservicetoken/README.md @@ -0,0 +1,71 @@ +# File-backed OIDC access token plugin + +This package provides a `keystoneauth1` plugin that extends the OIDC +access-token flow by reading the OIDC access token from a file +(`access_token_file`) at authentication and reauthentication time. + +## Auth type + +- `v3oidcaccesstokenfile` + +## Required options + +- `auth_url` +- `identity_provider` +- `protocol` +- `access_token_file` + +## Service configuration examples + +### Nova (`nova.conf`) + +```ini +[service_user] +auth_type = v3oidcaccesstokenfile +auth_url = https://keystone.example/v3 +identity_provider = k8s-workload-idp +protocol = openid +access_token_file = /var/run/secrets/openstack/nova-oidc-token +send_service_user_token = true +``` + +### Ironic -> Neutron client (`ironic.conf`) + +```ini +[neutron] +auth_type = v3oidcaccesstokenfile +auth_url = https://keystone.internal:5000/v3 +identity_provider = k8s-workload-idp +protocol = openid +access_token_file = /var/run/secrets/openstack/ironic-oidc-token +region_name = RegionOne +``` + +### Neutron -> Placement client (`neutron.conf`) + +```ini +[placement] +auth_type = v3oidcaccesstokenfile +auth_url = https://keystone.example/v3 +identity_provider = k8s-workload-idp +protocol = openid +access_token_file = /var/run/secrets/openstack/neutron-placement-oidc-token +valid_interfaces = internal +``` + +## Behavior notes + +- Token content is read from file on authentication and reauthentication. +- Whitespace in the token file is trimmed. +- Missing, unreadable, or empty token files fail with an explicit auth error. +- Keystone token caching is preserved; token file updates are consumed when + keystoneauth reauthenticates near Keystone token expiry. + +## Rollout notes + +1. Install this package where the OpenStack service runs. +2. Configure the service to use `auth_type = v3oidcaccesstokenfile`. +3. Set `access_token_file` to the rotated token file path. This will usually be + path to where the Kubernetes secret is mounted. +4. Optionally verify at least one full Keystone token renewal cycle to confirm + file updates are consumed. diff --git a/python/keystoneauth-kubeservicetoken/openspec/changes/add-file-based-oidc-access-token-plugin/design.md b/python/keystoneauth-kubeservicetoken/openspec/changes/add-file-based-oidc-access-token-plugin/design.md new file mode 100644 index 000000000..42a8cb30b --- /dev/null +++ b/python/keystoneauth-kubeservicetoken/openspec/changes/add-file-based-oidc-access-token-plugin/design.md @@ -0,0 +1,69 @@ +## Context + +This repository will provide a custom keystoneauth plugin for workloads that already rotate OIDC access tokens into a local file (for example, Kubernetes projected service account tokens or an external refresh process). The upstream `keystoneauth1.identity.v3.OpenIDConnectAccessToken` plugin expects an inline `access_token`, which does not work well when tokens are short-lived and updated frequently. The plugin must remain compatible with OpenStack service integration patterns used by nova, ironic, and neutron. + +## Goals / Non-Goals + +**Goals:** +- Provide a plugin that extends `keystoneauth1.identity.v3.OpenIDConnectAccessToken` and accepts a token file path instead of a static `access_token` value. +- Read the current token from disk when authenticating with Keystone. +- Align token file re-read behavior with Keystone token renewal timing so rotated OIDC tokens are picked up without reauthenticating on every request. +- Keep configuration and usage simple for service operators. + +**Non-Goals:** +- Implement a token refresh service or write new OIDC tokens to disk. +- Replace or modify upstream keystoneauth caching internals beyond what is needed for this plugin. +- Change Keystone server-side behavior, token formats, or federation configuration. + +## Decisions + +### 1) Inherit from `OpenIDConnectAccessToken` +The custom class will inherit from `keystoneauth1.identity.v3.OpenIDConnectAccessToken` to reuse existing auth payload construction and Keystone interaction behavior. + +Alternatives considered: +- Build a plugin from scratch on `v3.Auth`: rejected because it duplicates existing OIDC access-token auth behavior and increases maintenance. + +### 2) Introduce `access_token_file` as the primary input +The plugin will accept a required file path option (for example, `access_token_file`) and will not require callers to pass inline `access_token`. + +Alternatives considered: +- Support both `access_token` and `access_token_file` with precedence rules: rejected for initial scope to avoid ambiguous configuration and operator mistakes. + +### 3) Read token lazily at authentication time +The plugin will load the token from disk only when it needs to authenticate/re-authenticate with Keystone. This ensures the token is as fresh as possible at the point where it matters. + +Alternatives considered: +- Poll or watch the file continuously: rejected due to unnecessary complexity and overhead. + +### 4) Preserve keystoneauth session/token caching behavior +The plugin will rely on keystoneauth's normal Keystone token caching. When the Keystone token is still valid, no reauthentication occurs and file changes are ignored. When the Keystone token is near expiry and reauthentication is triggered, the plugin re-reads the file and uses the latest OIDC token. + +Alternatives considered: +- Force reauthentication whenever the file changes: rejected because the requirement is to reauthenticate near Keystone token expiry, not on every file update. + +### 5) Fail fast on invalid file state +If the configured file is missing, unreadable, or empty during authentication, the plugin should raise a clear authentication-related error so operators can diagnose configuration/runtime issues quickly. + +Alternatives considered: +- Silent fallback to previous token value: rejected because it can mask rotation and operational issues. + +## Risks / Trade-offs + +- [Token file rotates between read and auth request] -> Mitigation: read token immediately before building auth payload; rely on subsequent retries/reauth if needed. +- [Unreadable or malformed token file causes auth failures] -> Mitigation: clear errors, input trimming/validation, and test coverage for failure modes. +- [Different service process privileges affect file readability] -> Mitigation: document required file permissions and deployment expectations. +- [Assuming upstream reauth timing behavior] -> Mitigation: add tests/assertions around reauthentication flow to validate token file is consulted when Keystone token renewal occurs. + +## Migration Plan + +1. Implement and package the plugin in this repository. +2. Add operator-facing usage documentation and config examples for nova/ironic/neutron style keystoneauth options. +3. Update service configuration to reference the new plugin and set `access_token_file`. +4. Roll out in non-production first; verify successful authentication and reauthentication across at least one token rotation cycle. +5. Rollback strategy: switch service config back to the previous auth plugin/value and restart/reload services. + +## Open Questions + +- What final config option name should be exposed (`access_token_file` vs `token_file`) to best match keystoneauth conventions? +- Should optional behavior allow reading a custom file encoding, or is UTF-8-only sufficient? +- Do target services require any explicit entry-point naming conventions for discovery beyond standard keystoneauth plugin registration? diff --git a/python/keystoneauth-kubeservicetoken/openspec/changes/add-file-based-oidc-access-token-plugin/proposal.md b/python/keystoneauth-kubeservicetoken/openspec/changes/add-file-based-oidc-access-token-plugin/proposal.md new file mode 100644 index 000000000..b498f3185 --- /dev/null +++ b/python/keystoneauth-kubeservicetoken/openspec/changes/add-file-based-oidc-access-token-plugin/proposal.md @@ -0,0 +1,26 @@ +## Why + +OpenStack services that use `keystoneauth1.identity.v3.OpenIDConnectAccessToken` require a static `access_token` value, which is a poor fit for short-lived OIDC tokens refreshed outside the process. We need a plugin that reads the current token from a file and integrates cleanly with Keystone token lifecycle behavior. + +## What Changes + +- Add a custom auth plugin in this repository that extends `keystoneauth1.identity.v3.OpenIDConnectAccessToken`. +- Replace direct `access_token` configuration with a required token file path parameter. +- Read the OIDC access token from disk at authentication time. +- Ensure token-file updates are honored when Keystone token renewal is triggered (near Keystone token expiry), without forcing reauthentication on every request. +- Keep compatibility with OpenStack service consumers such as nova, ironic, and neutron that use keystoneauth plugin loading patterns. + +## Capabilities + +### New Capabilities +- `file-based-oidc-access-token-auth`: Authenticate to Keystone using an OIDC access token read from a configured file path, with refresh behavior aligned to Keystone token reauthentication timing. + +### Modified Capabilities +- None. + +## Impact + +- Affected code: new plugin module(s), plugin option handling, and tests in this repository. +- API/config impact: introduces a file-path based token input (instead of requiring inline `access_token`) for this custom plugin. +- Runtime behavior: token file is re-read when Keystone token reauthentication occurs near expiry, so externally rotated OIDC tokens are picked up. +- Dependencies: continues to rely on `keystoneauth1`; no new external service dependency is expected. diff --git a/python/keystoneauth-kubeservicetoken/openspec/changes/add-file-based-oidc-access-token-plugin/specs/file-based-oidc-access-token-auth/spec.md b/python/keystoneauth-kubeservicetoken/openspec/changes/add-file-based-oidc-access-token-plugin/specs/file-based-oidc-access-token-auth/spec.md new file mode 100644 index 000000000..6dbc91527 --- /dev/null +++ b/python/keystoneauth-kubeservicetoken/openspec/changes/add-file-based-oidc-access-token-plugin/specs/file-based-oidc-access-token-auth/spec.md @@ -0,0 +1,41 @@ +## ADDED Requirements + +### Requirement: Plugin accepts token file configuration +The authentication plugin SHALL require a configuration option that identifies the filesystem path of the OIDC access token file and SHALL use this option as the token source for authentication payload generation. + +#### Scenario: Valid token file path is configured +- **WHEN** the plugin is initialized with a readable token file path +- **THEN** the plugin accepts the configuration and is ready to authenticate + +#### Scenario: Token file path is missing +- **WHEN** the plugin is initialized without the required token file path option +- **THEN** initialization or first authentication attempt MUST fail with a clear configuration error + +### Requirement: Plugin reads token from file at authentication time +The plugin SHALL read the access token value from the configured file each time it performs Keystone authentication or reauthentication, and SHALL use the read value as the OIDC access token in the auth request. + +#### Scenario: File contains a valid token +- **WHEN** the plugin performs authentication and the configured file contains a non-empty token value +- **THEN** the plugin sends that token value in the Keystone authentication request + +#### Scenario: File is unreadable or empty +- **WHEN** the plugin performs authentication and the token file is unreadable, missing, or empty +- **THEN** the plugin MUST fail authentication with an explicit file/token read error + +### Requirement: Reauthentication behavior follows Keystone token lifecycle +The plugin MUST preserve normal keystoneauth Keystone token caching behavior and MUST NOT force reauthentication solely because the token file content changed while the Keystone token remains valid. + +#### Scenario: Token file changes while Keystone token is still valid +- **WHEN** the configured token file is updated after a successful Keystone authentication +- **THEN** the existing Keystone token continues to be used until reauthentication is triggered by keystoneauth lifecycle rules + +#### Scenario: Keystone token nears expiry +- **WHEN** keystoneauth triggers reauthentication because the Keystone token is near expiry +- **THEN** the plugin re-reads the configured token file and uses the latest token value for the new authentication request + +### Requirement: Plugin is usable by OpenStack service consumers +The plugin SHALL be discoverable and configurable through standard keystoneauth plugin loading conventions so OpenStack services such as nova, ironic, and neutron can use it without custom integration code. + +#### Scenario: Service loads plugin by configured auth type +- **WHEN** a service references the plugin's auth type and required options in its Keystone auth configuration +- **THEN** keystoneauth loads the plugin and performs authentication successfully using the file-sourced OIDC token diff --git a/python/keystoneauth-kubeservicetoken/openspec/changes/add-file-based-oidc-access-token-plugin/tasks.md b/python/keystoneauth-kubeservicetoken/openspec/changes/add-file-based-oidc-access-token-plugin/tasks.md new file mode 100644 index 000000000..06683d00d --- /dev/null +++ b/python/keystoneauth-kubeservicetoken/openspec/changes/add-file-based-oidc-access-token-plugin/tasks.md @@ -0,0 +1,29 @@ +## 1. Plugin Interface and Configuration + +- [x] 1.1 Create a custom plugin class that inherits from `keystoneauth1.identity.v3.oidc.OidcAccessToken` and introduces `access_token_file` configuration. +- [x] 1.2 Implement a custom loader/options definition that requires `access_token_file` and does not require inline `access_token`/`auth_token` configuration. +- [x] 1.3 Ensure inherited plugin behavior still works when `access_token`/`auth_token` is not provided by reading and supplying token value from file internally. +- [x] 1.4 Add loader and initialization tests proving plugin creation succeeds with only `access_token_file` (plus required federation options). + +## 2. Token File Read and Auth Flow + +- [x] 2.1 Implement token-file read logic used at authentication/reauthentication time, including whitespace trimming. +- [x] 2.2 Integrate file-sourced token into the inherited OIDC access-token auth request flow. +- [x] 2.3 Implement explicit failure handling for missing, unreadable, or empty token files. + +## 3. Reauthentication and Caching Semantics + +- [x] 3.1 Ensure implementation preserves normal Keystone token caching and does not force reauthentication while current Keystone token remains valid. +- [x] 3.2 Verify token file is re-read when keystoneauth triggers reauthentication near Keystone token expiry. +- [x] 3.3 Add tests that token-file updates are consumed on reauthentication timing rather than on every request. + +## 4. Plugin Registration and Service Compatibility + +- [x] 4.1 Register plugin entry point/auth type so it is discoverable through standard keystoneauth plugin loading. +- [x] 4.2 Add service-facing configuration examples for nova, ironic, and neutron style auth settings. + +## 5. Validation and Documentation + +- [x] 5.1 Add unit tests for successful auth, missing file-path option, and token file failure modes. +- [x] 5.2 Run targeted and/or full test suite and resolve regressions. +- [x] 5.3 Document rollout and rollback notes for operators migrating from inline access-token config. diff --git a/python/keystoneauth-kubeservicetoken/openspec/config.yaml b/python/keystoneauth-kubeservicetoken/openspec/config.yaml new file mode 100644 index 000000000..ae7fa21f3 --- /dev/null +++ b/python/keystoneauth-kubeservicetoken/openspec/config.yaml @@ -0,0 +1,22 @@ +schema: spec-driven +context: | + ## Documentation + + The source code of upstream keystoneauth is available in /home/skrobul/devel/keystoneauth/ which can be used to reference upstream code without need of consulting context7 MCP for documentation. + + + ## Python environment + If any python code needs to be ran, make sure to: + - use `uv` for module and requirement management of pyproject.toml + - run any code through venv generated/managed by uv + + +# Per-artifact rules (optional) +# Add custom rules for specific artifacts. +# Example: +# rules: +# proposal: +# - Keep proposals under 500 words +# - Always include a "Non-goals" section +# tasks: +# - Break tasks into chunks of max 2 hours diff --git a/python/keystoneauth-kubeservicetoken/pyproject.toml b/python/keystoneauth-kubeservicetoken/pyproject.toml new file mode 100644 index 000000000..b8f99a511 --- /dev/null +++ b/python/keystoneauth-kubeservicetoken/pyproject.toml @@ -0,0 +1,40 @@ +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "keystoneauth-kubeservicetoken" +version = "0.1.0" +description = "Keystoneauth plugin that reads OIDC access tokens from a file" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "keystoneauth1>=5.0.0", +] + +[dependency-groups] +dev = [ + "pytest>=8.0.0", + "pytest-cov>=7.1.0", +] + +[project.entry-points."keystoneauth1.plugin"] +v3oidcaccesstokenfile = "keystoneauth_kubeservicetoken.oidc:OpenIDConnectAccessTokenFileLoader" + +[tool.setuptools] +package-dir = {"" = "src"} + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.pytest.ini_options] +testpaths = ["tests"] + +[tool.ruff] +line-length = 88 + +[tool.ruff.lint] +select = ["E", "F", "B", "I", "UP", "S"] + +[tool.ruff.lint.per-file-ignores] +"tests/*.py" = ["S101", "S106"] diff --git a/python/keystoneauth-kubeservicetoken/src/keystoneauth_kubeservicetoken/__init__.py b/python/keystoneauth-kubeservicetoken/src/keystoneauth_kubeservicetoken/__init__.py new file mode 100644 index 000000000..0e85b8e7f --- /dev/null +++ b/python/keystoneauth-kubeservicetoken/src/keystoneauth_kubeservicetoken/__init__.py @@ -0,0 +1,11 @@ +"""keystoneauth_kubeservicetoken package.""" + +from keystoneauth_kubeservicetoken.oidc import ( + OpenIDConnectAccessTokenFile, + OpenIDConnectAccessTokenFileLoader, +) + +__all__ = [ + "OpenIDConnectAccessTokenFile", + "OpenIDConnectAccessTokenFileLoader", +] diff --git a/python/keystoneauth-kubeservicetoken/src/keystoneauth_kubeservicetoken/oidc.py b/python/keystoneauth-kubeservicetoken/src/keystoneauth_kubeservicetoken/oidc.py new file mode 100644 index 000000000..701582fee --- /dev/null +++ b/python/keystoneauth-kubeservicetoken/src/keystoneauth_kubeservicetoken/oidc.py @@ -0,0 +1,67 @@ +"""File-backed OIDC access token plugin for keystoneauth.""" + +from __future__ import annotations + +from keystoneauth1 import exceptions, loading +from keystoneauth1.identity.v3 import oidc +from keystoneauth1.loading._plugins.identity import v3 as identity_v3_loading + + +class OpenIDConnectAccessTokenFile(oidc.OidcAccessToken): + """OIDC access-token auth plugin that reads the token from a file.""" + + def __init__( + self, *args, access_token_file: str, access_token: str | None = None, **kwargs + ): + if not access_token_file: + raise exceptions.OptionError("'access_token_file' is required") + + self.access_token_file = access_token_file + + super().__init__(*args, access_token=access_token or "", **kwargs) + + def _read_access_token(self) -> str: + try: + with open(self.access_token_file, encoding="utf-8") as token_file: + token = token_file.read().strip() + except FileNotFoundError as exc: + msg = f"OIDC access token file does not exist: {self.access_token_file}" + raise exceptions.AuthorizationFailure(msg) from exc + except OSError as exc: + msg = ( + "Unable to read OIDC access token file " + f"'{self.access_token_file}': {exc}" + ) + raise exceptions.AuthorizationFailure(msg) from exc + + if not token: + msg = f"OIDC access token file is empty: {self.access_token_file}" + raise exceptions.AuthorizationFailure(msg) + + return token + + def get_unscoped_auth_ref(self, session): + self.access_token = self._read_access_token() + return super().get_unscoped_auth_ref(session) + + +class OpenIDConnectAccessTokenFileLoader(identity_v3_loading.OpenIDConnectAccessToken): + """Loader for the file-backed OIDC access-token auth plugin.""" + + @property + def plugin_class(self): + return OpenIDConnectAccessTokenFile + + def get_options(self): + options = [ + option for option in super().get_options() if option.dest != "access_token" + ] + + options.append( + loading.Opt( + "access-token-file", + required=True, + help="Path to a file containing the OIDC access token.", + ) + ) + return options diff --git a/python/keystoneauth-kubeservicetoken/tests/test_oidc_access_token_file.py b/python/keystoneauth-kubeservicetoken/tests/test_oidc_access_token_file.py new file mode 100644 index 000000000..f94db6b92 --- /dev/null +++ b/python/keystoneauth-kubeservicetoken/tests/test_oidc_access_token_file.py @@ -0,0 +1,160 @@ +from __future__ import annotations + +from pathlib import Path +from unittest.mock import patch + +import pytest +from keystoneauth1 import exceptions, loading +from keystoneauth1.identity.v3 import oidc as upstream_oidc + +from keystoneauth_kubeservicetoken.oidc import ( + OpenIDConnectAccessTokenFile, + OpenIDConnectAccessTokenFileLoader, +) + + +class FakeAuthRef: + def __init__(self, auth_token: str, expires_soon: bool): + self.auth_token = auth_token + self._expires_soon = expires_soon + + def will_expire_soon(self, _stale_duration: int) -> bool: + return self._expires_soon + + +def _create_plugin(token_file: Path) -> OpenIDConnectAccessTokenFile: + return OpenIDConnectAccessTokenFile( + auth_url="https://keystone.example/v3", + identity_provider="example-idp", + protocol="openid", + access_token_file=str(token_file), + ) + + +@pytest.fixture +def auth_options() -> dict[str, str]: + return { + "auth_url": "https://keystone.example/v3", + "identity_provider": "example-idp", + "protocol": "openid", + } + + +def test_loader_options_require_access_token_file_and_not_access_token(): + loader = OpenIDConnectAccessTokenFileLoader() + + options = {option.dest: option for option in loader.get_options()} + + assert "access_token_file" in options + assert options["access_token_file"].required + assert "access_token" not in options + + +def test_loader_can_initialize_plugin_with_access_token_file_only( + tmp_path, auth_options +): + token_file = tmp_path / "token" + token_file.write_text("oidc-token", encoding="utf-8") + + loader = OpenIDConnectAccessTokenFileLoader() + plugin = loader.load_from_options( + **auth_options, + access_token_file=str(token_file), + ) + + assert isinstance(plugin, OpenIDConnectAccessTokenFile) + assert plugin.access_token_file == str(token_file) + + +def test_plugin_loader_is_discoverable_by_auth_type(): + loader = loading.get_plugin_loader("v3oidcaccesstokenfile") + + assert isinstance(loader, OpenIDConnectAccessTokenFileLoader) + + +def test_missing_access_token_file_configuration_fails(auth_options): + with pytest.raises(exceptions.OptionError, match="access_token_file"): + OpenIDConnectAccessTokenFile( + **auth_options, + access_token_file="", + ) + + +def test_auth_reads_trimmed_token_from_file_each_time(tmp_path): + token_file = tmp_path / "token" + token_file.write_text(" token-a\n", encoding="utf-8") + + plugin = _create_plugin(token_file) + observed_tokens: list[str] = [] + + def fake_super_get_unscoped_auth_ref(self, _session): + observed_tokens.append(self.access_token) + return object() + + with patch.object( + upstream_oidc.OidcAccessToken, + "get_unscoped_auth_ref", + autospec=True, + side_effect=fake_super_get_unscoped_auth_ref, + ): + plugin.get_unscoped_auth_ref(session=None) + token_file.write_text("token-b\n", encoding="utf-8") + plugin.get_unscoped_auth_ref(session=None) + + assert observed_tokens == ["token-a", "token-b"] + + +def test_auth_fails_for_missing_file(tmp_path): + plugin = _create_plugin(tmp_path / "missing-token") + + with pytest.raises(exceptions.AuthorizationFailure, match="does not exist"): + plugin._read_access_token() + + +def test_auth_fails_for_unreadable_file(tmp_path): + token_file = tmp_path / "token" + token_file.write_text("token", encoding="utf-8") + plugin = _create_plugin(token_file) + + with patch("builtins.open", side_effect=OSError("permission denied")): + with pytest.raises(exceptions.AuthorizationFailure, match="Unable to read"): + plugin._read_access_token() + + +def test_auth_fails_for_empty_file(tmp_path): + token_file = tmp_path / "token" + token_file.write_text("\n\t", encoding="utf-8") + plugin = _create_plugin(token_file) + + with pytest.raises(exceptions.AuthorizationFailure, match="is empty"): + plugin._read_access_token() + + +def test_file_updates_consumed_on_reauth_not_every_request(tmp_path): + token_file = tmp_path / "token" + token_file.write_text("token-a", encoding="utf-8") + plugin = _create_plugin(token_file) + + plugin.auth_ref = FakeAuthRef( + auth_token="cached-keystone-token", expires_soon=False + ) + + def fake_get_auth_ref(_session): + plugin.get_unscoped_auth_ref(session=None) + return FakeAuthRef(auth_token=f"ks-{plugin.access_token}", expires_soon=False) + + with ( + patch.object( + upstream_oidc.OidcAccessToken, + "get_unscoped_auth_ref", + autospec=True, + return_value=object(), + ), + patch.object(plugin, "get_auth_ref", side_effect=fake_get_auth_ref), + ): + token_file.write_text("token-b", encoding="utf-8") + + assert plugin.get_token(session=None) == "cached-keystone-token" + + plugin.auth_ref = FakeAuthRef(auth_token="expiring-token", expires_soon=True) + assert plugin.get_token(session=None) == "ks-token-b" diff --git a/python/keystoneauth-kubeservicetoken/uv.lock b/python/keystoneauth-kubeservicetoken/uv.lock new file mode 100644 index 000000000..24f9f4370 --- /dev/null +++ b/python/keystoneauth-kubeservicetoken/uv.lock @@ -0,0 +1,512 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/8c/2c56124c6dc53a774d435f985b5973bc592f42d437be58c0c92d65ae7296/charset_normalizer-3.4.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95", size = 298751, upload-time = "2026-03-15T18:50:00.003Z" }, + { url = "https://files.pythonhosted.org/packages/86/2a/2a7db6b314b966a3bcad8c731c0719c60b931b931de7ae9f34b2839289ee/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd", size = 200027, upload-time = "2026-03-15T18:50:01.702Z" }, + { url = "https://files.pythonhosted.org/packages/68/f2/0fe775c74ae25e2a3b07b01538fc162737b3e3f795bada3bc26f4d4d495c/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4", size = 220741, upload-time = "2026-03-15T18:50:03.194Z" }, + { url = "https://files.pythonhosted.org/packages/10/98/8085596e41f00b27dd6aa1e68413d1ddda7e605f34dd546833c61fddd709/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db", size = 215802, upload-time = "2026-03-15T18:50:05.859Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ce/865e4e09b041bad659d682bbd98b47fb490b8e124f9398c9448065f64fee/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89", size = 207908, upload-time = "2026-03-15T18:50:07.676Z" }, + { url = "https://files.pythonhosted.org/packages/a8/54/8c757f1f7349262898c2f169e0d562b39dcb977503f18fdf0814e923db78/charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565", size = 194357, upload-time = "2026-03-15T18:50:09.327Z" }, + { url = "https://files.pythonhosted.org/packages/6f/29/e88f2fac9218907fc7a70722b393d1bbe8334c61fe9c46640dba349b6e66/charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9", size = 205610, upload-time = "2026-03-15T18:50:10.732Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c5/21d7bb0cb415287178450171d130bed9d664211fdd59731ed2c34267b07d/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7", size = 203512, upload-time = "2026-03-15T18:50:12.535Z" }, + { url = "https://files.pythonhosted.org/packages/a4/be/ce52f3c7fdb35cc987ad38a53ebcef52eec498f4fb6c66ecfe62cfe57ba2/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550", size = 195398, upload-time = "2026-03-15T18:50:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/81/a0/3ab5dd39d4859a3555e5dadfc8a9fa7f8352f8c183d1a65c90264517da0e/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0", size = 221772, upload-time = "2026-03-15T18:50:15.581Z" }, + { url = "https://files.pythonhosted.org/packages/04/6e/6a4e41a97ba6b2fa87f849c41e4d229449a586be85053c4d90135fe82d26/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8", size = 205759, upload-time = "2026-03-15T18:50:17.047Z" }, + { url = "https://files.pythonhosted.org/packages/db/3b/34a712a5ee64a6957bf355b01dc17b12de457638d436fdb05d01e463cd1c/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0", size = 216938, upload-time = "2026-03-15T18:50:18.44Z" }, + { url = "https://files.pythonhosted.org/packages/cb/05/5bd1e12da9ab18790af05c61aafd01a60f489778179b621ac2a305243c62/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b", size = 210138, upload-time = "2026-03-15T18:50:19.852Z" }, + { url = "https://files.pythonhosted.org/packages/bd/8e/3cb9e2d998ff6b21c0a1860343cb7b83eba9cdb66b91410e18fc4969d6ab/charset_normalizer-3.4.6-cp310-cp310-win32.whl", hash = "sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557", size = 144137, upload-time = "2026-03-15T18:50:21.505Z" }, + { url = "https://files.pythonhosted.org/packages/d8/8f/78f5489ffadb0db3eb7aff53d31c24531d33eb545f0c6f6567c25f49a5ff/charset_normalizer-3.4.6-cp310-cp310-win_amd64.whl", hash = "sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6", size = 154244, upload-time = "2026-03-15T18:50:22.81Z" }, + { url = "https://files.pythonhosted.org/packages/e4/74/e472659dffb0cadb2f411282d2d76c60da1fc94076d7fffed4ae8a93ec01/charset_normalizer-3.4.6-cp310-cp310-win_arm64.whl", hash = "sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058", size = 143312, upload-time = "2026-03-15T18:50:24.074Z" }, + { url = "https://files.pythonhosted.org/packages/62/28/ff6f234e628a2de61c458be2779cb182bc03f6eec12200d4a525bbfc9741/charset_normalizer-3.4.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e", size = 293582, upload-time = "2026-03-15T18:50:25.454Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b7/b1a117e5385cbdb3205f6055403c2a2a220c5ea80b8716c324eaf75c5c95/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9", size = 197240, upload-time = "2026-03-15T18:50:27.196Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5f/2574f0f09f3c3bc1b2f992e20bce6546cb1f17e111c5be07308dc5427956/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d", size = 217363, upload-time = "2026-03-15T18:50:28.601Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d1/0ae20ad77bc949ddd39b51bf383b6ca932f2916074c95cad34ae465ab71f/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de", size = 212994, upload-time = "2026-03-15T18:50:30.102Z" }, + { url = "https://files.pythonhosted.org/packages/60/ac/3233d262a310c1b12633536a07cde5ddd16985e6e7e238e9f3f9423d8eb9/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73", size = 204697, upload-time = "2026-03-15T18:50:31.654Z" }, + { url = "https://files.pythonhosted.org/packages/25/3c/8a18fc411f085b82303cfb7154eed5bd49c77035eb7608d049468b53f87c/charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c", size = 191673, upload-time = "2026-03-15T18:50:33.433Z" }, + { url = "https://files.pythonhosted.org/packages/ff/a7/11cfe61d6c5c5c7438d6ba40919d0306ed83c9ab957f3d4da2277ff67836/charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc", size = 201120, upload-time = "2026-03-15T18:50:35.105Z" }, + { url = "https://files.pythonhosted.org/packages/b5/10/cf491fa1abd47c02f69687046b896c950b92b6cd7337a27e6548adbec8e4/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f", size = 200911, upload-time = "2026-03-15T18:50:36.819Z" }, + { url = "https://files.pythonhosted.org/packages/28/70/039796160b48b18ed466fde0af84c1b090c4e288fae26cd674ad04a2d703/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef", size = 192516, upload-time = "2026-03-15T18:50:38.228Z" }, + { url = "https://files.pythonhosted.org/packages/ff/34/c56f3223393d6ff3124b9e78f7de738047c2d6bc40a4f16ac0c9d7a1cb3c/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398", size = 218795, upload-time = "2026-03-15T18:50:39.664Z" }, + { url = "https://files.pythonhosted.org/packages/e8/3b/ce2d4f86c5282191a041fdc5a4ce18f1c6bd40a5bd1f74cf8625f08d51c1/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e", size = 201833, upload-time = "2026-03-15T18:50:41.552Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9b/b6a9f76b0fd7c5b5ec58b228ff7e85095370282150f0bd50b3126f5506d6/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed", size = 213920, upload-time = "2026-03-15T18:50:43.33Z" }, + { url = "https://files.pythonhosted.org/packages/ae/98/7bc23513a33d8172365ed30ee3a3b3fe1ece14a395e5fc94129541fc6003/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021", size = 206951, upload-time = "2026-03-15T18:50:44.789Z" }, + { url = "https://files.pythonhosted.org/packages/32/73/c0b86f3d1458468e11aec870e6b3feac931facbe105a894b552b0e518e79/charset_normalizer-3.4.6-cp311-cp311-win32.whl", hash = "sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e", size = 143703, upload-time = "2026-03-15T18:50:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e3/76f2facfe8eddee0bbd38d2594e709033338eae44ebf1738bcefe0a06185/charset_normalizer-3.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4", size = 153857, upload-time = "2026-03-15T18:50:47.563Z" }, + { url = "https://files.pythonhosted.org/packages/e2/dc/9abe19c9b27e6cd3636036b9d1b387b78c40dedbf0b47f9366737684b4b0/charset_normalizer-3.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316", size = 142751, upload-time = "2026-03-15T18:50:49.234Z" }, + { url = "https://files.pythonhosted.org/packages/e5/62/c0815c992c9545347aeea7859b50dc9044d147e2e7278329c6e02ac9a616/charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab", size = 295154, upload-time = "2026-03-15T18:50:50.88Z" }, + { url = "https://files.pythonhosted.org/packages/a8/37/bdca6613c2e3c58c7421891d80cc3efa1d32e882f7c4a7ee6039c3fc951a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21", size = 199191, upload-time = "2026-03-15T18:50:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/6c/92/9934d1bbd69f7f398b38c5dae1cbf9cc672e7c34a4adf7b17c0a9c17d15d/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2", size = 218674, upload-time = "2026-03-15T18:50:54.102Z" }, + { url = "https://files.pythonhosted.org/packages/af/90/25f6ab406659286be929fd89ab0e78e38aa183fc374e03aa3c12d730af8a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff", size = 215259, upload-time = "2026-03-15T18:50:55.616Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ef/79a463eb0fff7f96afa04c1d4c51f8fc85426f918db467854bfb6a569ce3/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5", size = 207276, upload-time = "2026-03-15T18:50:57.054Z" }, + { url = "https://files.pythonhosted.org/packages/f7/72/d0426afec4b71dc159fa6b4e68f868cd5a3ecd918fec5813a15d292a7d10/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0", size = 195161, upload-time = "2026-03-15T18:50:58.686Z" }, + { url = "https://files.pythonhosted.org/packages/bf/18/c82b06a68bfcb6ce55e508225d210c7e6a4ea122bfc0748892f3dc4e8e11/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a", size = 203452, upload-time = "2026-03-15T18:51:00.196Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/0c25979b92f8adafdbb946160348d8d44aa60ce99afdc27df524379875cb/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2", size = 202272, upload-time = "2026-03-15T18:51:01.703Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3d/7fea3e8fe84136bebbac715dd1221cc25c173c57a699c030ab9b8900cbb7/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5", size = 195622, upload-time = "2026-03-15T18:51:03.526Z" }, + { url = "https://files.pythonhosted.org/packages/57/8a/d6f7fd5cb96c58ef2f681424fbca01264461336d2a7fc875e4446b1f1346/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6", size = 220056, upload-time = "2026-03-15T18:51:05.269Z" }, + { url = "https://files.pythonhosted.org/packages/16/50/478cdda782c8c9c3fb5da3cc72dd7f331f031e7f1363a893cdd6ca0f8de0/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d", size = 203751, upload-time = "2026-03-15T18:51:06.858Z" }, + { url = "https://files.pythonhosted.org/packages/75/fc/cc2fcac943939c8e4d8791abfa139f685e5150cae9f94b60f12520feaa9b/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2", size = 216563, upload-time = "2026-03-15T18:51:08.564Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b7/a4add1d9a5f68f3d037261aecca83abdb0ab15960a3591d340e829b37298/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923", size = 209265, upload-time = "2026-03-15T18:51:10.312Z" }, + { url = "https://files.pythonhosted.org/packages/6c/18/c094561b5d64a24277707698e54b7f67bd17a4f857bbfbb1072bba07c8bf/charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4", size = 144229, upload-time = "2026-03-15T18:51:11.694Z" }, + { url = "https://files.pythonhosted.org/packages/ab/20/0567efb3a8fd481b8f34f739ebddc098ed062a59fed41a8d193a61939e8f/charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb", size = 154277, upload-time = "2026-03-15T18:51:13.004Z" }, + { url = "https://files.pythonhosted.org/packages/15/57/28d79b44b51933119e21f65479d0864a8d5893e494cf5daab15df0247c17/charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4", size = 142817, upload-time = "2026-03-15T18:51:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" }, + { url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" }, + { url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" }, + { url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" }, + { url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" }, + { url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" }, + { url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" }, + { url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" }, + { url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" }, + { url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" }, + { url = "https://files.pythonhosted.org/packages/f1/11/897052ea6af56df3eef3ca94edafee410ca699ca0c7b87960ad19932c55e/charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", size = 143940, upload-time = "2026-03-15T18:51:36.15Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", size = 154101, upload-time = "2026-03-15T18:51:37.876Z" }, + { url = "https://files.pythonhosted.org/packages/01/a5/7abf15b4c0968e47020f9ca0935fb3274deb87cb288cd187cad92e8cdffd/charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", size = 143109, upload-time = "2026-03-15T18:51:39.565Z" }, + { url = "https://files.pythonhosted.org/packages/25/6f/ffe1e1259f384594063ea1869bfb6be5cdb8bc81020fc36c3636bc8302a1/charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8", size = 294458, upload-time = "2026-03-15T18:51:41.134Z" }, + { url = "https://files.pythonhosted.org/packages/56/60/09bb6c13a8c1016c2ed5c6a6488e4ffef506461aa5161662bd7636936fb1/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421", size = 199277, upload-time = "2026-03-15T18:51:42.953Z" }, + { url = "https://files.pythonhosted.org/packages/00/50/dcfbb72a5138bbefdc3332e8d81a23494bf67998b4b100703fd15fa52d81/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2", size = 218758, upload-time = "2026-03-15T18:51:44.339Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/d79a9a191bb75f5aa81f3aaaa387ef29ce7cb7a9e5074ba8ea095cc073c2/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30", size = 215299, upload-time = "2026-03-15T18:51:45.871Z" }, + { url = "https://files.pythonhosted.org/packages/76/7e/bc8911719f7084f72fd545f647601ea3532363927f807d296a8c88a62c0d/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db", size = 206811, upload-time = "2026-03-15T18:51:47.308Z" }, + { url = "https://files.pythonhosted.org/packages/e2/40/c430b969d41dda0c465aa36cc7c2c068afb67177bef50905ac371b28ccc7/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8", size = 193706, upload-time = "2026-03-15T18:51:48.849Z" }, + { url = "https://files.pythonhosted.org/packages/48/15/e35e0590af254f7df984de1323640ef375df5761f615b6225ba8deb9799a/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815", size = 202706, upload-time = "2026-03-15T18:51:50.257Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bd/f736f7b9cc5e93a18b794a50346bb16fbfd6b37f99e8f306f7951d27c17c/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a", size = 202497, upload-time = "2026-03-15T18:51:52.012Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ba/2cc9e3e7dfdf7760a6ed8da7446d22536f3d0ce114ac63dee2a5a3599e62/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43", size = 193511, upload-time = "2026-03-15T18:51:53.723Z" }, + { url = "https://files.pythonhosted.org/packages/9e/cb/5be49b5f776e5613be07298c80e1b02a2d900f7a7de807230595c85a8b2e/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0", size = 220133, upload-time = "2026-03-15T18:51:55.333Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/99f1b5dad345accb322c80c7821071554f791a95ee50c1c90041c157ae99/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1", size = 203035, upload-time = "2026-03-15T18:51:56.736Z" }, + { url = "https://files.pythonhosted.org/packages/87/9a/62c2cb6a531483b55dddff1a68b3d891a8b498f3ca555fbcf2978e804d9d/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f", size = 216321, upload-time = "2026-03-15T18:51:58.17Z" }, + { url = "https://files.pythonhosted.org/packages/6e/79/94a010ff81e3aec7c293eb82c28f930918e517bc144c9906a060844462eb/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815", size = 208973, upload-time = "2026-03-15T18:51:59.998Z" }, + { url = "https://files.pythonhosted.org/packages/2a/57/4ecff6d4ec8585342f0c71bc03efaa99cb7468f7c91a57b105bcd561cea8/charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d", size = 144610, upload-time = "2026-03-15T18:52:02.213Z" }, + { url = "https://files.pythonhosted.org/packages/80/94/8434a02d9d7f168c25767c64671fead8d599744a05d6a6c877144c754246/charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f", size = 154962, upload-time = "2026-03-15T18:52:03.658Z" }, + { url = "https://files.pythonhosted.org/packages/46/4c/48f2cdbfd923026503dfd67ccea45c94fd8fe988d9056b468579c66ed62b/charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e", size = 143595, upload-time = "2026-03-15T18:52:05.123Z" }, + { url = "https://files.pythonhosted.org/packages/31/93/8878be7569f87b14f1d52032946131bcb6ebbd8af3e20446bc04053dc3f1/charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866", size = 314828, upload-time = "2026-03-15T18:52:06.831Z" }, + { url = "https://files.pythonhosted.org/packages/06/b6/fae511ca98aac69ecc35cde828b0a3d146325dd03d99655ad38fc2cc3293/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc", size = 208138, upload-time = "2026-03-15T18:52:08.239Z" }, + { url = "https://files.pythonhosted.org/packages/54/57/64caf6e1bf07274a1e0b7c160a55ee9e8c9ec32c46846ce59b9c333f7008/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e", size = 224679, upload-time = "2026-03-15T18:52:10.043Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cb/9ff5a25b9273ef160861b41f6937f86fae18b0792fe0a8e75e06acb08f1d/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077", size = 223475, upload-time = "2026-03-15T18:52:11.854Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f", size = 215230, upload-time = "2026-03-15T18:52:13.325Z" }, + { url = "https://files.pythonhosted.org/packages/cd/24/afff630feb571a13f07c8539fbb502d2ab494019492aaffc78ef41f1d1d0/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e", size = 199045, upload-time = "2026-03-15T18:52:14.752Z" }, + { url = "https://files.pythonhosted.org/packages/e5/17/d1399ecdaf7e0498c327433e7eefdd862b41236a7e484355b8e0e5ebd64b/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484", size = 211658, upload-time = "2026-03-15T18:52:16.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/38/16baa0affb957b3d880e5ac2144caf3f9d7de7bc4a91842e447fbb5e8b67/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7", size = 210769, upload-time = "2026-03-15T18:52:17.782Z" }, + { url = "https://files.pythonhosted.org/packages/05/34/c531bc6ac4c21da9ddfddb3107be2287188b3ea4b53b70fc58f2a77ac8d8/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff", size = 201328, upload-time = "2026-03-15T18:52:19.553Z" }, + { url = "https://files.pythonhosted.org/packages/fa/73/a5a1e9ca5f234519c1953608a03fe109c306b97fdfb25f09182babad51a7/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e", size = 225302, upload-time = "2026-03-15T18:52:21.043Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f6/cd782923d112d296294dea4bcc7af5a7ae0f86ab79f8fefbda5526b6cfc0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659", size = 211127, upload-time = "2026-03-15T18:52:22.491Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c5/0b6898950627af7d6103a449b22320372c24c6feda91aa24e201a478d161/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602", size = 222840, upload-time = "2026-03-15T18:52:24.113Z" }, + { url = "https://files.pythonhosted.org/packages/7d/25/c4bba773bef442cbdc06111d40daa3de5050a676fa26e85090fc54dd12f0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407", size = 216890, upload-time = "2026-03-15T18:52:25.541Z" }, + { url = "https://files.pythonhosted.org/packages/35/1a/05dacadb0978da72ee287b0143097db12f2e7e8d3ffc4647da07a383b0b7/charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579", size = 155379, upload-time = "2026-03-15T18:52:27.05Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7a/d269d834cb3a76291651256f3b9a5945e81d0a49ab9f4a498964e83c0416/charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4", size = 169043, upload-time = "2026-03-15T18:52:28.502Z" }, + { url = "https://files.pythonhosted.org/packages/23/06/28b29fba521a37a8932c6a84192175c34d49f84a6d4773fa63d05f9aff22/charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c", size = 148523, upload-time = "2026-03-15T18:52:29.956Z" }, + { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/33/e8c48488c29a73fd089f9d71f9653c1be7478f2ad6b5bc870db11a55d23d/coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5", size = 219255, upload-time = "2026-03-17T10:29:51.081Z" }, + { url = "https://files.pythonhosted.org/packages/da/bd/b0ebe9f677d7f4b74a3e115eec7ddd4bcf892074963a00d91e8b164a6386/coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf", size = 219772, upload-time = "2026-03-17T10:29:52.867Z" }, + { url = "https://files.pythonhosted.org/packages/48/cc/5cb9502f4e01972f54eedd48218bb203fe81e294be606a2bc93970208013/coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8", size = 246532, upload-time = "2026-03-17T10:29:54.688Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d8/3217636d86c7e7b12e126e4f30ef1581047da73140614523af7495ed5f2d/coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4", size = 248333, upload-time = "2026-03-17T10:29:56.221Z" }, + { url = "https://files.pythonhosted.org/packages/2b/30/2002ac6729ba2d4357438e2ed3c447ad8562866c8c63fc16f6dfc33afe56/coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d", size = 250211, upload-time = "2026-03-17T10:29:57.938Z" }, + { url = "https://files.pythonhosted.org/packages/6c/85/552496626d6b9359eb0e2f86f920037c9cbfba09b24d914c6e1528155f7d/coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930", size = 252125, upload-time = "2026-03-17T10:29:59.388Z" }, + { url = "https://files.pythonhosted.org/packages/44/21/40256eabdcbccdb6acf6b381b3016a154399a75fe39d406f790ae84d1f3c/coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d", size = 247219, upload-time = "2026-03-17T10:30:01.199Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/96e2a6c3f21a0ea77d7830b254a1542d0328acc8d7bdf6a284ba7e529f77/coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40", size = 248248, upload-time = "2026-03-17T10:30:03.317Z" }, + { url = "https://files.pythonhosted.org/packages/da/ba/8477f549e554827da390ec659f3c38e4b6d95470f4daafc2d8ff94eaa9c2/coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878", size = 246254, upload-time = "2026-03-17T10:30:04.832Z" }, + { url = "https://files.pythonhosted.org/packages/55/59/bc22aef0e6aa179d5b1b001e8b3654785e9adf27ef24c93dc4228ebd5d68/coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400", size = 250067, upload-time = "2026-03-17T10:30:06.535Z" }, + { url = "https://files.pythonhosted.org/packages/de/1b/c6a023a160806a5137dca53468fd97530d6acad24a22003b1578a9c2e429/coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0", size = 246521, upload-time = "2026-03-17T10:30:08.486Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3f/3532c85a55aa2f899fa17c186f831cfa1aa434d88ff792a709636f64130e/coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0", size = 247126, upload-time = "2026-03-17T10:30:09.966Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2e/b9d56af4a24ef45dfbcda88e06870cb7d57b2b0bfa3a888d79b4c8debd76/coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58", size = 221860, upload-time = "2026-03-17T10:30:11.393Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cc/d938417e7a4d7f0433ad4edee8bb2acdc60dc7ac5af19e2a07a048ecbee3/coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e", size = 222788, upload-time = "2026-03-17T10:30:12.886Z" }, + { url = "https://files.pythonhosted.org/packages/4b/37/d24c8f8220ff07b839b2c043ea4903a33b0f455abe673ae3c03bbdb7f212/coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d", size = 219381, upload-time = "2026-03-17T10:30:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587", size = 219880, upload-time = "2026-03-17T10:30:16.231Z" }, + { url = "https://files.pythonhosted.org/packages/55/2f/e0e5b237bffdb5d6c530ce87cc1d413a5b7d7dfd60fb067ad6d254c35c76/coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642", size = 250303, upload-time = "2026-03-17T10:30:17.748Z" }, + { url = "https://files.pythonhosted.org/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b", size = 252218, upload-time = "2026-03-17T10:30:19.804Z" }, + { url = "https://files.pythonhosted.org/packages/da/69/2f47bb6fa1b8d1e3e5d0c4be8ccb4313c63d742476a619418f85740d597b/coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686", size = 254326, upload-time = "2026-03-17T10:30:21.321Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d0/79db81da58965bd29dabc8f4ad2a2af70611a57cba9d1ec006f072f30a54/coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743", size = 256267, upload-time = "2026-03-17T10:30:23.094Z" }, + { url = "https://files.pythonhosted.org/packages/e5/32/d0d7cc8168f91ddab44c0ce4806b969df5f5fdfdbb568eaca2dbc2a04936/coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75", size = 250430, upload-time = "2026-03-17T10:30:25.311Z" }, + { url = "https://files.pythonhosted.org/packages/4d/06/a055311d891ddbe231cd69fdd20ea4be6e3603ffebddf8704b8ca8e10a3c/coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209", size = 252017, upload-time = "2026-03-17T10:30:27.284Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f6/d0fd2d21e29a657b5f77a2fe7082e1568158340dceb941954f776dce1b7b/coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a", size = 250080, upload-time = "2026-03-17T10:30:29.481Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ab/0d7fb2efc2e9a5eb7ddcc6e722f834a69b454b7e6e5888c3a8567ecffb31/coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e", size = 253843, upload-time = "2026-03-17T10:30:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/ba/6f/7467b917bbf5408610178f62a49c0ed4377bb16c1657f689cc61470da8ce/coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd", size = 249802, upload-time = "2026-03-17T10:30:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/75/2c/1172fb689df92135f5bfbbd69fc83017a76d24ea2e2f3a1154007e2fb9f8/coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8", size = 250707, upload-time = "2026-03-17T10:30:35.2Z" }, + { url = "https://files.pythonhosted.org/packages/67/21/9ac389377380a07884e3b48ba7a620fcd9dbfaf1d40565facdc6b36ec9ef/coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf", size = 221880, upload-time = "2026-03-17T10:30:36.775Z" }, + { url = "https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9", size = 222816, upload-time = "2026-03-17T10:30:38.891Z" }, + { url = "https://files.pythonhosted.org/packages/12/a6/1d3f6155fb0010ca68eba7fe48ca6c9da7385058b77a95848710ecf189b1/coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028", size = 221483, upload-time = "2026-03-17T10:30:40.463Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, + { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, + { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, + { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, + { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, + { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, + { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, + { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, + { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, + { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "iso8601" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/f3/ef59cee614d5e0accf6fd0cbba025b93b272e626ca89fb70a3e9187c5d15/iso8601-2.1.0.tar.gz", hash = "sha256:6b1d3829ee8921c4301998c909f7829fa9ed3cbdac0d3b16af2d743aed1ba8df", size = 6522, upload-time = "2023-10-03T00:25:39.317Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/0c/f37b6a241f0759b7653ffa7213889d89ad49a2b76eb2ddf3b57b2738c347/iso8601-2.1.0-py3-none-any.whl", hash = "sha256:aac4145c4dcb66ad8b648a02830f5e2ff6c24af20f4f482689be402db2429242", size = 7545, upload-time = "2023-10-03T00:25:32.304Z" }, +] + +[[package]] +name = "keystoneauth-kubeservicetoken" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "keystoneauth1" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-cov" }, +] + +[package.metadata] +requires-dist = [{ name = "keystoneauth1", specifier = ">=5.0.0" }] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=8.0.0" }, + { name = "pytest-cov", specifier = ">=7.1.0" }, +] + +[[package]] +name = "keystoneauth1" +version = "5.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "iso8601" }, + { name = "os-service-types" }, + { name = "pbr" }, + { name = "requests" }, + { name = "stevedore" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/bc/d99872ca0bc8bf5f248b50e3d7386dedec5278f8dd989a2a981d329a8069/keystoneauth1-5.13.1.tar.gz", hash = "sha256:e011e47ac3f3c671ffae33505c095548650cc19dab7f6af3b2ea5bd18c98f0c9", size = 288548, upload-time = "2026-02-20T15:16:47.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/68/ff03933cff7568277e2d55cb33aeb60a5c2d77bd88b0ea139244079df638/keystoneauth1-5.13.1-py3-none-any.whl", hash = "sha256:6e6d0296bc341e5f9a08e985f7083206c93a4ffd999933c2d1b7e1c833c2e501", size = 343539, upload-time = "2026-02-20T15:16:45.873Z" }, +] + +[[package]] +name = "os-service-types" +version = "1.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pbr" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/62/31e39aa8f2ac5bff0b061ce053f0610c9fe659e12aeca20bfb26d1665024/os_service_types-1.8.2.tar.gz", hash = "sha256:ab7648d7232849943196e1bb00a30e2e25e600fa3b57bb241d15b7f521b5b575", size = 27476, upload-time = "2025-11-21T13:55:47.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/26/0937af7b4383f1eba5bca789b8d191c0e09e59bb64962b18f4a14534ce41/os_service_types-1.8.2-py3-none-any.whl", hash = "sha256:f78890d71814deffabf0ed4358288ec2ced579bc4d0bb87a79ae806cbb4deb6e", size = 24876, upload-time = "2025-11-21T13:55:46.093Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pbr" +version = "7.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/ab/1de9a4f730edde1bdbbc2b8d19f8fa326f036b4f18b2f72cfbea7dc53c26/pbr-7.0.3.tar.gz", hash = "sha256:b46004ec30a5324672683ec848aed9e8fc500b0d261d40a3229c2d2bbfcedc29", size = 135625, upload-time = "2025-11-03T17:04:56.274Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/db/61efa0d08a99f897ef98256b03e563092d36cc38dc4ebe4a85020fe40b31/pbr-7.0.3-py2.py3-none-any.whl", hash = "sha256:ff223894eb1cd271a98076b13d3badff3bb36c424074d26334cd25aebeecea6b", size = 131898, upload-time = "2025-11-03T17:04:54.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[package]] +name = "requests" +version = "2.33.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", size = 134232, upload-time = "2026-03-25T15:10:41.586Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017, upload-time = "2026-03-25T15:10:40.382Z" }, +] + +[[package]] +name = "setuptools" +version = "82.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" }, +] + +[[package]] +name = "stevedore" +version = "5.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6d/90764092216fa560f6587f83bb70113a8ba510ba436c6476a2b47359057c/stevedore-5.7.0.tar.gz", hash = "sha256:31dd6fe6b3cbe921e21dcefabc9a5f1cf848cf538a1f27543721b8ca09948aa3", size = 516200, upload-time = "2026-02-20T13:27:06.765Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/06/36d260a695f383345ab5bbc3fd447249594ae2fa8dfd19c533d5ae23f46b/stevedore-5.7.0-py3-none-any.whl", hash = "sha256:fd25efbb32f1abb4c9e502f385f0018632baac11f9ee5d1b70f88cc5e22ad4ed", size = 54483, upload-time = "2026-02-20T13:27:05.561Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +]