From 49b397de7e10a7d7b6e1b1cc185e21642bb2f655 Mon Sep 17 00:00:00 2001 From: Pierangelo Di Pilato Date: Thu, 28 Aug 2025 21:11:51 +0200 Subject: [PATCH 1/7] [llmisvc] Improve config merge and update well-known presets (#4663) Signed-off-by: Pierangelo Di Pilato --- ....kserve.io_llminferenceserviceconfigs.yaml | 19 +- ...erving.kserve.io_llminferenceservices.yaml | 19 +- .../templates/config-llm-decode-template.yaml | 20 +- ...onfig-llm-decode-worker-data-parallel.yaml | 180 ++++--- .../config-llm-prefill-template.yaml | 13 +- ...nfig-llm-prefill-worker-data-parallel.yaml | 168 ++++--- .../templates/config-llm-router-route.yaml | 2 +- .../templates/config-llm-scheduler.yaml | 5 +- .../templates/config-llm-template.yaml | 11 +- .../config-llm-worker-data-parallel.yaml | 168 ++++--- ....kserve.io_llminferenceserviceconfigs.yaml | 19 +- ...erving.kserve.io_llminferenceservices.yaml | 19 +- .../llmisvc/config-llm-decode-template.yaml | 20 +- ...onfig-llm-decode-worker-data-parallel.yaml | 180 ++++--- .../llmisvc/config-llm-prefill-template.yaml | 13 +- ...nfig-llm-prefill-worker-data-parallel.yaml | 168 ++++--- config/llmisvc/config-llm-router-route.yaml | 2 +- config/llmisvc/config-llm-scheduler.yaml | 5 +- config/llmisvc/config-llm-template.yaml | 11 +- .../config-llm-worker-data-parallel.yaml | 168 ++++--- config/llmisvc/kustomization.yaml | 2 +- .../llm_inference_service_defaults.go | 24 +- .../v1alpha1/llm_inference_service_types.go | 5 +- .../serving/v1alpha1/zz_generated.deepcopy.go | 2 +- .../v1alpha1/llmisvc/config_merge.go | 125 +++-- .../v1alpha1/llmisvc/config_merge_test.go | 438 ++++++++++++++++-- .../v1alpha1/llmisvc/config_presets_test.go | 183 +++++--- pkg/testing/httproute_matchers.go | 157 +++++++ pkg/testing/log.go | 135 ++++++ test/crds/serving.kserve.io_all_crds.yaml | 38 +- 30 files changed, 1737 insertions(+), 582 deletions(-) create mode 100644 pkg/testing/httproute_matchers.go create mode 100644 pkg/testing/log.go diff --git a/charts/llmisvc-crd/templates/serving.kserve.io_llminferenceserviceconfigs.yaml b/charts/llmisvc-crd/templates/serving.kserve.io_llminferenceserviceconfigs.yaml index cc339954d46..936a3f3dcbd 100644 --- a/charts/llmisvc-crd/templates/serving.kserve.io_llminferenceserviceconfigs.yaml +++ b/charts/llmisvc-crd/templates/serving.kserve.io_llminferenceserviceconfigs.yaml @@ -46,24 +46,7 @@ spec: lora: properties: adapters: - items: - properties: - framework: - type: string - memory: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - storageUri: - type: string - required: - - framework - - memory - - storageUri - type: object - type: array + x-kubernetes-preserve-unknown-fields: true type: object name: type: string diff --git a/charts/llmisvc-crd/templates/serving.kserve.io_llminferenceservices.yaml b/charts/llmisvc-crd/templates/serving.kserve.io_llminferenceservices.yaml index d5169fe06ae..440f752d064 100644 --- a/charts/llmisvc-crd/templates/serving.kserve.io_llminferenceservices.yaml +++ b/charts/llmisvc-crd/templates/serving.kserve.io_llminferenceservices.yaml @@ -65,24 +65,7 @@ spec: lora: properties: adapters: - items: - properties: - framework: - type: string - memory: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - storageUri: - type: string - required: - - framework - - memory - - storageUri - type: object - type: array + x-kubernetes-preserve-unknown-fields: true type: object name: type: string diff --git a/charts/llmisvc-resources/templates/config-llm-decode-template.yaml b/charts/llmisvc-resources/templates/config-llm-decode-template.yaml index 07934abacf7..c12de1dfdcb 100644 --- a/charts/llmisvc-resources/templates/config-llm-decode-template.yaml +++ b/charts/llmisvc-resources/templates/config-llm-decode-template.yaml @@ -5,7 +5,7 @@ metadata: spec: template: containers: - - image: ghcr.io/llm-d/llm-d:v0.2.0 + - image: ghcr.io/llm-d/llm-d-dev:v0.2.2 imagePullPolicy: IfNotPresent name: main ports: @@ -14,6 +14,7 @@ spec: command: - vllm - serve + - /mnt/models args: - --served-model-name - "{{ .Spec.Model.Name }}" @@ -34,9 +35,13 @@ spec: value: /models securityContext: allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + runAsNonRoot: true capabilities: drop: - - MKNOD + - ALL + seccompProfile: + type: RuntimeDefault terminationMessagePath: /dev/termination-log terminationMessagePolicy: FallbackToLogsOnError livenessProbe: @@ -76,7 +81,13 @@ spec: - containerPort: 8000 protocol: TCP resources: { } - securityContext: { } + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + runAsNonRoot: true + capabilities: + drop: + - ALL terminationMessagePath: /dev/termination-log terminationMessagePolicy: FallbackToLogsOnError livenessProbe: @@ -100,6 +111,7 @@ spec: args: - "--port=8000" - "--vllm-port=8001" + - "--connector=nixlv2" - "--secure-proxy=true" - "--cert-path=/etc/ssl/certs" - "--decoder-use-tls=true" @@ -128,4 +140,4 @@ spec: name: model-cache - name: tls-certs secret: - secretName: "{{ ChildName .ObjectMeta.Name `-kserve-self-signed-certs` }}" \ No newline at end of file + secretName: "{{ ChildName .ObjectMeta.Name `-kserve-self-signed-certs` }}" diff --git a/charts/llmisvc-resources/templates/config-llm-decode-worker-data-parallel.yaml b/charts/llmisvc-resources/templates/config-llm-decode-worker-data-parallel.yaml index bf5e9c8e197..4dae126f69a 100644 --- a/charts/llmisvc-resources/templates/config-llm-decode-worker-data-parallel.yaml +++ b/charts/llmisvc-resources/templates/config-llm-decode-worker-data-parallel.yaml @@ -3,7 +3,7 @@ kind: LLMInferenceServiceConfig metadata: name: kserve-config-llm-decode-worker-data-parallel spec: - worker: + template: initContainers: - name: llm-d-routing-sidecar imagePullPolicy: IfNotPresent @@ -12,6 +12,15 @@ spec: ports: - containerPort: 8000 protocol: TCP + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault terminationMessagePath: /dev/termination-log terminationMessagePolicy: FallbackToLogsOnError livenessProbe: @@ -35,6 +44,7 @@ spec: args: - "--port=8000" - "--vllm-port=8001" + - "--connector=nixlv2" - "--secure-proxy=true" - "--cert-path=/etc/ssl/certs" - "--decoder-use-tls=true" @@ -52,66 +62,39 @@ spec: fieldRef: fieldPath: metadata.namespace containers: - - image: ghcr.io/llm-d/llm-d:v0.2.0 + - image: ghcr.io/llm-d/llm-d-dev:v0.2.2 imagePullPolicy: IfNotPresent name: main ports: - containerPort: 8001 protocol: TCP - stdin: true - tty: true command: - "/bin/sh" - "-c" args: - |- - - START_RANK=$(( ${LWS_WORKER_INDEX:-0} * {{ or .Spec.Parallelism.DataLocal 1 }} )) - if [ "${LWS_WORKER_INDEX:-0}" -eq 0 ]; then - ################# - # Leader-only launch - ################# - vllm serve \ - {{ .Spec.Model.Name }} \ - --port 8001 \ - --api-server-count 4 \ - --disable-log-requests \ - {{- if .Spec.Parallelism.Expert -}}--enable-expert-parallel \{{- end }} - {{- if .Spec.Parallelism.Tensor -}}--tensor-parallel-size {{ .Spec.Parallelism.Tensor }} \{{- end }} - --data-parallel-size {{ or .Spec.Parallelism.Data 1 }} \ - --data-parallel-size-local {{ or .Spec.Parallelism.DataLocal 1 }} \ - --data-parallel-address $(LWS_LEADER_ADDRESS) \ - --data-parallel-rpc-port {{ if .Spec.Parallelism.DataRPCPort }}{{ .Spec.Parallelism.DataRPCPort }}{{ else }}5555{{- end }} \ - --data-parallel-start-rank $START_RANK \ - --trust-remote-code \ - --enable-ssl-refresh \ - --ssl-certfile \ - /etc/ssl/certs/tls.crt \ - --ssl-keyfile \ - /etc/ssl/certs/tls.key - else - ################# - # Worker-only launch - ################# - vllm serve \ - {{ .Spec.Model.Name }} \ - --port 8001 \ - --disable-log-requests \ - {{- if .Spec.Parallelism.Expert }}--enable-expert-parallel \{{- end }} - {{- if .Spec.Parallelism.Tensor }}--tensor-parallel-size {{ .Spec.Parallelism.Tensor }} \{{- end }} - --data-parallel-size {{ or .Spec.Parallelism.Data 1 }} \ - --data-parallel-size-local {{ or .Spec.Parallelism.DataLocal 1 }} \ - --data-parallel-address $(LWS_LEADER_ADDRESS) \ - --data-parallel-rpc-port {{ if .Spec.Parallelism.DataRPCPort }}{{ .Spec.Parallelism.DataRPCPort }}{{ else }}5555{{- end }} \ - --data-parallel-start-rank $START_RANK \ - --trust-remote-code \ - --headless \ - --enable-ssl-refresh \ - --ssl-certfile \ - /etc/ssl/certs/tls.crt \ - --ssl-keyfile \ - /etc/ssl/certs/tls.key - fi + START_RANK=0 + eval "vllm serve \ + /mnt/models \ + --served-model-name "{{ .Spec.Model.Name }}" \ + --port 8001 \ + --api-server-count ${VLLM_API_SERVER_COUNT:-8} \ + --disable-log-requests \ + {{- if .Spec.Parallelism.Expert -}}--enable-expert-parallel{{- end }} \ + {{- if .Spec.Parallelism.Tensor -}}--tensor-parallel-size {{ .Spec.Parallelism.Tensor }}{{- end }} \ + --data-parallel-size $(( {{ or .Spec.Parallelism.DataLocal 1 }} * {{ or .Spec.Parallelism.Data 1 }} )) \ + --data-parallel-size-local {{ or .Spec.Parallelism.DataLocal 1 }} \ + --data-parallel-address $(LWS_LEADER_ADDRESS) \ + --data-parallel-rpc-port {{ if .Spec.Parallelism.DataRPCPort }}{{ .Spec.Parallelism.DataRPCPort }}{{ else }}5555{{- end }} \ + --data-parallel-start-rank $START_RANK \ + --data-parallel-hybrid-lb \ + ${VLLM_ADDITIONAL_ARGS} \ + --trust-remote-code \ + --enable-ssl-refresh \ + --ssl-certfile \ + /etc/ssl/certs/tls.crt \ + --ssl-keyfile \ + /etc/ssl/certs/tls.key" env: - name: HOME value: /home @@ -121,10 +104,16 @@ spec: value: /models securityContext: allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + runAsNonRoot: true capabilities: add: - "IPC_LOCK" - "SYS_RAWIO" + drop: + - ALL + seccompProfile: + type: RuntimeDefault terminationMessagePath: /dev/termination-log terminationMessagePolicy: FallbackToLogsOnError livenessProbe: @@ -132,7 +121,7 @@ spec: path: /health port: 8001 scheme: HTTPS - initialDelaySeconds: 120 + initialDelaySeconds: 300 periodSeconds: 10 timeoutSeconds: 10 failureThreshold: 3 @@ -141,8 +130,8 @@ spec: path: /health port: 8001 scheme: HTTPS - initialDelaySeconds: 10 - periodSeconds: 10 + initialDelaySeconds: 200 + periodSeconds: 30 timeoutSeconds: 5 failureThreshold: 60 volumeMounts: @@ -167,4 +156,85 @@ spec: name: model-cache - name: tls-certs secret: - secretName: "{{ ChildName .ObjectMeta.Name `-kserve-self-signed-certs` }}" \ No newline at end of file + secretName: "{{ ChildName .ObjectMeta.Name `-kserve-self-signed-certs` }}" + worker: + containers: + - image: ghcr.io/llm-d/llm-d-dev:v0.2.2 + imagePullPolicy: IfNotPresent + name: main + ports: + - containerPort: 8001 + protocol: TCP + command: + - "/bin/sh" + - "-c" + args: + - |- + START_RANK=$(( ${LWS_WORKER_INDEX:-0} * {{ or .Spec.Parallelism.DataLocal 1 }} )) + eval "vllm serve \ + /mnt/models \ + --served-model-name "{{ .Spec.Model.Name }}" \ + --port 8001 \ + --disable-log-requests \ + {{- if .Spec.Parallelism.Expert }}--enable-expert-parallel{{- end }} \ + {{- if .Spec.Parallelism.Tensor }}--tensor-parallel-size {{ .Spec.Parallelism.Tensor }}{{- end }} \ + --data-parallel-size $(( {{ or .Spec.Parallelism.DataLocal 1 }} * {{ or .Spec.Parallelism.Data 1 }} )) \ + --data-parallel-size-local {{ or .Spec.Parallelism.DataLocal 1 }} \ + --data-parallel-address $(LWS_LEADER_ADDRESS) \ + --data-parallel-rpc-port {{ if .Spec.Parallelism.DataRPCPort }}{{ .Spec.Parallelism.DataRPCPort }}{{ else }}5555{{- end }} \ + --data-parallel-start-rank $START_RANK \ + --data-parallel-hybrid-lb \ + ${VLLM_ADDITIONAL_ARGS} \ + --trust-remote-code \ + --headless \ + --enable-ssl-refresh \ + --ssl-certfile \ + /etc/ssl/certs/tls.crt \ + --ssl-keyfile \ + /etc/ssl/certs/tls.key" + env: + - name: HOME + value: /home + - name: VLLM_LOGGING_LEVEL + value: INFO + - name: HF_HUB_CACHE + value: /models + - name: VLLM_RANDOMIZE_DP_DUMMY_INPUTS + value: "1" + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + runAsNonRoot: true + capabilities: + add: + - "IPC_LOCK" + - "SYS_RAWIO" + drop: + - ALL + seccompProfile: + type: RuntimeDefault + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: FallbackToLogsOnError + volumeMounts: + - mountPath: /home + name: home + - mountPath: /dev/shm + name: dshm + - mountPath: /models + name: model-cache + - mountPath: /etc/ssl/certs + name: tls-certs + readOnly: true + terminationGracePeriodSeconds: 30 + volumes: + - emptyDir: { } + name: home + - emptyDir: + medium: Memory + sizeLimit: 1Gi + name: dshm + - emptyDir: { } + name: model-cache + - name: tls-certs + secret: + secretName: "{{ ChildName .ObjectMeta.Name `-kserve-self-signed-certs` }}" diff --git a/charts/llmisvc-resources/templates/config-llm-prefill-template.yaml b/charts/llmisvc-resources/templates/config-llm-prefill-template.yaml index 3b00fa0390f..82e1ebf89c4 100644 --- a/charts/llmisvc-resources/templates/config-llm-prefill-template.yaml +++ b/charts/llmisvc-resources/templates/config-llm-prefill-template.yaml @@ -6,7 +6,7 @@ spec: prefill: template: containers: - - image: ghcr.io/llm-d/llm-d:v0.2.0 + - image: ghcr.io/llm-d/llm-d-dev:v0.2.2 imagePullPolicy: IfNotPresent name: main ports: @@ -15,7 +15,7 @@ spec: command: - vllm - serve - - "{{ .Spec.Model.Name }}" + - /mnt/models args: - --served-model-name - "{{ .Spec.Model.Name }}" @@ -36,6 +36,13 @@ spec: value: /models securityContext: allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + runAsNonRoot: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault terminationMessagePath: /dev/termination-log terminationMessagePolicy: File livenessProbe: @@ -78,4 +85,4 @@ spec: name: model-cache - name: tls-certs secret: - secretName: "{{ ChildName .ObjectMeta.Name `-kserve-self-signed-certs` }}" \ No newline at end of file + secretName: "{{ ChildName .ObjectMeta.Name `-kserve-self-signed-certs` }}" diff --git a/charts/llmisvc-resources/templates/config-llm-prefill-worker-data-parallel.yaml b/charts/llmisvc-resources/templates/config-llm-prefill-worker-data-parallel.yaml index b9ef3b80e5e..1e90a6e2e02 100644 --- a/charts/llmisvc-resources/templates/config-llm-prefill-worker-data-parallel.yaml +++ b/charts/llmisvc-resources/templates/config-llm-prefill-worker-data-parallel.yaml @@ -4,68 +4,42 @@ metadata: name: kserve-config-llm-prefill-worker-data-parallel spec: prefill: - worker: + template: containers: - - image: ghcr.io/llm-d/llm-d:v0.2.0 + - image: ghcr.io/llm-d/llm-d-dev:v0.2.2 imagePullPolicy: IfNotPresent name: main ports: - containerPort: 8000 protocol: TCP - stdin: true - tty: true command: - "/bin/sh" - "-c" args: - |- - START_RANK=$(( ${LWS_WORKER_INDEX:-0} * {{ or .Spec.Prefill.Parallelism.DataLocal 1 }} )) - if [ "${LWS_WORKER_INDEX:-0}" -eq 0 ]; then - ################# - # Leader-only launch - ################# - vllm serve \ - {{ .Spec.Model.Name }} \ - --port 8000 \ - --api-server-count 4 \ - --disable-log-requests \ - {{- if .Spec.Prefill.Parallelism.Expert -}}--enable-expert-parallel \{{- end }} - {{- if .Spec.Prefill.Parallelism.Tensor -}}--tensor-parallel-size {{ .Spec.Prefill.Parallelism.Tensor }} \{{- end }} - --data-parallel-size {{ or .Spec.Prefill.Parallelism.Data 1 }} \ - --data-parallel-size-local {{ or .Spec.Prefill.Parallelism.DataLocal 1 }} \ - --data-parallel-address $(LWS_LEADER_ADDRESS) \ - --data-parallel-rpc-port {{ if .Spec.Prefill.Parallelism.DataRPCPort }}{{ .Spec.Prefill.Parallelism.DataRPCPort }}{{ else }}5555{{- end }} \ - --data-parallel-start-rank $START_RANK \ - --trust-remote-code \ - --enable-ssl-refresh \ - --ssl-certfile \ - /etc/ssl/certs/tls.crt \ - --ssl-keyfile \ - /etc/ssl/certs/tls.key - else - ################# - # Worker-only launch - ################# - vllm serve \ - {{ .Spec.Model.Name }} \ - --port 8000 \ - --disable-log-requests \ - {{- if .Spec.Prefill.Parallelism.Expert -}}--enable-expert-parallel \{{- end }} - {{- if .Spec.Prefill.Parallelism.Tensor -}}--tensor-parallel-size {{ .Spec.Prefill.Parallelism.Tensor }} \{{- end }} - --data-parallel-size {{ or .Spec.Prefill.Parallelism.Data 1 }} \ - --data-parallel-size-local {{ or .Spec.Prefill.Parallelism.DataLocal 1 }} \ - --data-parallel-address $(LWS_LEADER_ADDRESS) \ - --data-parallel-rpc-port {{ if .Spec.Prefill.Parallelism.DataRPCPort }}{{ .Spec.Prefill.Parallelism.DataRPCPort }}{{ else }}5555{{- end }} \ - --data-parallel-start-rank $START_RANK \ - --trust-remote-code \ - --headless \ - --enable-ssl-refresh \ - --ssl-certfile \ - /etc/ssl/certs/tls.crt \ - --ssl-keyfile \ - /etc/ssl/certs/tls.key - fi + START_RANK=0 + eval "vllm serve \ + /mnt/models \ + --served-model-name "{{ .Spec.Model.Name }}" \ + --port 8000 \ + --api-server-count ${VLLM_API_SERVER_COUNT:-8} \ + --disable-log-requests \ + {{- if .Spec.Prefill.Parallelism.Expert -}}--enable-expert-parallel{{- end }} \ + {{- if .Spec.Prefill.Parallelism.Tensor -}}--tensor-parallel-size {{ .Spec.Prefill.Parallelism.Tensor }}{{- end }} \ + --data-parallel-size $(( {{ or .Spec.Prefill.Parallelism.DataLocal 1 }} * {{ or .Spec.Prefill.Parallelism.Data 1 }} )) \ + --data-parallel-size-local {{ or .Spec.Prefill.Parallelism.DataLocal 1 }} \ + --data-parallel-address $(LWS_LEADER_ADDRESS) \ + --data-parallel-rpc-port {{ if .Spec.Prefill.Parallelism.DataRPCPort }}{{ .Spec.Prefill.Parallelism.DataRPCPort }}{{ else }}5555{{- end }} \ + --data-parallel-start-rank $START_RANK \ + --data-parallel-hybrid-lb \ + ${VLLM_ADDITIONAL_ARGS} \ + --trust-remote-code \ + --enable-ssl-refresh \ + --ssl-certfile \ + /etc/ssl/certs/tls.crt \ + --ssl-keyfile \ + /etc/ssl/certs/tls.key" env: - name: HOME value: /home @@ -75,10 +49,16 @@ spec: value: /models securityContext: allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + runAsNonRoot: true capabilities: add: - "IPC_LOCK" - "SYS_RAWIO" + drop: + - ALL + seccompProfile: + type: RuntimeDefault terminationMessagePath: /dev/termination-log terminationMessagePolicy: FallbackToLogsOnError livenessProbe: @@ -86,7 +66,7 @@ spec: path: /health port: 8000 scheme: HTTPS - initialDelaySeconds: 120 + initialDelaySeconds: 300 periodSeconds: 10 timeoutSeconds: 10 failureThreshold: 3 @@ -95,8 +75,8 @@ spec: path: /health port: 8000 scheme: HTTPS - initialDelaySeconds: 10 - periodSeconds: 10 + initialDelaySeconds: 200 + periodSeconds: 30 timeoutSeconds: 5 failureThreshold: 60 volumeMounts: @@ -121,4 +101,84 @@ spec: name: model-cache - name: tls-certs secret: - secretName: "{{ ChildName .ObjectMeta.Name `-kserve-self-signed-certs` }}" \ No newline at end of file + secretName: "{{ ChildName .ObjectMeta.Name `-kserve-self-signed-certs` }}" + worker: + containers: + - image: ghcr.io/llm-d/llm-d-dev:v0.2.2 + imagePullPolicy: IfNotPresent + name: main + ports: + - containerPort: 8000 + protocol: TCP + command: + - "/bin/sh" + - "-c" + args: + - |- + + START_RANK=$(( ${LWS_WORKER_INDEX:-0} * {{ or .Spec.Prefill.Parallelism.DataLocal 1 }} )) + eval "vllm serve \ + /mnt/models \ + --served-model-name "{{ .Spec.Model.Name }}" \ + --port 8000 \ + --disable-log-requests \ + {{- if .Spec.Prefill.Parallelism.Expert }}--enable-expert-parallel{{- end }} \ + {{- if .Spec.Prefill.Parallelism.Tensor }}--tensor-parallel-size {{ .Spec.Prefill.Parallelism.Tensor }}{{- end }} \ + --data-parallel-size $(( {{ or .Spec.Prefill.Parallelism.DataLocal 1 }} * {{ or .Spec.Prefill.Parallelism.Data 1 }} )) \ + --data-parallel-size-local {{ or .Spec.Prefill.Parallelism.DataLocal 1 }} \ + --data-parallel-address $(LWS_LEADER_ADDRESS) \ + --data-parallel-rpc-port {{ if .Spec.Prefill.Parallelism.DataRPCPort }}{{ .Spec.Prefill.Parallelism.DataRPCPort }}{{ else }}5555{{- end }} \ + --data-parallel-start-rank $START_RANK \ + --data-parallel-hybrid-lb \ + ${VLLM_ADDITIONAL_ARGS} \ + --trust-remote-code \ + --headless \ + --enable-ssl-refresh \ + --ssl-certfile \ + /etc/ssl/certs/tls.crt \ + --ssl-keyfile \ + /etc/ssl/certs/tls.key" + env: + - name: HOME + value: /home + - name: VLLM_LOGGING_LEVEL + value: INFO + - name: HF_HUB_CACHE + value: /models + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + runAsNonRoot: true + capabilities: + add: + - "IPC_LOCK" + - "SYS_RAWIO" + drop: + - ALL + seccompProfile: + type: RuntimeDefault + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: FallbackToLogsOnError + volumeMounts: + - mountPath: /home + name: home + - mountPath: /dev/shm + name: dshm + - mountPath: /models + name: model-cache + - mountPath: /etc/ssl/certs + name: tls-certs + readOnly: true + terminationGracePeriodSeconds: 30 + volumes: + - emptyDir: { } + name: home + - emptyDir: + medium: Memory + sizeLimit: 1Gi + name: dshm + - emptyDir: { } + name: model-cache + - name: tls-certs + secret: + secretName: "{{ ChildName .ObjectMeta.Name `-kserve-self-signed-certs` }}" diff --git a/charts/llmisvc-resources/templates/config-llm-router-route.yaml b/charts/llmisvc-resources/templates/config-llm-router-route.yaml index bf4b5e8145c..39646092b25 100644 --- a/charts/llmisvc-resources/templates/config-llm-router-route.yaml +++ b/charts/llmisvc-resources/templates/config-llm-router-route.yaml @@ -35,4 +35,4 @@ spec: replacePrefixMatch: / timeouts: backendRequest: 0s - request: 0s \ No newline at end of file + request: 0s diff --git a/charts/llmisvc-resources/templates/config-llm-scheduler.yaml b/charts/llmisvc-resources/templates/config-llm-scheduler.yaml index fc614fd7b0e..1f5965d2e24 100644 --- a/charts/llmisvc-resources/templates/config-llm-scheduler.yaml +++ b/charts/llmisvc-resources/templates/config-llm-scheduler.yaml @@ -59,6 +59,9 @@ spec: - --grpcHealthPort - "9003" - --secureServing + - --modelServerMetricsScheme + - "https" + - --modelServerMetricsHttpsInsecureSkipVerify - --certPath - "/etc/ssl/certs" resources: @@ -86,4 +89,4 @@ spec: secretName: "{{ ChildName .ObjectMeta.Name `-kserve-self-signed-certs` }}" dnsPolicy: ClusterFirst restartPolicy: Always - terminationGracePeriodSeconds: 30 \ No newline at end of file + terminationGracePeriodSeconds: 30 diff --git a/charts/llmisvc-resources/templates/config-llm-template.yaml b/charts/llmisvc-resources/templates/config-llm-template.yaml index b731c15ce38..f021cc03cfd 100644 --- a/charts/llmisvc-resources/templates/config-llm-template.yaml +++ b/charts/llmisvc-resources/templates/config-llm-template.yaml @@ -5,7 +5,7 @@ metadata: spec: template: containers: - - image: ghcr.io/llm-d/llm-d:v0.2.0 + - image: ghcr.io/llm-d/llm-d-dev:v0.2.2 imagePullPolicy: IfNotPresent name: main ports: @@ -14,6 +14,7 @@ spec: command: - vllm - serve + - /mnt/models args: - --served-model-name - "{{ .Spec.Model.Name }}" @@ -34,9 +35,13 @@ spec: value: /models securityContext: allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + runAsNonRoot: true capabilities: drop: - - MKNOD + - ALL + seccompProfile: + type: RuntimeDefault terminationMessagePath: /dev/termination-log terminationMessagePolicy: FallbackToLogsOnError livenessProbe: @@ -79,4 +84,4 @@ spec: name: model-cache - name: tls-certs secret: - secretName: "{{ ChildName .ObjectMeta.Name `-kserve-self-signed-certs` }}" \ No newline at end of file + secretName: "{{ ChildName .ObjectMeta.Name `-kserve-self-signed-certs` }}" diff --git a/charts/llmisvc-resources/templates/config-llm-worker-data-parallel.yaml b/charts/llmisvc-resources/templates/config-llm-worker-data-parallel.yaml index f66e58cc1c2..3c37b566890 100644 --- a/charts/llmisvc-resources/templates/config-llm-worker-data-parallel.yaml +++ b/charts/llmisvc-resources/templates/config-llm-worker-data-parallel.yaml @@ -3,68 +3,42 @@ kind: LLMInferenceServiceConfig metadata: name: kserve-config-llm-worker-data-parallel spec: - worker: + template: containers: - - image: ghcr.io/llm-d/llm-d:v0.2.0 + - image: ghcr.io/llm-d/llm-d-dev:v0.2.2 imagePullPolicy: IfNotPresent name: main ports: - containerPort: 8000 protocol: TCP - stdin: true - tty: true command: - "/bin/sh" - "-c" args: - |- - START_RANK=$(( ${LWS_WORKER_INDEX:-0} * {{ or .Spec.Parallelism.DataLocal 1 }} )) - if [ "${LWS_WORKER_INDEX:-0}" -eq 0 ]; then - ################# - # Leader-only launch - ################# - vllm serve \ - {{ .Spec.Model.Name }} \ - --port 8000 \ - --api-server-count 4 \ - --disable-log-requests \ - {{- if .Spec.Parallelism.Expert -}}--enable-expert-parallel \{{- end }} - {{- if .Spec.Parallelism.Tensor -}}--tensor-parallel-size {{ .Spec.Parallelism.Tensor }} \{{- end }} - --data-parallel-size {{ or .Spec.Parallelism.Data 1 }} \ - --data-parallel-size-local {{ or .Spec.Parallelism.DataLocal 1 }} \ - --data-parallel-address $(LWS_LEADER_ADDRESS) \ - --data-parallel-rpc-port {{ if .Spec.Parallelism.DataRPCPort }}{{ .Spec.Parallelism.DataRPCPort }}{{ else }}5555{{- end }} \ - --data-parallel-start-rank $START_RANK \ - --trust-remote-code \ - --enable-ssl-refresh \ - --ssl-certfile \ - /etc/ssl/certs/tls.crt \ - --ssl-keyfile \ - /etc/ssl/certs/tls.key - else - ################# - # Worker-only launch - ################# - vllm serve \ - {{ .Spec.Model.Name }} \ - --port 8000 \ - --disable-log-requests \ - {{- if .Spec.Parallelism.Expert }}--enable-expert-parallel \{{- end }} - {{- if .Spec.Parallelism.Tensor }}--tensor-parallel-size {{ .Spec.Parallelism.Tensor }} \{{- end }} - --data-parallel-size {{ or .Spec.Parallelism.Data 1 }} \ - --data-parallel-size-local {{ or .Spec.Parallelism.DataLocal 1 }} \ - --data-parallel-address $(LWS_LEADER_ADDRESS) \ - --data-parallel-rpc-port {{ if .Spec.Parallelism.DataRPCPort }}{{ .Spec.Parallelism.DataRPCPort }}{{ else }}5555{{- end }} \ - --data-parallel-start-rank $START_RANK \ - --trust-remote-code \ - --headless \ - --enable-ssl-refresh \ - --ssl-certfile \ - /etc/ssl/certs/tls.crt \ - --ssl-keyfile \ - /etc/ssl/certs/tls.key - fi + START_RANK=0 + eval "vllm serve \ + /mnt/models \ + --served-model-name "{{ .Spec.Model.Name }}" \ + --port 8000 \ + --api-server-count ${VLLM_API_SERVER_COUNT:-8} \ + --disable-log-requests \ + {{- if .Spec.Parallelism.Expert -}}--enable-expert-parallel{{- end }} \ + {{- if .Spec.Parallelism.Tensor -}}--tensor-parallel-size {{ .Spec.Parallelism.Tensor }}{{- end }} \ + --data-parallel-size $(( {{ or .Spec.Parallelism.DataLocal 1 }} * {{ or .Spec.Parallelism.Data 1 }} )) \ + --data-parallel-size-local {{ or .Spec.Parallelism.DataLocal 1 }} \ + --data-parallel-address $(LWS_LEADER_ADDRESS) \ + --data-parallel-rpc-port {{ if .Spec.Parallelism.DataRPCPort }}{{ .Spec.Parallelism.DataRPCPort }}{{ else }}5555{{- end }} \ + --data-parallel-start-rank $START_RANK \ + --data-parallel-hybrid-lb \ + ${VLLM_ADDITIONAL_ARGS} \ + --trust-remote-code \ + --enable-ssl-refresh \ + --ssl-certfile \ + /etc/ssl/certs/tls.crt \ + --ssl-keyfile \ + /etc/ssl/certs/tls.key" env: - name: HOME value: /home @@ -74,10 +48,16 @@ spec: value: /models securityContext: allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + runAsNonRoot: true capabilities: add: - "IPC_LOCK" - "SYS_RAWIO" + drop: + - ALL + seccompProfile: + type: RuntimeDefault terminationMessagePath: /dev/termination-log terminationMessagePolicy: FallbackToLogsOnError livenessProbe: @@ -85,7 +65,7 @@ spec: path: /health port: 8000 scheme: HTTPS - initialDelaySeconds: 120 + initialDelaySeconds: 300 periodSeconds: 10 timeoutSeconds: 10 failureThreshold: 3 @@ -94,8 +74,8 @@ spec: path: /health port: 8000 scheme: HTTPS - initialDelaySeconds: 10 - periodSeconds: 10 + initialDelaySeconds: 200 + periodSeconds: 30 timeoutSeconds: 5 failureThreshold: 60 volumeMounts: @@ -120,4 +100,84 @@ spec: name: model-cache - name: tls-certs secret: - secretName: "{{ ChildName .ObjectMeta.Name `-kserve-self-signed-certs` }}" \ No newline at end of file + secretName: "{{ ChildName .ObjectMeta.Name `-kserve-self-signed-certs` }}" + worker: + containers: + - image: ghcr.io/llm-d/llm-d-dev:v0.2.2 + imagePullPolicy: IfNotPresent + name: main + ports: + - containerPort: 8000 + protocol: TCP + command: + - "/bin/sh" + - "-c" + args: + - |- + + START_RANK=$(( ${LWS_WORKER_INDEX:-0} * {{ or .Spec.Parallelism.DataLocal 1 }} )) + eval "vllm serve \ + /mnt/models \ + --served-model-name "{{ .Spec.Model.Name }}" \ + --port 8000 \ + --disable-log-requests \ + {{- if .Spec.Parallelism.Expert }}--enable-expert-parallel{{- end }} \ + {{- if .Spec.Parallelism.Tensor }}--tensor-parallel-size {{ .Spec.Parallelism.Tensor }}{{- end }} \ + --data-parallel-size $(( {{ or .Spec.Parallelism.DataLocal 1 }} * {{ or .Spec.Parallelism.Data 1 }} )) \ + --data-parallel-size-local {{ or .Spec.Parallelism.DataLocal 1 }} \ + --data-parallel-address $(LWS_LEADER_ADDRESS) \ + --data-parallel-rpc-port {{ if .Spec.Parallelism.DataRPCPort }}{{ .Spec.Parallelism.DataRPCPort }}{{ else }}5555{{- end }} \ + --data-parallel-start-rank $START_RANK \ + --data-parallel-hybrid-lb \ + ${VLLM_ADDITIONAL_ARGS} \ + --trust-remote-code \ + --headless \ + --enable-ssl-refresh \ + --ssl-certfile \ + /etc/ssl/certs/tls.crt \ + --ssl-keyfile \ + /etc/ssl/certs/tls.key" + env: + - name: HOME + value: /home + - name: VLLM_LOGGING_LEVEL + value: INFO + - name: HF_HUB_CACHE + value: /models + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + runAsNonRoot: true + capabilities: + add: + - "IPC_LOCK" + - "SYS_RAWIO" + drop: + - ALL + seccompProfile: + type: RuntimeDefault + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: FallbackToLogsOnError + volumeMounts: + - mountPath: /home + name: home + - mountPath: /dev/shm + name: dshm + - mountPath: /models + name: model-cache + - mountPath: /etc/ssl/certs + name: tls-certs + readOnly: true + terminationGracePeriodSeconds: 30 + volumes: + - emptyDir: { } + name: home + - emptyDir: + medium: Memory + sizeLimit: 1Gi + name: dshm + - emptyDir: { } + name: model-cache + - name: tls-certs + secret: + secretName: "{{ ChildName .ObjectMeta.Name `-kserve-self-signed-certs` }}" diff --git a/config/crd/full/serving.kserve.io_llminferenceserviceconfigs.yaml b/config/crd/full/serving.kserve.io_llminferenceserviceconfigs.yaml index cc339954d46..936a3f3dcbd 100644 --- a/config/crd/full/serving.kserve.io_llminferenceserviceconfigs.yaml +++ b/config/crd/full/serving.kserve.io_llminferenceserviceconfigs.yaml @@ -46,24 +46,7 @@ spec: lora: properties: adapters: - items: - properties: - framework: - type: string - memory: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - storageUri: - type: string - required: - - framework - - memory - - storageUri - type: object - type: array + x-kubernetes-preserve-unknown-fields: true type: object name: type: string diff --git a/config/crd/full/serving.kserve.io_llminferenceservices.yaml b/config/crd/full/serving.kserve.io_llminferenceservices.yaml index d5169fe06ae..440f752d064 100644 --- a/config/crd/full/serving.kserve.io_llminferenceservices.yaml +++ b/config/crd/full/serving.kserve.io_llminferenceservices.yaml @@ -65,24 +65,7 @@ spec: lora: properties: adapters: - items: - properties: - framework: - type: string - memory: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - storageUri: - type: string - required: - - framework - - memory - - storageUri - type: object - type: array + x-kubernetes-preserve-unknown-fields: true type: object name: type: string diff --git a/config/llmisvc/config-llm-decode-template.yaml b/config/llmisvc/config-llm-decode-template.yaml index 07934abacf7..c12de1dfdcb 100644 --- a/config/llmisvc/config-llm-decode-template.yaml +++ b/config/llmisvc/config-llm-decode-template.yaml @@ -5,7 +5,7 @@ metadata: spec: template: containers: - - image: ghcr.io/llm-d/llm-d:v0.2.0 + - image: ghcr.io/llm-d/llm-d-dev:v0.2.2 imagePullPolicy: IfNotPresent name: main ports: @@ -14,6 +14,7 @@ spec: command: - vllm - serve + - /mnt/models args: - --served-model-name - "{{ .Spec.Model.Name }}" @@ -34,9 +35,13 @@ spec: value: /models securityContext: allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + runAsNonRoot: true capabilities: drop: - - MKNOD + - ALL + seccompProfile: + type: RuntimeDefault terminationMessagePath: /dev/termination-log terminationMessagePolicy: FallbackToLogsOnError livenessProbe: @@ -76,7 +81,13 @@ spec: - containerPort: 8000 protocol: TCP resources: { } - securityContext: { } + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + runAsNonRoot: true + capabilities: + drop: + - ALL terminationMessagePath: /dev/termination-log terminationMessagePolicy: FallbackToLogsOnError livenessProbe: @@ -100,6 +111,7 @@ spec: args: - "--port=8000" - "--vllm-port=8001" + - "--connector=nixlv2" - "--secure-proxy=true" - "--cert-path=/etc/ssl/certs" - "--decoder-use-tls=true" @@ -128,4 +140,4 @@ spec: name: model-cache - name: tls-certs secret: - secretName: "{{ ChildName .ObjectMeta.Name `-kserve-self-signed-certs` }}" \ No newline at end of file + secretName: "{{ ChildName .ObjectMeta.Name `-kserve-self-signed-certs` }}" diff --git a/config/llmisvc/config-llm-decode-worker-data-parallel.yaml b/config/llmisvc/config-llm-decode-worker-data-parallel.yaml index bf5e9c8e197..4dae126f69a 100644 --- a/config/llmisvc/config-llm-decode-worker-data-parallel.yaml +++ b/config/llmisvc/config-llm-decode-worker-data-parallel.yaml @@ -3,7 +3,7 @@ kind: LLMInferenceServiceConfig metadata: name: kserve-config-llm-decode-worker-data-parallel spec: - worker: + template: initContainers: - name: llm-d-routing-sidecar imagePullPolicy: IfNotPresent @@ -12,6 +12,15 @@ spec: ports: - containerPort: 8000 protocol: TCP + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault terminationMessagePath: /dev/termination-log terminationMessagePolicy: FallbackToLogsOnError livenessProbe: @@ -35,6 +44,7 @@ spec: args: - "--port=8000" - "--vllm-port=8001" + - "--connector=nixlv2" - "--secure-proxy=true" - "--cert-path=/etc/ssl/certs" - "--decoder-use-tls=true" @@ -52,66 +62,39 @@ spec: fieldRef: fieldPath: metadata.namespace containers: - - image: ghcr.io/llm-d/llm-d:v0.2.0 + - image: ghcr.io/llm-d/llm-d-dev:v0.2.2 imagePullPolicy: IfNotPresent name: main ports: - containerPort: 8001 protocol: TCP - stdin: true - tty: true command: - "/bin/sh" - "-c" args: - |- - - START_RANK=$(( ${LWS_WORKER_INDEX:-0} * {{ or .Spec.Parallelism.DataLocal 1 }} )) - if [ "${LWS_WORKER_INDEX:-0}" -eq 0 ]; then - ################# - # Leader-only launch - ################# - vllm serve \ - {{ .Spec.Model.Name }} \ - --port 8001 \ - --api-server-count 4 \ - --disable-log-requests \ - {{- if .Spec.Parallelism.Expert -}}--enable-expert-parallel \{{- end }} - {{- if .Spec.Parallelism.Tensor -}}--tensor-parallel-size {{ .Spec.Parallelism.Tensor }} \{{- end }} - --data-parallel-size {{ or .Spec.Parallelism.Data 1 }} \ - --data-parallel-size-local {{ or .Spec.Parallelism.DataLocal 1 }} \ - --data-parallel-address $(LWS_LEADER_ADDRESS) \ - --data-parallel-rpc-port {{ if .Spec.Parallelism.DataRPCPort }}{{ .Spec.Parallelism.DataRPCPort }}{{ else }}5555{{- end }} \ - --data-parallel-start-rank $START_RANK \ - --trust-remote-code \ - --enable-ssl-refresh \ - --ssl-certfile \ - /etc/ssl/certs/tls.crt \ - --ssl-keyfile \ - /etc/ssl/certs/tls.key - else - ################# - # Worker-only launch - ################# - vllm serve \ - {{ .Spec.Model.Name }} \ - --port 8001 \ - --disable-log-requests \ - {{- if .Spec.Parallelism.Expert }}--enable-expert-parallel \{{- end }} - {{- if .Spec.Parallelism.Tensor }}--tensor-parallel-size {{ .Spec.Parallelism.Tensor }} \{{- end }} - --data-parallel-size {{ or .Spec.Parallelism.Data 1 }} \ - --data-parallel-size-local {{ or .Spec.Parallelism.DataLocal 1 }} \ - --data-parallel-address $(LWS_LEADER_ADDRESS) \ - --data-parallel-rpc-port {{ if .Spec.Parallelism.DataRPCPort }}{{ .Spec.Parallelism.DataRPCPort }}{{ else }}5555{{- end }} \ - --data-parallel-start-rank $START_RANK \ - --trust-remote-code \ - --headless \ - --enable-ssl-refresh \ - --ssl-certfile \ - /etc/ssl/certs/tls.crt \ - --ssl-keyfile \ - /etc/ssl/certs/tls.key - fi + START_RANK=0 + eval "vllm serve \ + /mnt/models \ + --served-model-name "{{ .Spec.Model.Name }}" \ + --port 8001 \ + --api-server-count ${VLLM_API_SERVER_COUNT:-8} \ + --disable-log-requests \ + {{- if .Spec.Parallelism.Expert -}}--enable-expert-parallel{{- end }} \ + {{- if .Spec.Parallelism.Tensor -}}--tensor-parallel-size {{ .Spec.Parallelism.Tensor }}{{- end }} \ + --data-parallel-size $(( {{ or .Spec.Parallelism.DataLocal 1 }} * {{ or .Spec.Parallelism.Data 1 }} )) \ + --data-parallel-size-local {{ or .Spec.Parallelism.DataLocal 1 }} \ + --data-parallel-address $(LWS_LEADER_ADDRESS) \ + --data-parallel-rpc-port {{ if .Spec.Parallelism.DataRPCPort }}{{ .Spec.Parallelism.DataRPCPort }}{{ else }}5555{{- end }} \ + --data-parallel-start-rank $START_RANK \ + --data-parallel-hybrid-lb \ + ${VLLM_ADDITIONAL_ARGS} \ + --trust-remote-code \ + --enable-ssl-refresh \ + --ssl-certfile \ + /etc/ssl/certs/tls.crt \ + --ssl-keyfile \ + /etc/ssl/certs/tls.key" env: - name: HOME value: /home @@ -121,10 +104,16 @@ spec: value: /models securityContext: allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + runAsNonRoot: true capabilities: add: - "IPC_LOCK" - "SYS_RAWIO" + drop: + - ALL + seccompProfile: + type: RuntimeDefault terminationMessagePath: /dev/termination-log terminationMessagePolicy: FallbackToLogsOnError livenessProbe: @@ -132,7 +121,7 @@ spec: path: /health port: 8001 scheme: HTTPS - initialDelaySeconds: 120 + initialDelaySeconds: 300 periodSeconds: 10 timeoutSeconds: 10 failureThreshold: 3 @@ -141,8 +130,8 @@ spec: path: /health port: 8001 scheme: HTTPS - initialDelaySeconds: 10 - periodSeconds: 10 + initialDelaySeconds: 200 + periodSeconds: 30 timeoutSeconds: 5 failureThreshold: 60 volumeMounts: @@ -167,4 +156,85 @@ spec: name: model-cache - name: tls-certs secret: - secretName: "{{ ChildName .ObjectMeta.Name `-kserve-self-signed-certs` }}" \ No newline at end of file + secretName: "{{ ChildName .ObjectMeta.Name `-kserve-self-signed-certs` }}" + worker: + containers: + - image: ghcr.io/llm-d/llm-d-dev:v0.2.2 + imagePullPolicy: IfNotPresent + name: main + ports: + - containerPort: 8001 + protocol: TCP + command: + - "/bin/sh" + - "-c" + args: + - |- + START_RANK=$(( ${LWS_WORKER_INDEX:-0} * {{ or .Spec.Parallelism.DataLocal 1 }} )) + eval "vllm serve \ + /mnt/models \ + --served-model-name "{{ .Spec.Model.Name }}" \ + --port 8001 \ + --disable-log-requests \ + {{- if .Spec.Parallelism.Expert }}--enable-expert-parallel{{- end }} \ + {{- if .Spec.Parallelism.Tensor }}--tensor-parallel-size {{ .Spec.Parallelism.Tensor }}{{- end }} \ + --data-parallel-size $(( {{ or .Spec.Parallelism.DataLocal 1 }} * {{ or .Spec.Parallelism.Data 1 }} )) \ + --data-parallel-size-local {{ or .Spec.Parallelism.DataLocal 1 }} \ + --data-parallel-address $(LWS_LEADER_ADDRESS) \ + --data-parallel-rpc-port {{ if .Spec.Parallelism.DataRPCPort }}{{ .Spec.Parallelism.DataRPCPort }}{{ else }}5555{{- end }} \ + --data-parallel-start-rank $START_RANK \ + --data-parallel-hybrid-lb \ + ${VLLM_ADDITIONAL_ARGS} \ + --trust-remote-code \ + --headless \ + --enable-ssl-refresh \ + --ssl-certfile \ + /etc/ssl/certs/tls.crt \ + --ssl-keyfile \ + /etc/ssl/certs/tls.key" + env: + - name: HOME + value: /home + - name: VLLM_LOGGING_LEVEL + value: INFO + - name: HF_HUB_CACHE + value: /models + - name: VLLM_RANDOMIZE_DP_DUMMY_INPUTS + value: "1" + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + runAsNonRoot: true + capabilities: + add: + - "IPC_LOCK" + - "SYS_RAWIO" + drop: + - ALL + seccompProfile: + type: RuntimeDefault + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: FallbackToLogsOnError + volumeMounts: + - mountPath: /home + name: home + - mountPath: /dev/shm + name: dshm + - mountPath: /models + name: model-cache + - mountPath: /etc/ssl/certs + name: tls-certs + readOnly: true + terminationGracePeriodSeconds: 30 + volumes: + - emptyDir: { } + name: home + - emptyDir: + medium: Memory + sizeLimit: 1Gi + name: dshm + - emptyDir: { } + name: model-cache + - name: tls-certs + secret: + secretName: "{{ ChildName .ObjectMeta.Name `-kserve-self-signed-certs` }}" diff --git a/config/llmisvc/config-llm-prefill-template.yaml b/config/llmisvc/config-llm-prefill-template.yaml index 3b00fa0390f..82e1ebf89c4 100644 --- a/config/llmisvc/config-llm-prefill-template.yaml +++ b/config/llmisvc/config-llm-prefill-template.yaml @@ -6,7 +6,7 @@ spec: prefill: template: containers: - - image: ghcr.io/llm-d/llm-d:v0.2.0 + - image: ghcr.io/llm-d/llm-d-dev:v0.2.2 imagePullPolicy: IfNotPresent name: main ports: @@ -15,7 +15,7 @@ spec: command: - vllm - serve - - "{{ .Spec.Model.Name }}" + - /mnt/models args: - --served-model-name - "{{ .Spec.Model.Name }}" @@ -36,6 +36,13 @@ spec: value: /models securityContext: allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + runAsNonRoot: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault terminationMessagePath: /dev/termination-log terminationMessagePolicy: File livenessProbe: @@ -78,4 +85,4 @@ spec: name: model-cache - name: tls-certs secret: - secretName: "{{ ChildName .ObjectMeta.Name `-kserve-self-signed-certs` }}" \ No newline at end of file + secretName: "{{ ChildName .ObjectMeta.Name `-kserve-self-signed-certs` }}" diff --git a/config/llmisvc/config-llm-prefill-worker-data-parallel.yaml b/config/llmisvc/config-llm-prefill-worker-data-parallel.yaml index b9ef3b80e5e..1e90a6e2e02 100644 --- a/config/llmisvc/config-llm-prefill-worker-data-parallel.yaml +++ b/config/llmisvc/config-llm-prefill-worker-data-parallel.yaml @@ -4,68 +4,42 @@ metadata: name: kserve-config-llm-prefill-worker-data-parallel spec: prefill: - worker: + template: containers: - - image: ghcr.io/llm-d/llm-d:v0.2.0 + - image: ghcr.io/llm-d/llm-d-dev:v0.2.2 imagePullPolicy: IfNotPresent name: main ports: - containerPort: 8000 protocol: TCP - stdin: true - tty: true command: - "/bin/sh" - "-c" args: - |- - START_RANK=$(( ${LWS_WORKER_INDEX:-0} * {{ or .Spec.Prefill.Parallelism.DataLocal 1 }} )) - if [ "${LWS_WORKER_INDEX:-0}" -eq 0 ]; then - ################# - # Leader-only launch - ################# - vllm serve \ - {{ .Spec.Model.Name }} \ - --port 8000 \ - --api-server-count 4 \ - --disable-log-requests \ - {{- if .Spec.Prefill.Parallelism.Expert -}}--enable-expert-parallel \{{- end }} - {{- if .Spec.Prefill.Parallelism.Tensor -}}--tensor-parallel-size {{ .Spec.Prefill.Parallelism.Tensor }} \{{- end }} - --data-parallel-size {{ or .Spec.Prefill.Parallelism.Data 1 }} \ - --data-parallel-size-local {{ or .Spec.Prefill.Parallelism.DataLocal 1 }} \ - --data-parallel-address $(LWS_LEADER_ADDRESS) \ - --data-parallel-rpc-port {{ if .Spec.Prefill.Parallelism.DataRPCPort }}{{ .Spec.Prefill.Parallelism.DataRPCPort }}{{ else }}5555{{- end }} \ - --data-parallel-start-rank $START_RANK \ - --trust-remote-code \ - --enable-ssl-refresh \ - --ssl-certfile \ - /etc/ssl/certs/tls.crt \ - --ssl-keyfile \ - /etc/ssl/certs/tls.key - else - ################# - # Worker-only launch - ################# - vllm serve \ - {{ .Spec.Model.Name }} \ - --port 8000 \ - --disable-log-requests \ - {{- if .Spec.Prefill.Parallelism.Expert -}}--enable-expert-parallel \{{- end }} - {{- if .Spec.Prefill.Parallelism.Tensor -}}--tensor-parallel-size {{ .Spec.Prefill.Parallelism.Tensor }} \{{- end }} - --data-parallel-size {{ or .Spec.Prefill.Parallelism.Data 1 }} \ - --data-parallel-size-local {{ or .Spec.Prefill.Parallelism.DataLocal 1 }} \ - --data-parallel-address $(LWS_LEADER_ADDRESS) \ - --data-parallel-rpc-port {{ if .Spec.Prefill.Parallelism.DataRPCPort }}{{ .Spec.Prefill.Parallelism.DataRPCPort }}{{ else }}5555{{- end }} \ - --data-parallel-start-rank $START_RANK \ - --trust-remote-code \ - --headless \ - --enable-ssl-refresh \ - --ssl-certfile \ - /etc/ssl/certs/tls.crt \ - --ssl-keyfile \ - /etc/ssl/certs/tls.key - fi + START_RANK=0 + eval "vllm serve \ + /mnt/models \ + --served-model-name "{{ .Spec.Model.Name }}" \ + --port 8000 \ + --api-server-count ${VLLM_API_SERVER_COUNT:-8} \ + --disable-log-requests \ + {{- if .Spec.Prefill.Parallelism.Expert -}}--enable-expert-parallel{{- end }} \ + {{- if .Spec.Prefill.Parallelism.Tensor -}}--tensor-parallel-size {{ .Spec.Prefill.Parallelism.Tensor }}{{- end }} \ + --data-parallel-size $(( {{ or .Spec.Prefill.Parallelism.DataLocal 1 }} * {{ or .Spec.Prefill.Parallelism.Data 1 }} )) \ + --data-parallel-size-local {{ or .Spec.Prefill.Parallelism.DataLocal 1 }} \ + --data-parallel-address $(LWS_LEADER_ADDRESS) \ + --data-parallel-rpc-port {{ if .Spec.Prefill.Parallelism.DataRPCPort }}{{ .Spec.Prefill.Parallelism.DataRPCPort }}{{ else }}5555{{- end }} \ + --data-parallel-start-rank $START_RANK \ + --data-parallel-hybrid-lb \ + ${VLLM_ADDITIONAL_ARGS} \ + --trust-remote-code \ + --enable-ssl-refresh \ + --ssl-certfile \ + /etc/ssl/certs/tls.crt \ + --ssl-keyfile \ + /etc/ssl/certs/tls.key" env: - name: HOME value: /home @@ -75,10 +49,16 @@ spec: value: /models securityContext: allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + runAsNonRoot: true capabilities: add: - "IPC_LOCK" - "SYS_RAWIO" + drop: + - ALL + seccompProfile: + type: RuntimeDefault terminationMessagePath: /dev/termination-log terminationMessagePolicy: FallbackToLogsOnError livenessProbe: @@ -86,7 +66,7 @@ spec: path: /health port: 8000 scheme: HTTPS - initialDelaySeconds: 120 + initialDelaySeconds: 300 periodSeconds: 10 timeoutSeconds: 10 failureThreshold: 3 @@ -95,8 +75,8 @@ spec: path: /health port: 8000 scheme: HTTPS - initialDelaySeconds: 10 - periodSeconds: 10 + initialDelaySeconds: 200 + periodSeconds: 30 timeoutSeconds: 5 failureThreshold: 60 volumeMounts: @@ -121,4 +101,84 @@ spec: name: model-cache - name: tls-certs secret: - secretName: "{{ ChildName .ObjectMeta.Name `-kserve-self-signed-certs` }}" \ No newline at end of file + secretName: "{{ ChildName .ObjectMeta.Name `-kserve-self-signed-certs` }}" + worker: + containers: + - image: ghcr.io/llm-d/llm-d-dev:v0.2.2 + imagePullPolicy: IfNotPresent + name: main + ports: + - containerPort: 8000 + protocol: TCP + command: + - "/bin/sh" + - "-c" + args: + - |- + + START_RANK=$(( ${LWS_WORKER_INDEX:-0} * {{ or .Spec.Prefill.Parallelism.DataLocal 1 }} )) + eval "vllm serve \ + /mnt/models \ + --served-model-name "{{ .Spec.Model.Name }}" \ + --port 8000 \ + --disable-log-requests \ + {{- if .Spec.Prefill.Parallelism.Expert }}--enable-expert-parallel{{- end }} \ + {{- if .Spec.Prefill.Parallelism.Tensor }}--tensor-parallel-size {{ .Spec.Prefill.Parallelism.Tensor }}{{- end }} \ + --data-parallel-size $(( {{ or .Spec.Prefill.Parallelism.DataLocal 1 }} * {{ or .Spec.Prefill.Parallelism.Data 1 }} )) \ + --data-parallel-size-local {{ or .Spec.Prefill.Parallelism.DataLocal 1 }} \ + --data-parallel-address $(LWS_LEADER_ADDRESS) \ + --data-parallel-rpc-port {{ if .Spec.Prefill.Parallelism.DataRPCPort }}{{ .Spec.Prefill.Parallelism.DataRPCPort }}{{ else }}5555{{- end }} \ + --data-parallel-start-rank $START_RANK \ + --data-parallel-hybrid-lb \ + ${VLLM_ADDITIONAL_ARGS} \ + --trust-remote-code \ + --headless \ + --enable-ssl-refresh \ + --ssl-certfile \ + /etc/ssl/certs/tls.crt \ + --ssl-keyfile \ + /etc/ssl/certs/tls.key" + env: + - name: HOME + value: /home + - name: VLLM_LOGGING_LEVEL + value: INFO + - name: HF_HUB_CACHE + value: /models + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + runAsNonRoot: true + capabilities: + add: + - "IPC_LOCK" + - "SYS_RAWIO" + drop: + - ALL + seccompProfile: + type: RuntimeDefault + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: FallbackToLogsOnError + volumeMounts: + - mountPath: /home + name: home + - mountPath: /dev/shm + name: dshm + - mountPath: /models + name: model-cache + - mountPath: /etc/ssl/certs + name: tls-certs + readOnly: true + terminationGracePeriodSeconds: 30 + volumes: + - emptyDir: { } + name: home + - emptyDir: + medium: Memory + sizeLimit: 1Gi + name: dshm + - emptyDir: { } + name: model-cache + - name: tls-certs + secret: + secretName: "{{ ChildName .ObjectMeta.Name `-kserve-self-signed-certs` }}" diff --git a/config/llmisvc/config-llm-router-route.yaml b/config/llmisvc/config-llm-router-route.yaml index bf4b5e8145c..39646092b25 100644 --- a/config/llmisvc/config-llm-router-route.yaml +++ b/config/llmisvc/config-llm-router-route.yaml @@ -35,4 +35,4 @@ spec: replacePrefixMatch: / timeouts: backendRequest: 0s - request: 0s \ No newline at end of file + request: 0s diff --git a/config/llmisvc/config-llm-scheduler.yaml b/config/llmisvc/config-llm-scheduler.yaml index fc614fd7b0e..1f5965d2e24 100644 --- a/config/llmisvc/config-llm-scheduler.yaml +++ b/config/llmisvc/config-llm-scheduler.yaml @@ -59,6 +59,9 @@ spec: - --grpcHealthPort - "9003" - --secureServing + - --modelServerMetricsScheme + - "https" + - --modelServerMetricsHttpsInsecureSkipVerify - --certPath - "/etc/ssl/certs" resources: @@ -86,4 +89,4 @@ spec: secretName: "{{ ChildName .ObjectMeta.Name `-kserve-self-signed-certs` }}" dnsPolicy: ClusterFirst restartPolicy: Always - terminationGracePeriodSeconds: 30 \ No newline at end of file + terminationGracePeriodSeconds: 30 diff --git a/config/llmisvc/config-llm-template.yaml b/config/llmisvc/config-llm-template.yaml index b731c15ce38..f021cc03cfd 100644 --- a/config/llmisvc/config-llm-template.yaml +++ b/config/llmisvc/config-llm-template.yaml @@ -5,7 +5,7 @@ metadata: spec: template: containers: - - image: ghcr.io/llm-d/llm-d:v0.2.0 + - image: ghcr.io/llm-d/llm-d-dev:v0.2.2 imagePullPolicy: IfNotPresent name: main ports: @@ -14,6 +14,7 @@ spec: command: - vllm - serve + - /mnt/models args: - --served-model-name - "{{ .Spec.Model.Name }}" @@ -34,9 +35,13 @@ spec: value: /models securityContext: allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + runAsNonRoot: true capabilities: drop: - - MKNOD + - ALL + seccompProfile: + type: RuntimeDefault terminationMessagePath: /dev/termination-log terminationMessagePolicy: FallbackToLogsOnError livenessProbe: @@ -79,4 +84,4 @@ spec: name: model-cache - name: tls-certs secret: - secretName: "{{ ChildName .ObjectMeta.Name `-kserve-self-signed-certs` }}" \ No newline at end of file + secretName: "{{ ChildName .ObjectMeta.Name `-kserve-self-signed-certs` }}" diff --git a/config/llmisvc/config-llm-worker-data-parallel.yaml b/config/llmisvc/config-llm-worker-data-parallel.yaml index f66e58cc1c2..3c37b566890 100644 --- a/config/llmisvc/config-llm-worker-data-parallel.yaml +++ b/config/llmisvc/config-llm-worker-data-parallel.yaml @@ -3,68 +3,42 @@ kind: LLMInferenceServiceConfig metadata: name: kserve-config-llm-worker-data-parallel spec: - worker: + template: containers: - - image: ghcr.io/llm-d/llm-d:v0.2.0 + - image: ghcr.io/llm-d/llm-d-dev:v0.2.2 imagePullPolicy: IfNotPresent name: main ports: - containerPort: 8000 protocol: TCP - stdin: true - tty: true command: - "/bin/sh" - "-c" args: - |- - START_RANK=$(( ${LWS_WORKER_INDEX:-0} * {{ or .Spec.Parallelism.DataLocal 1 }} )) - if [ "${LWS_WORKER_INDEX:-0}" -eq 0 ]; then - ################# - # Leader-only launch - ################# - vllm serve \ - {{ .Spec.Model.Name }} \ - --port 8000 \ - --api-server-count 4 \ - --disable-log-requests \ - {{- if .Spec.Parallelism.Expert -}}--enable-expert-parallel \{{- end }} - {{- if .Spec.Parallelism.Tensor -}}--tensor-parallel-size {{ .Spec.Parallelism.Tensor }} \{{- end }} - --data-parallel-size {{ or .Spec.Parallelism.Data 1 }} \ - --data-parallel-size-local {{ or .Spec.Parallelism.DataLocal 1 }} \ - --data-parallel-address $(LWS_LEADER_ADDRESS) \ - --data-parallel-rpc-port {{ if .Spec.Parallelism.DataRPCPort }}{{ .Spec.Parallelism.DataRPCPort }}{{ else }}5555{{- end }} \ - --data-parallel-start-rank $START_RANK \ - --trust-remote-code \ - --enable-ssl-refresh \ - --ssl-certfile \ - /etc/ssl/certs/tls.crt \ - --ssl-keyfile \ - /etc/ssl/certs/tls.key - else - ################# - # Worker-only launch - ################# - vllm serve \ - {{ .Spec.Model.Name }} \ - --port 8000 \ - --disable-log-requests \ - {{- if .Spec.Parallelism.Expert }}--enable-expert-parallel \{{- end }} - {{- if .Spec.Parallelism.Tensor }}--tensor-parallel-size {{ .Spec.Parallelism.Tensor }} \{{- end }} - --data-parallel-size {{ or .Spec.Parallelism.Data 1 }} \ - --data-parallel-size-local {{ or .Spec.Parallelism.DataLocal 1 }} \ - --data-parallel-address $(LWS_LEADER_ADDRESS) \ - --data-parallel-rpc-port {{ if .Spec.Parallelism.DataRPCPort }}{{ .Spec.Parallelism.DataRPCPort }}{{ else }}5555{{- end }} \ - --data-parallel-start-rank $START_RANK \ - --trust-remote-code \ - --headless \ - --enable-ssl-refresh \ - --ssl-certfile \ - /etc/ssl/certs/tls.crt \ - --ssl-keyfile \ - /etc/ssl/certs/tls.key - fi + START_RANK=0 + eval "vllm serve \ + /mnt/models \ + --served-model-name "{{ .Spec.Model.Name }}" \ + --port 8000 \ + --api-server-count ${VLLM_API_SERVER_COUNT:-8} \ + --disable-log-requests \ + {{- if .Spec.Parallelism.Expert -}}--enable-expert-parallel{{- end }} \ + {{- if .Spec.Parallelism.Tensor -}}--tensor-parallel-size {{ .Spec.Parallelism.Tensor }}{{- end }} \ + --data-parallel-size $(( {{ or .Spec.Parallelism.DataLocal 1 }} * {{ or .Spec.Parallelism.Data 1 }} )) \ + --data-parallel-size-local {{ or .Spec.Parallelism.DataLocal 1 }} \ + --data-parallel-address $(LWS_LEADER_ADDRESS) \ + --data-parallel-rpc-port {{ if .Spec.Parallelism.DataRPCPort }}{{ .Spec.Parallelism.DataRPCPort }}{{ else }}5555{{- end }} \ + --data-parallel-start-rank $START_RANK \ + --data-parallel-hybrid-lb \ + ${VLLM_ADDITIONAL_ARGS} \ + --trust-remote-code \ + --enable-ssl-refresh \ + --ssl-certfile \ + /etc/ssl/certs/tls.crt \ + --ssl-keyfile \ + /etc/ssl/certs/tls.key" env: - name: HOME value: /home @@ -74,10 +48,16 @@ spec: value: /models securityContext: allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + runAsNonRoot: true capabilities: add: - "IPC_LOCK" - "SYS_RAWIO" + drop: + - ALL + seccompProfile: + type: RuntimeDefault terminationMessagePath: /dev/termination-log terminationMessagePolicy: FallbackToLogsOnError livenessProbe: @@ -85,7 +65,7 @@ spec: path: /health port: 8000 scheme: HTTPS - initialDelaySeconds: 120 + initialDelaySeconds: 300 periodSeconds: 10 timeoutSeconds: 10 failureThreshold: 3 @@ -94,8 +74,8 @@ spec: path: /health port: 8000 scheme: HTTPS - initialDelaySeconds: 10 - periodSeconds: 10 + initialDelaySeconds: 200 + periodSeconds: 30 timeoutSeconds: 5 failureThreshold: 60 volumeMounts: @@ -120,4 +100,84 @@ spec: name: model-cache - name: tls-certs secret: - secretName: "{{ ChildName .ObjectMeta.Name `-kserve-self-signed-certs` }}" \ No newline at end of file + secretName: "{{ ChildName .ObjectMeta.Name `-kserve-self-signed-certs` }}" + worker: + containers: + - image: ghcr.io/llm-d/llm-d-dev:v0.2.2 + imagePullPolicy: IfNotPresent + name: main + ports: + - containerPort: 8000 + protocol: TCP + command: + - "/bin/sh" + - "-c" + args: + - |- + + START_RANK=$(( ${LWS_WORKER_INDEX:-0} * {{ or .Spec.Parallelism.DataLocal 1 }} )) + eval "vllm serve \ + /mnt/models \ + --served-model-name "{{ .Spec.Model.Name }}" \ + --port 8000 \ + --disable-log-requests \ + {{- if .Spec.Parallelism.Expert }}--enable-expert-parallel{{- end }} \ + {{- if .Spec.Parallelism.Tensor }}--tensor-parallel-size {{ .Spec.Parallelism.Tensor }}{{- end }} \ + --data-parallel-size $(( {{ or .Spec.Parallelism.DataLocal 1 }} * {{ or .Spec.Parallelism.Data 1 }} )) \ + --data-parallel-size-local {{ or .Spec.Parallelism.DataLocal 1 }} \ + --data-parallel-address $(LWS_LEADER_ADDRESS) \ + --data-parallel-rpc-port {{ if .Spec.Parallelism.DataRPCPort }}{{ .Spec.Parallelism.DataRPCPort }}{{ else }}5555{{- end }} \ + --data-parallel-start-rank $START_RANK \ + --data-parallel-hybrid-lb \ + ${VLLM_ADDITIONAL_ARGS} \ + --trust-remote-code \ + --headless \ + --enable-ssl-refresh \ + --ssl-certfile \ + /etc/ssl/certs/tls.crt \ + --ssl-keyfile \ + /etc/ssl/certs/tls.key" + env: + - name: HOME + value: /home + - name: VLLM_LOGGING_LEVEL + value: INFO + - name: HF_HUB_CACHE + value: /models + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + runAsNonRoot: true + capabilities: + add: + - "IPC_LOCK" + - "SYS_RAWIO" + drop: + - ALL + seccompProfile: + type: RuntimeDefault + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: FallbackToLogsOnError + volumeMounts: + - mountPath: /home + name: home + - mountPath: /dev/shm + name: dshm + - mountPath: /models + name: model-cache + - mountPath: /etc/ssl/certs + name: tls-certs + readOnly: true + terminationGracePeriodSeconds: 30 + volumes: + - emptyDir: { } + name: home + - emptyDir: + medium: Memory + sizeLimit: 1Gi + name: dshm + - emptyDir: { } + name: model-cache + - name: tls-certs + secret: + secretName: "{{ ChildName .ObjectMeta.Name `-kserve-self-signed-certs` }}" diff --git a/config/llmisvc/kustomization.yaml b/config/llmisvc/kustomization.yaml index a29a5458d69..904a145e76e 100644 --- a/config/llmisvc/kustomization.yaml +++ b/config/llmisvc/kustomization.yaml @@ -11,4 +11,4 @@ resources: - config-llm-router-route.yaml - config-llm-scheduler.yaml - config-llm-template.yaml - - config-llm-worker-data-parallel.yaml \ No newline at end of file + - config-llm-worker-data-parallel.yaml diff --git a/pkg/apis/serving/v1alpha1/llm_inference_service_defaults.go b/pkg/apis/serving/v1alpha1/llm_inference_service_defaults.go index 7eab13f8612..69bcdcbdc20 100644 --- a/pkg/apis/serving/v1alpha1/llm_inference_service_defaults.go +++ b/pkg/apis/serving/v1alpha1/llm_inference_service_defaults.go @@ -1,5 +1,4 @@ /* - Copyright 2025 The KServe Authors. Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,14 +19,35 @@ package v1alpha1 import ( "context" + corev1 "k8s.io/api/core/v1" "k8s.io/utils/ptr" "knative.dev/pkg/apis" ) var _ apis.Defaultable = &LLMInferenceService{} -func (in *LLMInferenceService) SetDefaults(_ context.Context) { +func (in *LLMInferenceService) SetDefaults(ctx context.Context) { if in.Spec.Model.Name == nil || *in.Spec.Model.Name == "" { in.Spec.Model.Name = ptr.To(in.GetName()) } + in.Spec.SetDefaults(ctx) +} + +func (in *LLMInferenceServiceSpec) SetDefaults(_ context.Context) { + // Setting containers to empty slices will prevent the merge logic from removing containers. + // This happens only on `Containers` because they don't have the `omitempty` tag and json.Marshal always keeps them. + + if in.WorkloadSpec.Template != nil && in.WorkloadSpec.Template.Containers == nil { + in.WorkloadSpec.Template.Containers = []corev1.Container{} + } + if in.WorkloadSpec.Worker != nil && in.WorkloadSpec.Worker.Containers == nil { + in.WorkloadSpec.Worker.Containers = []corev1.Container{} + } + + if in.Prefill != nil && in.Prefill.Template != nil && in.Prefill.Template.Containers == nil { + in.Prefill.Template.Containers = []corev1.Container{} + } + if in.Prefill != nil && in.Prefill.Worker != nil && in.Prefill.Worker.Containers == nil { + in.Prefill.Worker.Containers = []corev1.Container{} + } } diff --git a/pkg/apis/serving/v1alpha1/llm_inference_service_types.go b/pkg/apis/serving/v1alpha1/llm_inference_service_types.go index 85c50d103dc..e7eef2edbc9 100644 --- a/pkg/apis/serving/v1alpha1/llm_inference_service_types.go +++ b/pkg/apis/serving/v1alpha1/llm_inference_service_types.go @@ -141,7 +141,10 @@ type LoRASpec struct { // Adapters is the static specification for one or more LoRA adapters. // Each adapter is defined by its own ModelSpec. // +optional - Adapters []ModelSpec `json:"adapters,omitempty"` + // This type is recursive https://github.com/kubernetes-sigs/controller-tools/issues/585 + // +kubebuilder:pruning:PreserveUnknownFields + // +kubebuilder:validation:Schemaless + Adapters []LLMModelSpec `json:"adapters,omitempty"` } // RouterSpec defines the routing configuration for exposing the service. diff --git a/pkg/apis/serving/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/serving/v1alpha1/zz_generated.deepcopy.go index d1874abe10b..29a773cc821 100644 --- a/pkg/apis/serving/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/serving/v1alpha1/zz_generated.deepcopy.go @@ -731,7 +731,7 @@ func (in *LoRASpec) DeepCopyInto(out *LoRASpec) { *out = *in if in.Adapters != nil { in, out := &in.Adapters, &out.Adapters - *out = make([]ModelSpec, len(*in)) + *out = make([]LLMModelSpec, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } diff --git a/pkg/controller/v1alpha1/llmisvc/config_merge.go b/pkg/controller/v1alpha1/llmisvc/config_merge.go index 27e785438a4..3e27202dfef 100644 --- a/pkg/controller/v1alpha1/llmisvc/config_merge.go +++ b/pkg/controller/v1alpha1/llmisvc/config_merge.go @@ -23,17 +23,19 @@ import ( "fmt" "text/template" - "github.com/kserve/kserve/pkg/constants" - corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/strategicpatch" + "k8s.io/utils/ptr" "knative.dev/pkg/kmeta" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" igwapi "sigs.k8s.io/gateway-api-inference-extension/api/v1alpha2" + gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" "github.com/kserve/kserve/pkg/apis/serving/v1alpha1" + "github.com/kserve/kserve/pkg/constants" ) const ( @@ -73,6 +75,8 @@ var WellKnownDefaultConfigs = sets.New[string]( // enabled. These LLMInferenceServiceConfig resources must exist in either resource namespace (prioritized) or // SystemNamespace (e.g. `kserve`). func (r *LLMISVCReconciler) combineBaseRefsConfig(ctx context.Context, llmSvc *v1alpha1.LLMInferenceService, reconcilerConfig *Config) (*v1alpha1.LLMInferenceServiceConfig, error) { + logger := log.FromContext(ctx).WithName("combineBaseRefsConfig") + // Creates the initial spec with the merged BaseRefs, so that we know what's "Enabled". resolvedSpec := *llmSvc.Spec.DeepCopy() for _, ref := range llmSvc.Spec.BaseRefs { @@ -82,7 +86,7 @@ func (r *LLMISVCReconciler) combineBaseRefsConfig(ctx context.Context, llmSvc *v } if cfg != nil { var resolvedErr error - resolvedSpec, resolvedErr = mergeSpecs(resolvedSpec, cfg.Spec) + resolvedSpec, resolvedErr = mergeSpecs(ctx, resolvedSpec, cfg.Spec) if resolvedErr != nil { return nil, fmt.Errorf("failed to merge specs: %w", resolvedErr) } @@ -94,6 +98,8 @@ func (r *LLMISVCReconciler) combineBaseRefsConfig(ctx context.Context, llmSvc *v llmSvc.Spec.Model.Name = resolvedSpec.Model.Name } + logger.V(2).Info("Resolved spec", "spec", resolvedSpec) + refs := make([]corev1.LocalObjectReference, 0, len(llmSvc.Spec.BaseRefs)) if resolvedSpec.Router != nil && resolvedSpec.Router.Scheduler != nil && !resolvedSpec.Router.Scheduler.Pool.HasRef() { refs = append(refs, corev1.LocalObjectReference{Name: configRouterSchedulerName}) @@ -101,30 +107,50 @@ func (r *LLMISVCReconciler) combineBaseRefsConfig(ctx context.Context, llmSvc *v if resolvedSpec.Router != nil && resolvedSpec.Router.Route != nil && !resolvedSpec.Router.Route.HTTP.HasRefs() { refs = append(refs, corev1.LocalObjectReference{Name: configRouterRouteName}) } - switch { - // Disaggregated prefill and decode (P/D) cases. - case resolvedSpec.Prefill != nil && resolvedSpec.Prefill.Worker == nil: - refs = append(refs, corev1.LocalObjectReference{Name: configPrefillTemplateName}) - refs = append(refs, corev1.LocalObjectReference{Name: configDecodeTemplateName}) - case resolvedSpec.Prefill != nil && resolvedSpec.Prefill.Worker != nil && resolvedSpec.Prefill.Parallelism.IsPipelineParallel(): - refs = append(refs, corev1.LocalObjectReference{Name: configDecodeWorkerPipelineParallelName}) - refs = append(refs, corev1.LocalObjectReference{Name: configPrefillWorkerPipelineParallelName}) - case resolvedSpec.Prefill != nil && resolvedSpec.Prefill.Worker != nil && resolvedSpec.Prefill.Parallelism.IsDataParallel(): - refs = append(refs, corev1.LocalObjectReference{Name: configDecodeWorkerDataParallelName}) - refs = append(refs, corev1.LocalObjectReference{Name: configPrefillWorkerDataParallelName}) - // Multi Node without Disaggregated prefill and decode (P/D) cases. - case resolvedSpec.Worker != nil && resolvedSpec.Parallelism.IsPipelineParallel(): - refs = append(refs, corev1.LocalObjectReference{Name: configWorkerPipelineParallelName}) - case resolvedSpec.Worker != nil && resolvedSpec.Parallelism.IsDataParallel(): - refs = append(refs, corev1.LocalObjectReference{Name: configWorkerDataParallelName}) - default: - // Single Node case. - refs = append(refs, corev1.LocalObjectReference{Name: configTemplateName}) + + if resolvedSpec.Prefill != nil { // P/D + // Prefill + switch { + case resolvedSpec.Prefill.Worker == nil: + // single-node prefill + refs = append(refs, corev1.LocalObjectReference{Name: configPrefillTemplateName}) + case resolvedSpec.Prefill.Worker != nil && resolvedSpec.Prefill.Parallelism.IsDataParallel(): + // multi-node Data Parallel prefill + refs = append(refs, corev1.LocalObjectReference{Name: configPrefillWorkerDataParallelName}) + case resolvedSpec.Prefill.Worker != nil && resolvedSpec.Prefill.Parallelism.IsPipelineParallel(): + // multi-node Pipeline Parallel prefill + refs = append(refs, corev1.LocalObjectReference{Name: configPrefillWorkerPipelineParallelName}) + } + // Decode + switch { + case resolvedSpec.Worker == nil: + // single-node decode + refs = append(refs, corev1.LocalObjectReference{Name: configDecodeTemplateName}) + case resolvedSpec.Worker != nil && resolvedSpec.Parallelism.IsDataParallel(): + // multi-node Data Parallel decode + refs = append(refs, corev1.LocalObjectReference{Name: configDecodeWorkerDataParallelName}) + case resolvedSpec.Worker != nil && resolvedSpec.Parallelism.IsPipelineParallel(): + // multi-node Pipeline Parallel decode + refs = append(refs, corev1.LocalObjectReference{Name: configDecodeWorkerPipelineParallelName}) + } + } else { // Non P/D + switch { + case resolvedSpec.Worker == nil: + // single-node + refs = append(refs, corev1.LocalObjectReference{Name: configTemplateName}) + case resolvedSpec.Worker != nil && resolvedSpec.Parallelism.IsDataParallel(): + // multi-node Data Parallel + refs = append(refs, corev1.LocalObjectReference{Name: configWorkerDataParallelName}) + case resolvedSpec.Worker != nil && resolvedSpec.Parallelism.IsPipelineParallel(): + // multi-node Pipeline Parallel + refs = append(refs, corev1.LocalObjectReference{Name: configWorkerPipelineParallelName}) + } } + // Append explicit base refs to override well know configs. refs = append(refs, llmSvc.Spec.BaseRefs...) - specs := make([]v1alpha1.LLMInferenceServiceSpec, 0, len(llmSvc.Spec.BaseRefs)+1) + specs := make([]v1alpha1.LLMInferenceServiceSpec, 0, len(refs)) for _, ref := range refs { cfg, err := r.getConfig(ctx, llmSvc, ref.Name) if err != nil { @@ -134,7 +160,7 @@ func (r *LLMISVCReconciler) combineBaseRefsConfig(ctx context.Context, llmSvc *v specs = append(specs, cfg.Spec) } } - spec, err := MergeSpecs(append(specs, llmSvc.Spec)...) + spec, err := MergeSpecs(ctx, append(specs, llmSvc.Spec)...) if err != nil { return nil, fmt.Errorf("failed to merge specs: %w", err) } @@ -170,6 +196,38 @@ func (r *LLMISVCReconciler) combineBaseRefsConfig(ctx context.Context, llmSvc *v return llmSvcCfg, err } + // Point HTTPRoute to a Service if there is no Scheduler or InferencePool, and the HTTPRoute uses the default + // InferencePool (to handle cases where the HTTPRoute Spec uses a custom BackendRef). + if llmSvcCfg.Spec.Router != nil && + llmSvcCfg.Spec.Router.Route != nil && + llmSvcCfg.Spec.Router.Route.HTTP.HasSpec() && + llmSvcCfg.Spec.Router.Scheduler == nil { + for i := range llmSvcCfg.Spec.Router.Route.HTTP.Spec.Rules { + for j := range llmSvcCfg.Spec.Router.Route.HTTP.Spec.Rules[i].BackendRefs { + if isDefaultBackendRef(llmSvc, llmSvcCfg.Spec.Router.Route.HTTP.Spec.Rules[i].BackendRefs[j].BackendRef) { + llmSvcCfg.Spec.Router.Route.HTTP.Spec.Rules[i].BackendRefs[j].Group = ptr.To[gwapiv1.Group]("") + llmSvcCfg.Spec.Router.Route.HTTP.Spec.Rules[i].BackendRefs[j].Kind = ptr.To[gwapiv1.Kind]("Service") + llmSvcCfg.Spec.Router.Route.HTTP.Spec.Rules[i].BackendRefs[j].Name = gwapiv1.ObjectName(kmeta.ChildName(llmSvc.GetName(), "-kserve-workload-svc")) + } + } + } + } + + // Point HTTPRoute to InferencePool reference if specified. + if llmSvcCfg.Spec.Router != nil && + llmSvcCfg.Spec.Router.Route != nil && + llmSvcCfg.Spec.Router.Route.HTTP.HasSpec() && + llmSvcCfg.Spec.Router.Scheduler != nil && + llmSvcCfg.Spec.Router.Scheduler.Pool.HasRef() { + for i := range llmSvcCfg.Spec.Router.Route.HTTP.Spec.Rules { + for j := range llmSvcCfg.Spec.Router.Route.HTTP.Spec.Rules[i].BackendRefs { + if isDefaultBackendRef(llmSvc, llmSvcCfg.Spec.Router.Route.HTTP.Spec.Rules[i].BackendRefs[j].BackendRef) { + llmSvcCfg.Spec.Router.Route.HTTP.Spec.Rules[i].BackendRefs[j].Name = gwapiv1.ObjectName(llmSvcCfg.Spec.Router.Scheduler.Pool.Ref.Name) + } + } + } + } + return llmSvcCfg, nil } @@ -222,7 +280,7 @@ func (r *LLMISVCReconciler) getConfig(ctx context.Context, llmSvc *v1alpha1.LLMI return cfg, nil } -func MergeSpecs(cfgs ...v1alpha1.LLMInferenceServiceSpec) (v1alpha1.LLMInferenceServiceSpec, error) { +func MergeSpecs(ctx context.Context, cfgs ...v1alpha1.LLMInferenceServiceSpec) (v1alpha1.LLMInferenceServiceSpec, error) { if len(cfgs) == 0 { return v1alpha1.LLMInferenceServiceSpec{}, nil } @@ -231,7 +289,7 @@ func MergeSpecs(cfgs ...v1alpha1.LLMInferenceServiceSpec) (v1alpha1.LLMInference for i := 1; i < len(cfgs); i++ { cfg := cfgs[i] var err error - out, err = mergeSpecs(out, cfg) + out, err = mergeSpecs(ctx, out, cfg) if err != nil { return v1alpha1.LLMInferenceServiceSpec{}, fmt.Errorf("failed to merge specs: %w", err) } @@ -241,7 +299,7 @@ func MergeSpecs(cfgs ...v1alpha1.LLMInferenceServiceSpec) (v1alpha1.LLMInference // mergeSpecs performs a strategic merge by creating a clean patch from the override // object and applying it to the base object. -func mergeSpecs(base, override v1alpha1.LLMInferenceServiceSpec) (v1alpha1.LLMInferenceServiceSpec, error) { +func mergeSpecs(ctx context.Context, base, override v1alpha1.LLMInferenceServiceSpec) (v1alpha1.LLMInferenceServiceSpec, error) { baseJSON, err := json.Marshal(base) if err != nil { return v1alpha1.LLMInferenceServiceSpec{}, fmt.Errorf("could not marshal base spec: %w", err) @@ -257,17 +315,23 @@ func mergeSpecs(base, override v1alpha1.LLMInferenceServiceSpec) (v1alpha1.LLMIn return v1alpha1.LLMInferenceServiceSpec{}, fmt.Errorf("could not marshal zero spec: %w", err) } + override.SetDefaults(ctx) + overrideJSON, err := json.Marshal(override) if err != nil { return v1alpha1.LLMInferenceServiceSpec{}, fmt.Errorf("could not marshal override spec: %w", err) } + logger := log.FromContext(ctx) + // Create the patch. It will only contain the non-default fields from the override. patch, err := strategicpatch.CreateTwoWayMergePatch(zeroJSON, overrideJSON, v1alpha1.LLMInferenceServiceSpec{}) if err != nil { return v1alpha1.LLMInferenceServiceSpec{}, fmt.Errorf("could not create merge patch from override: %w", err) } + logger.V(2).Info("merging specs (patch)", "patch", string(patch), "base", string(baseJSON), "override", string(overrideJSON), "zero", string(zeroJSON)) + // Apply this "clean" patch to the base JSON. The strategic merge logic will correctly // merge lists and objects based on their Kubernetes patch strategy annotations. mergedJSON, err := strategicpatch.StrategicMergePatch(baseJSON, patch, v1alpha1.LLMInferenceServiceSpec{}) @@ -282,3 +346,10 @@ func mergeSpecs(base, override v1alpha1.LLMInferenceServiceSpec) (v1alpha1.LLMIn } return finalSpec, nil } + +func isDefaultBackendRef(llmSvc *v1alpha1.LLMInferenceService, ref gwapiv1.BackendRef) bool { + defaultInfPoolName := (&v1alpha1.SchedulerSpec{}).InferencePoolName(llmSvc) + return ptr.Deref[gwapiv1.Group](ref.Group, "") == igwapi.GroupName && + ptr.Deref[gwapiv1.Kind](ref.Kind, "") == "InferencePool" && + string(ref.Name) == defaultInfPoolName +} diff --git a/pkg/controller/v1alpha1/llmisvc/config_merge_test.go b/pkg/controller/v1alpha1/llmisvc/config_merge_test.go index b26f74f1093..3e1eaf4263b 100644 --- a/pkg/controller/v1alpha1/llmisvc/config_merge_test.go +++ b/pkg/controller/v1alpha1/llmisvc/config_merge_test.go @@ -19,6 +19,14 @@ package llmisvc_test import ( "testing" + . "github.com/onsi/gomega" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/yaml" + + ktesting "github.com/kserve/kserve/pkg/testing" + + "github.com/kserve/kserve/pkg/controller/v1alpha1/llmisvc" + "github.com/google/go-cmp/cmp" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" @@ -29,7 +37,7 @@ import ( gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" "github.com/kserve/kserve/pkg/apis/serving/v1alpha1" - "github.com/kserve/kserve/pkg/controller/v1alpha1/llmisvc" + pkgtest "github.com/kserve/kserve/pkg/testing" ) func TestMergeSpecs(t *testing.T) { @@ -48,18 +56,58 @@ func TestMergeSpecs(t *testing.T) { { name: "single config", cfgs: []v1alpha1.LLMInferenceServiceSpec{ - {Model: v1alpha1.LLMModelSpec{URI: mustParseURL("model-a")}}, + {Model: v1alpha1.LLMModelSpec{URI: apis.URL{Path: "model-a"}}}, }, - want: v1alpha1.LLMInferenceServiceSpec{Model: v1alpha1.LLMModelSpec{URI: mustParseURL("model-a")}}, + want: v1alpha1.LLMInferenceServiceSpec{Model: v1alpha1.LLMModelSpec{URI: apis.URL{Path: "model-a"}}}, wantErr: false, }, { name: "two configs simple merge", cfgs: []v1alpha1.LLMInferenceServiceSpec{ - {Model: v1alpha1.LLMModelSpec{URI: mustParseURL("model-a")}}, + {Model: v1alpha1.LLMModelSpec{URI: apis.URL{Path: "model-a"}}}, }, want: v1alpha1.LLMInferenceServiceSpec{ - Model: v1alpha1.LLMModelSpec{URI: mustParseURL("model-a")}, + Model: v1alpha1.LLMModelSpec{URI: apis.URL{Path: "model-a"}}, + }, + wantErr: false, + }, + { + name: "two configs simple merge with sub-field override", + cfgs: []v1alpha1.LLMInferenceServiceSpec{ + { + Model: v1alpha1.LLMModelSpec{URI: apis.URL{Path: "model-a"}}, + Router: &v1alpha1.RouterSpec{ + Route: &v1alpha1.GatewayRoutesSpec{}, + Gateway: &v1alpha1.GatewaySpec{}, + Scheduler: &v1alpha1.SchedulerSpec{}, + }, + }, + { + Model: v1alpha1.LLMModelSpec{URI: apis.URL{Path: "model-a"}}, + Router: &v1alpha1.RouterSpec{ + Scheduler: &v1alpha1.SchedulerSpec{ + Pool: &v1alpha1.InferencePoolSpec{ + Spec: &igwapi.InferencePoolSpec{ + TargetPortNumber: 9999, + }, + }, + }, + }, + }, + }, + want: v1alpha1.LLMInferenceServiceSpec{ + Model: v1alpha1.LLMModelSpec{URI: apis.URL{Path: "model-a"}}, + Router: &v1alpha1.RouterSpec{ + Route: &v1alpha1.GatewayRoutesSpec{}, + Gateway: &v1alpha1.GatewaySpec{}, + Scheduler: &v1alpha1.SchedulerSpec{ + Pool: &v1alpha1.InferencePoolSpec{ + Spec: &igwapi.InferencePoolSpec{ + TargetPortNumber: 9999, + }, + }, + }, + }, }, wantErr: false, }, @@ -67,20 +115,20 @@ func TestMergeSpecs(t *testing.T) { name: "two configs with override", cfgs: []v1alpha1.LLMInferenceServiceSpec{ { - Model: v1alpha1.LLMModelSpec{URI: mustParseURL("model-a")}, + Model: v1alpha1.LLMModelSpec{URI: apis.URL{Path: "model-a"}}, WorkloadSpec: v1alpha1.WorkloadSpec{ Replicas: ptr.To[int32](1), }, }, { - Model: v1alpha1.LLMModelSpec{URI: mustParseURL("model-b")}, + Model: v1alpha1.LLMModelSpec{URI: apis.URL{Path: "model-b"}}, WorkloadSpec: v1alpha1.WorkloadSpec{ Replicas: ptr.To[int32](2), }, }, }, want: v1alpha1.LLMInferenceServiceSpec{ - Model: v1alpha1.LLMModelSpec{URI: mustParseURL("model-b")}, + Model: v1alpha1.LLMModelSpec{URI: apis.URL{Path: "model-b"}}, WorkloadSpec: v1alpha1.WorkloadSpec{ Replicas: ptr.To[int32](2), }, @@ -90,13 +138,13 @@ func TestMergeSpecs(t *testing.T) { { name: "three configs chained merge", cfgs: []v1alpha1.LLMInferenceServiceSpec{ - {Model: v1alpha1.LLMModelSpec{URI: mustParseURL("model-a")}}, + {Model: v1alpha1.LLMModelSpec{URI: apis.URL{Path: "model-a"}}}, { - Model: v1alpha1.LLMModelSpec{URI: mustParseURL("model-b")}, + Model: v1alpha1.LLMModelSpec{URI: apis.URL{Path: "model-b"}}, }, }, want: v1alpha1.LLMInferenceServiceSpec{ - Model: v1alpha1.LLMModelSpec{URI: mustParseURL("model-b")}, + Model: v1alpha1.LLMModelSpec{URI: apis.URL{Path: "model-b"}}, }, wantErr: false, }, @@ -744,10 +792,10 @@ func TestMergeSpecs(t *testing.T) { cfgs: []v1alpha1.LLMInferenceServiceSpec{ { Model: v1alpha1.LLMModelSpec{ - URI: mustParseURL("base-model"), + URI: apis.URL{Path: "base-model"}, LoRA: &v1alpha1.LoRASpec{ - Adapters: []v1alpha1.ModelSpec{ - {StorageURI: "s3://bucket/adapter1", Framework: "pytorch", Memory: resource.MustParse("1Gi")}, + Adapters: []v1alpha1.LLMModelSpec{ + {URI: apis.URL{Path: "lora-model"}}, }, }, }, @@ -755,8 +803,8 @@ func TestMergeSpecs(t *testing.T) { { Model: v1alpha1.LLMModelSpec{ LoRA: &v1alpha1.LoRASpec{ - Adapters: []v1alpha1.ModelSpec{ - {StorageURI: "s3://bucket/adapter2", Framework: "pytorch", Memory: resource.MustParse("512Mi")}, + Adapters: []v1alpha1.LLMModelSpec{ + {URI: apis.URL{Path: "lora-model2"}}, }, }, }, @@ -764,10 +812,10 @@ func TestMergeSpecs(t *testing.T) { }, want: v1alpha1.LLMInferenceServiceSpec{ Model: v1alpha1.LLMModelSpec{ - URI: mustParseURL("base-model"), + URI: apis.URL{Path: "base-model"}, LoRA: &v1alpha1.LoRASpec{ - Adapters: []v1alpha1.ModelSpec{ - {StorageURI: "s3://bucket/adapter2", Framework: "pytorch", Memory: resource.MustParse("512Mi")}, + Adapters: []v1alpha1.LLMModelSpec{ + {URI: apis.URL{Path: "lora-model2"}}, }, }, }, @@ -778,7 +826,7 @@ func TestMergeSpecs(t *testing.T) { cfgs: []v1alpha1.LLMInferenceServiceSpec{ { Model: v1alpha1.LLMModelSpec{ - URI: mustParseURL("model-uri"), + URI: apis.URL{Path: "model-uri"}, Criticality: ptr.To(igwapi.Sheddable), }, }, @@ -790,7 +838,7 @@ func TestMergeSpecs(t *testing.T) { }, want: v1alpha1.LLMInferenceServiceSpec{ Model: v1alpha1.LLMModelSpec{ - URI: mustParseURL("model-uri"), + URI: apis.URL{Path: "model-uri"}, Criticality: ptr.To(igwapi.Critical), }, }, @@ -873,7 +921,7 @@ func TestMergeSpecs(t *testing.T) { cfgs: []v1alpha1.LLMInferenceServiceSpec{ { Model: v1alpha1.LLMModelSpec{ - URI: mustParseURL("model-uri"), + URI: apis.URL{Path: "model-uri"}, Name: ptr.To("base-name"), }, WorkloadSpec: v1alpha1.WorkloadSpec{ @@ -891,7 +939,7 @@ func TestMergeSpecs(t *testing.T) { }, want: v1alpha1.LLMInferenceServiceSpec{ Model: v1alpha1.LLMModelSpec{ - URI: mustParseURL("model-uri"), + URI: apis.URL{Path: "model-uri"}, Name: ptr.To("base-name"), // Base value should be preserved }, WorkloadSpec: v1alpha1.WorkloadSpec{ @@ -904,12 +952,12 @@ func TestMergeSpecs(t *testing.T) { cfgs: []v1alpha1.LLMInferenceServiceSpec{ { Model: v1alpha1.LLMModelSpec{ - URI: mustParseURL("base-model"), + URI: apis.URL{Path: "base-model"}, Name: ptr.To("base-name"), Criticality: ptr.To(igwapi.Sheddable), LoRA: &v1alpha1.LoRASpec{ - Adapters: []v1alpha1.ModelSpec{ - {StorageURI: "base-adapter", Framework: "pytorch", Memory: resource.MustParse("1Gi")}, + Adapters: []v1alpha1.LLMModelSpec{ + {URI: apis.URL{Path: "lora-model"}}, }, }, }, @@ -930,8 +978,8 @@ func TestMergeSpecs(t *testing.T) { Name: ptr.To("override-name"), Criticality: ptr.To(igwapi.Critical), LoRA: &v1alpha1.LoRASpec{ - Adapters: []v1alpha1.ModelSpec{ - {StorageURI: "override-adapter", Framework: "tensorflow", Memory: resource.MustParse("2Gi")}, + Adapters: []v1alpha1.LLMModelSpec{ + {URI: apis.URL{Path: "lora-model2"}}, }, }, }, @@ -952,12 +1000,12 @@ func TestMergeSpecs(t *testing.T) { }, want: v1alpha1.LLMInferenceServiceSpec{ Model: v1alpha1.LLMModelSpec{ - URI: mustParseURL("base-model"), // Base URI preserved - Name: ptr.To("override-name"), // Override name + URI: apis.URL{Path: "base-model"}, // Base URI preserved + Name: ptr.To("override-name"), // Override name Criticality: ptr.To(igwapi.Critical), LoRA: &v1alpha1.LoRASpec{ - Adapters: []v1alpha1.ModelSpec{ - {StorageURI: "override-adapter", Framework: "tensorflow", Memory: resource.MustParse("2Gi")}, + Adapters: []v1alpha1.LLMModelSpec{ + {URI: apis.URL{Path: "lora-model2"}}, }, }, }, @@ -988,13 +1036,13 @@ func TestMergeSpecs(t *testing.T) { }, { Model: v1alpha1.LLMModelSpec{ - URI: mustParseURL("populated-model"), + URI: apis.URL{Path: "populated-model"}, }, }, }, want: v1alpha1.LLMInferenceServiceSpec{ Model: v1alpha1.LLMModelSpec{ - URI: mustParseURL("populated-model"), + URI: apis.URL{Path: "populated-model"}, }, }, }, @@ -1018,10 +1066,256 @@ func TestMergeSpecs(t *testing.T) { }, }, }, + { + name: "merge pod spec with nil containers", + cfgs: []v1alpha1.LLMInferenceServiceSpec{ + { + WorkloadSpec: v1alpha1.WorkloadSpec{ + Template: &corev1.PodSpec{ + Containers: []corev1.Container{ + { + Image: "busybox", + Name: "busybox", + }, + }, + InitContainers: []corev1.Container{ + { + Image: "busybox-init", + Name: "busybox-init", + }, + }, + Volumes: []corev1.Volume{ + { + Name: "vol1", + }, + }, + }, + Replicas: ptr.To[int32](1), + }, + }, + { + WorkloadSpec: v1alpha1.WorkloadSpec{ + Template: &corev1.PodSpec{}, + Replicas: nil, + }, + }, + }, + want: v1alpha1.LLMInferenceServiceSpec{ + WorkloadSpec: v1alpha1.WorkloadSpec{ + Template: &corev1.PodSpec{ + Containers: []corev1.Container{ + { + Image: "busybox", + Name: "busybox", + }, + }, + InitContainers: []corev1.Container{ + { + Image: "busybox-init", + Name: "busybox-init", + }, + }, + Volumes: []corev1.Volume{ + { + Name: "vol1", + }, + }, + }, + Replicas: ptr.To[int32](1), + }, + }, + }, + { + name: "merge pod spec with empty containers", + cfgs: []v1alpha1.LLMInferenceServiceSpec{ + { + WorkloadSpec: v1alpha1.WorkloadSpec{ + Template: &corev1.PodSpec{ + Containers: []corev1.Container{ + { + Image: "busybox", + Name: "busybox", + }, + }, + InitContainers: []corev1.Container{ + { + Image: "busybox-init", + Name: "busybox-init", + }, + }, + Volumes: []corev1.Volume{ + { + Name: "vol1", + }, + }, + }, + Replicas: ptr.To[int32](1), + }, + }, + { + WorkloadSpec: v1alpha1.WorkloadSpec{ + Template: &corev1.PodSpec{ + Containers: []corev1.Container{}, + }, + Replicas: nil, + }, + }, + }, + want: v1alpha1.LLMInferenceServiceSpec{ + WorkloadSpec: v1alpha1.WorkloadSpec{ + Template: &corev1.PodSpec{ + Containers: []corev1.Container{ + { + Image: "busybox", + Name: "busybox", + }, + }, + InitContainers: []corev1.Container{ + { + Image: "busybox-init", + Name: "busybox-init", + }, + }, + Volumes: []corev1.Volume{ + { + Name: "vol1", + }, + }, + }, + Replicas: ptr.To[int32](1), + }, + }, + }, + { + name: "merge pod spec, add container", + cfgs: []v1alpha1.LLMInferenceServiceSpec{ + { + WorkloadSpec: v1alpha1.WorkloadSpec{ + Template: &corev1.PodSpec{ + Containers: []corev1.Container{ + { + Image: "busybox", + Name: "busybox", + }, + }, + InitContainers: []corev1.Container{ + { + Image: "busybox-sidecar", + Name: "busybox-sidecar", + }, + }, + }, + Replicas: ptr.To[int32](2), + }, + }, + { + WorkloadSpec: v1alpha1.WorkloadSpec{ + Template: &corev1.PodSpec{ + Containers: []corev1.Container{ + { + Image: "busybox-2", + Name: "busybox-2", + }, + }, + InitContainers: []corev1.Container{}, + }, + Replicas: nil, + }, + }, + }, + want: v1alpha1.LLMInferenceServiceSpec{ + WorkloadSpec: v1alpha1.WorkloadSpec{ + Template: &corev1.PodSpec{ + Containers: []corev1.Container{ + { + Image: "busybox-2", + Name: "busybox-2", + }, + { + Image: "busybox", + Name: "busybox", + }, + }, + InitContainers: []corev1.Container{ + { + Image: "busybox-sidecar", + Name: "busybox-sidecar", + }, + }, + }, + Replicas: ptr.To[int32](2), + }, + }, + }, + { + name: "merge pod spec, add container", + cfgs: []v1alpha1.LLMInferenceServiceSpec{ + { + WorkloadSpec: v1alpha1.WorkloadSpec{ + Template: &corev1.PodSpec{ + Containers: []corev1.Container{ + { + Image: "busybox", + Name: "busybox", + }, + }, + InitContainers: []corev1.Container{ + { + Image: "busybox-sidecar", + Name: "busybox-sidecar", + }, + }, + }, + Replicas: ptr.To[int32](2), + }, + }, + { + WorkloadSpec: v1alpha1.WorkloadSpec{ + Template: &corev1.PodSpec{ + Containers: []corev1.Container{ + { + Image: "busybox-2", + Name: "busybox-2", + }, + }, + InitContainers: []corev1.Container{}, + }, + Replicas: nil, + }, + }, + {}, + }, + want: v1alpha1.LLMInferenceServiceSpec{ + WorkloadSpec: v1alpha1.WorkloadSpec{ + Template: &corev1.PodSpec{ + Containers: []corev1.Container{ + { + Image: "busybox-2", + Name: "busybox-2", + }, + { + Image: "busybox", + Name: "busybox", + }, + }, + InitContainers: []corev1.Container{ + { + Image: "busybox-sidecar", + Name: "busybox-sidecar", + }, + }, + }, + Replicas: ptr.To[int32](2), + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := llmisvc.MergeSpecs(tt.cfgs...) + ctx := t.Context() + ctx = log.IntoContext(ctx, pkgtest.NewTestLogger(t)) + + got, err := llmisvc.MergeSpecs(ctx, tt.cfgs...) if (err != nil) != tt.wantErr { t.Errorf("MergeSpecs() error = %v, wantErr %v", err, tt.wantErr) return @@ -1422,3 +1716,77 @@ func mustParseURL(s string) apis.URL { } return *u } + +func TestAdditionalData(t *testing.T) { + tests := []struct { + name string + config string + reconcilerConfig llmisvc.Config + wantErr bool + want func(llmSvc *v1alpha1.LLMInferenceServiceConfig, g *GomegaWithT) + }{ + { + name: "additional structs replacements", + config: `apiVersion: serving.kserve.io/v1alpha1 +kind: LLMInferenceServiceConfig +metadata: + name: test-config + namespace: "{{ .GlobalConfig.SystemNamespace }}" +spec: + router: + route: + http: + spec: + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: "{{ .GlobalConfig.IngressGatewayName }}" + namespace: "{{ .GlobalConfig.IngressGatewayNamespace }}"`, + reconcilerConfig: llmisvc.Config{ + SystemNamespace: "my-kserve", + IngressGatewayName: "my-gateway", + IngressGatewayNamespace: "my-ns", + }, + want: func(llmSvc *v1alpha1.LLMInferenceServiceConfig, g *GomegaWithT) { + httpRouteSpec := llmSvc.Spec.Router.Route.HTTP.Spec + expectedGatewayRef := gwapiv1.ParentReference{ + Name: "my-gateway", + Namespace: ptr.To(gwapiv1.Namespace("my-ns")), + } + g.Expect(httpRouteSpec).To(ktesting.HaveGatewayRefs(expectedGatewayRef)) + g.Expect(llmSvc.Namespace).To(Equal("my-kserve")) + }, + }, + { + name: "template with non-existing key should error", + config: `apiVersion: serving.kserve.io/v1alpha1 +kind: LLMInferenceServiceConfig +metadata: + name: "{{ .GlobalConfig.NonExistentConfig.SomeField }}" +spec: + model: + name: "static-model"`, + wantErr: true, + want: func(llmSvc *v1alpha1.LLMInferenceServiceConfig, g *GomegaWithT) {}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + preset := &v1alpha1.LLMInferenceServiceConfig{} + if err := yaml.Unmarshal([]byte(tt.config), preset); err != nil { + t.Errorf("Failed to unmarshal YAML: %v", err) + return + } + + llmSvc := &v1alpha1.LLMInferenceService{} + got, err := llmisvc.ReplaceVariables(llmSvc, preset, &tt.reconcilerConfig) + if (err != nil) != tt.wantErr { + t.Errorf("ReplaceVariables() error = %v, wantErr %v", err, tt.wantErr) + return + } + + tt.want(got, NewGomegaWithT(t)) + }) + } +} diff --git a/pkg/controller/v1alpha1/llmisvc/config_presets_test.go b/pkg/controller/v1alpha1/llmisvc/config_presets_test.go index 5ceb488c38b..89d66738c55 100644 --- a/pkg/controller/v1alpha1/llmisvc/config_presets_test.go +++ b/pkg/controller/v1alpha1/llmisvc/config_presets_test.go @@ -62,7 +62,7 @@ func TestPresetFiles(t *testing.T) { }, Spec: v1alpha1.LLMInferenceServiceSpec{ WorkloadSpec: v1alpha1.WorkloadSpec{ - Worker: &corev1.PodSpec{ + Template: &corev1.PodSpec{ Volumes: []corev1.Volume{ { Name: "home", @@ -90,7 +90,6 @@ func TestPresetFiles(t *testing.T) { VolumeSource: corev1.VolumeSource{Secret: &corev1.SecretVolumeSource{SecretName: "test-llm-preset-kserve-self-signed-certs"}}, }, }, - TerminationGracePeriodSeconds: ptr.To(int64(30)), InitContainers: []corev1.Container{ { Name: "llm-d-routing-sidecar", @@ -98,6 +97,7 @@ func TestPresetFiles(t *testing.T) { Args: []string{ "--port=8000", "--vllm-port=8001", + "--connector=nixlv2", "--secure-proxy=true", "--cert-path=/etc/ssl/certs", "--decoder-use-tls=true", @@ -122,6 +122,17 @@ func TestPresetFiles(t *testing.T) { Protocol: corev1.ProtocolTCP, }, }, + SecurityContext: &corev1.SecurityContext{ + AllowPrivilegeEscalation: ptr.To(false), + RunAsNonRoot: ptr.To(true), + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + ReadOnlyRootFilesystem: ptr.To(true), + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeRuntimeDefault, + }, + }, RestartPolicy: ptr.To(corev1.ContainerRestartPolicyAlways), TerminationMessagePath: "/dev/termination-log", TerminationMessagePolicy: "FallbackToLogsOnError", @@ -164,14 +175,29 @@ func TestPresetFiles(t *testing.T) { Containers: []corev1.Container{ { Name: "main", - Image: "ghcr.io/llm-d/llm-d:v0.2.0", + Image: "ghcr.io/llm-d/llm-d-dev:v0.2.2", Command: []string{"/bin/sh", "-c"}, + Args: []string{"START_RANK=0\neval \"vllm serve \\\n /mnt/models \\\n --served-model-name \"llama\" \\\n --port 8001 \\\n --api-server-count ${VLLM_API_SERVER_COUNT:-8} \\\n --disable-log-requests \\\n--enable-expert-parallel \\\n--tensor-parallel-size 1 \\\n --data-parallel-size $(( 2 * 4 )) \\\n --data-parallel-size-local 2 \\\n --data-parallel-address $(LWS_LEADER_ADDRESS) \\\n --data-parallel-rpc-port 5555 \\\n --data-parallel-start-rank $START_RANK \\\n --data-parallel-hybrid-lb \\\n ${VLLM_ADDITIONAL_ARGS} \\\n --trust-remote-code \\\n --enable-ssl-refresh \\\n --ssl-certfile \\\n /etc/ssl/certs/tls.crt \\\n --ssl-keyfile \\\n /etc/ssl/certs/tls.key\""}, Ports: []corev1.ContainerPort{ { ContainerPort: 8001, Protocol: corev1.ProtocolTCP, }, }, + Env: []corev1.EnvVar{ + { + Name: "HOME", + Value: "/home", + }, + { + Name: "VLLM_LOGGING_LEVEL", + Value: "INFO", + }, + { + Name: "HF_HUB_CACHE", + Value: "/models", + }, + }, VolumeMounts: []corev1.VolumeMount{ { Name: "home", @@ -199,9 +225,9 @@ func TestPresetFiles(t *testing.T) { Scheme: corev1.URISchemeHTTPS, }, }, - InitialDelaySeconds: 120, - PeriodSeconds: 10, + InitialDelaySeconds: 300, TimeoutSeconds: 10, + PeriodSeconds: 10, FailureThreshold: 3, }, ReadinessProbe: &corev1.Probe{ @@ -212,18 +238,106 @@ func TestPresetFiles(t *testing.T) { Scheme: corev1.URISchemeHTTPS, }, }, - InitialDelaySeconds: 10, - PeriodSeconds: 10, + InitialDelaySeconds: 200, TimeoutSeconds: 5, + PeriodSeconds: 30, FailureThreshold: 60, }, + TerminationMessagePath: "/dev/termination-log", + TerminationMessagePolicy: "FallbackToLogsOnError", + ImagePullPolicy: "IfNotPresent", SecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{ + "IPC_LOCK", + "SYS_RAWIO", + }, + Drop: []corev1.Capability{"ALL"}, + }, AllowPrivilegeEscalation: ptr.To(false), + RunAsNonRoot: ptr.To(true), + ReadOnlyRootFilesystem: ptr.To(false), + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeRuntimeDefault, + }, + }, + }, + }, + TerminationGracePeriodSeconds: ptr.To(int64(30)), + }, + Worker: &corev1.PodSpec{ + Volumes: []corev1.Volume{ + { + Name: "home", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + { + Name: "dshm", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{ + Medium: corev1.StorageMediumMemory, + SizeLimit: ptr.To(resource.MustParse("1Gi")), + }, + }, + }, + { + Name: "model-cache", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + { + Name: "tls-certs", + VolumeSource: corev1.VolumeSource{Secret: &corev1.SecretVolumeSource{SecretName: "test-llm-preset-kserve-self-signed-certs"}}, + }, + }, + TerminationGracePeriodSeconds: ptr.To(int64(30)), + Containers: []corev1.Container{ + { + Name: "main", + Image: "ghcr.io/llm-d/llm-d-dev:v0.2.2", + Command: []string{"/bin/sh", "-c"}, + Args: []string{"START_RANK=$(( ${LWS_WORKER_INDEX:-0} * 2 ))\neval \"vllm serve \\\n /mnt/models \\\n --served-model-name \"llama\" \\\n --port 8001 \\\n --disable-log-requests \\\n--enable-expert-parallel \\\n--tensor-parallel-size 1 \\\n --data-parallel-size $(( 2 * 4 )) \\\n --data-parallel-size-local 2 \\\n --data-parallel-address $(LWS_LEADER_ADDRESS) \\\n --data-parallel-rpc-port 5555 \\\n --data-parallel-start-rank $START_RANK \\\n --data-parallel-hybrid-lb \\\n ${VLLM_ADDITIONAL_ARGS} \\\n --trust-remote-code \\\n --headless \\\n --enable-ssl-refresh \\\n --ssl-certfile \\\n /etc/ssl/certs/tls.crt \\\n --ssl-keyfile \\\n /etc/ssl/certs/tls.key\""}, + Ports: []corev1.ContainerPort{ + { + ContainerPort: 8001, + Protocol: corev1.ProtocolTCP, + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "home", + MountPath: "/home", + }, + { + Name: "dshm", + MountPath: "/dev/shm", + }, + { + Name: "model-cache", + MountPath: "/models", + }, + { + Name: "tls-certs", + ReadOnly: true, + MountPath: "/etc/ssl/certs", + }, + }, + SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Add: []corev1.Capability{ "IPC_LOCK", "SYS_RAWIO", }, + Drop: []corev1.Capability{"ALL"}, + }, + AllowPrivilegeEscalation: ptr.To(false), + RunAsNonRoot: ptr.To(true), + ReadOnlyRootFilesystem: ptr.To(false), + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeRuntimeDefault, }, }, Env: []corev1.EnvVar{ @@ -239,59 +353,14 @@ func TestPresetFiles(t *testing.T) { Name: "HF_HUB_CACHE", Value: "/models", }, + { + Name: "VLLM_RANDOMIZE_DP_DUMMY_INPUTS", + Value: "1", + }, }, TerminationMessagePath: "/dev/termination-log", TerminationMessagePolicy: "FallbackToLogsOnError", ImagePullPolicy: "IfNotPresent", - Stdin: true, - TTY: true, - Args: []string{` -START_RANK=$(( ${LWS_WORKER_INDEX:-0} * 2 )) -if [ "${LWS_WORKER_INDEX:-0}" -eq 0 ]; then - ################# - # Leader-only launch - ################# - vllm serve \ - llama \ - --port 8001 \ - --api-server-count 4 \ - --disable-log-requests \ ---enable-expert-parallel \ ---tensor-parallel-size 1 \ - --data-parallel-size 4 \ - --data-parallel-size-local 2 \ - --data-parallel-address $(LWS_LEADER_ADDRESS) \ - --data-parallel-rpc-port 5555 \ - --data-parallel-start-rank $START_RANK \ - --trust-remote-code \ - --enable-ssl-refresh \ - --ssl-certfile \ - /etc/ssl/certs/tls.crt \ - --ssl-keyfile \ - /etc/ssl/certs/tls.key -else - ################# - # Worker-only launch - ################# - vllm serve \ - llama \ - --port 8001 \ - --disable-log-requests \ ---enable-expert-parallel \ ---tensor-parallel-size 1 \ - --data-parallel-size 4 \ - --data-parallel-size-local 2 \ - --data-parallel-address $(LWS_LEADER_ADDRESS) \ - --data-parallel-rpc-port 5555 \ - --data-parallel-start-rank $START_RANK \ - --trust-remote-code \ - --headless \ - --enable-ssl-refresh \ - --ssl-certfile \ - /etc/ssl/certs/tls.crt \ - --ssl-keyfile \ - /etc/ssl/certs/tls.key -fi`}, }, }, }, @@ -338,7 +407,7 @@ fi`}, // Verify the actual Spec rendered if provided for the found file. if tc, exist := tt[filename]; exist { - if !equality.Semantic.DeepEqual(tc.expected, out) { + if !equality.Semantic.DeepDerivative(tc.expected, out) { diff := cmp.Diff(tc.expected, out) t.Errorf("ReplaceVariables() returned unexpected diff (-want +got):\n%s", diff) } diff --git a/pkg/testing/httproute_matchers.go b/pkg/testing/httproute_matchers.go new file mode 100644 index 00000000000..24a8ee8431f --- /dev/null +++ b/pkg/testing/httproute_matchers.go @@ -0,0 +1,157 @@ +/* +Copyright 2025 The KServe Authors. + +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. +*/ + +package testing + +import ( + "encoding/json" + "errors" + "fmt" + + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/utils/ptr" + + "github.com/onsi/gomega/types" + gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +// extractHTTPRoute safely extracts HTTPRoute from either pointer or value type +func extractHTTPRoute(actual any) (*gwapiv1.HTTPRoute, error) { + switch v := actual.(type) { + case gwapiv1.HTTPRouteSpec: + return &gwapiv1.HTTPRoute{Spec: v}, nil + case *gwapiv1.HTTPRouteSpec: + if v == nil { + return nil, errors.New("expected non-nil gatewayapi.HTTPRouteSpec, but got nil") + } + return &gwapiv1.HTTPRoute{Spec: *v}, nil + case *gwapiv1.HTTPRoute: + if v == nil { + return nil, errors.New("expected non-nil gatewayapi.HTTPRoute, but got nil") + } + return v, nil + case gwapiv1.HTTPRoute: + return &v, nil + default: + return nil, fmt.Errorf("expected gatewayapi.HTTPRoute gatewayapi.HTTPRouteSpec, but got %T", actual) + } +} + +// HaveGatewayRefs returns a matcher that checks if an HTTPRoute has the specified gateway parent refs +func HaveGatewayRefs(expectedGateways ...gwapiv1.ParentReference) types.GomegaMatcher { + return &haveGatewayRefsMatcher{ + expectedGatewayNames: expectedGateways, + } +} + +type haveGatewayRefsMatcher struct { + expectedGatewayNames []gwapiv1.ParentReference + actualParentRefs []gwapiv1.ParentReference + actualGatewayNames []string +} + +func (matcher *haveGatewayRefsMatcher) Match(actual any) (success bool, err error) { + httpRoute, err := extractHTTPRoute(actual) + if err != nil { + return false, err + } + + matcher.actualParentRefs = httpRoute.Spec.ParentRefs + + expectedSet := make(map[string]gwapiv1.ParentReference) + for _, ref := range matcher.expectedGatewayNames { + expectedSet[string(ref.Name)] = ref + } + + for _, ref := range matcher.actualParentRefs { + expectedRef, found := expectedSet[string(ref.Name)] + if !found { + return false, nil + } + + if expectedRef.Namespace != nil { + return ptr.Deref(expectedRef.Namespace, "") == ptr.Deref(ref.Namespace, ""), nil + } + } + + return true, nil +} + +func (matcher *haveGatewayRefsMatcher) FailureMessage(actual any) string { + return fmt.Sprintf("Expected %T to have gateway refs %v, but found %v", + actual, matcher.expectedGatewayNames, matcher.actualGatewayNames) +} + +func (matcher *haveGatewayRefsMatcher) NegatedFailureMessage(actual any) string { + return fmt.Sprintf("Expected %T to not have gateway refs %v, but they were found", + actual, matcher.expectedGatewayNames) +} + +// HaveBackendRefs returns a matcher that checks if an HTTPRoute has the specified backend refs. +func HaveBackendRefs(backends ...gwapiv1.HTTPBackendRef) types.GomegaMatcher { + return &haveBackendRefsMatcher{ + expectedBackendRefs: backends, + } +} + +type haveBackendRefsMatcher struct { + expectedBackendRefs []gwapiv1.HTTPBackendRef + actualBackendRefs []gwapiv1.HTTPBackendRef +} + +func (matcher *haveBackendRefsMatcher) Match(actual any) (success bool, err error) { + httpRoute, err := extractHTTPRoute(actual) + if err != nil { + return false, err + } + + for _, rule := range httpRoute.Spec.Rules { + matcher.actualBackendRefs = append(matcher.actualBackendRefs, rule.BackendRefs...) + } + + if len(matcher.actualBackendRefs) != len(matcher.expectedBackendRefs) { + return false, nil + } + + for _, want := range matcher.expectedBackendRefs { + found := false + for _, got := range matcher.actualBackendRefs { + if equality.Semantic.DeepEqual(want, got) { + found = true + break + } + } + if !found { + return false, nil + } + } + + return true, nil +} + +func (matcher *haveBackendRefsMatcher) FailureMessage(actual any) string { + expected, _ := json.MarshalIndent(matcher.expectedBackendRefs, "", " ") + got, _ := json.MarshalIndent(matcher.actualBackendRefs, "", " ") + return fmt.Sprintf("Expected %T to have backend refs:\n%s\nbut found:\n%s", + actual, expected, got) +} + +func (matcher *haveBackendRefsMatcher) NegatedFailureMessage(actual any) string { + expected, _ := json.MarshalIndent(matcher.expectedBackendRefs, "", " ") + got, _ := json.MarshalIndent(matcher.actualBackendRefs, "", " ") + return fmt.Sprintf("Expected %T to not have backend refs:\n%s, got:\n%s", + actual, expected, got) +} diff --git a/pkg/testing/log.go b/pkg/testing/log.go new file mode 100644 index 00000000000..75a9035c1a6 --- /dev/null +++ b/pkg/testing/log.go @@ -0,0 +1,135 @@ +/* +Copyright 2025 The KServe Authors. + +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. +*/ + +package testing + +import ( + "fmt" + "slices" + "strings" + + "github.com/go-logr/logr" +) + +// testingT is a subset of the testing.T interface. +// This is used to allow for easy testing of the logger itself. +type testingT interface { + Log(args ...interface{}) + Helper() +} + +// NewTestLogger returns a logr.Logger that prints to the given testing.T. +// This is useful for logging in tests, as the output will be buffered and +// printed only if the test fails. +func NewTestLogger(t testingT) logr.Logger { + return logr.New(&testingLogSink{t: t}) +} + +// testingLogSink is a logr.LogSink that prints to a testing.T. +type testingLogSink struct { + t testingT + name string + values []interface{} +} + +// ensure testingLogSink implements logr.LogSink +var _ logr.LogSink = &testingLogSink{} + +// Init is called by logr.New to initialize the sink. +// We don't need any special initialization, so this is a no-op. +func (l *testingLogSink) Init(info logr.RuntimeInfo) {} + +// Enabled returns true if the log level is enabled. +// For testing, we generally want to see all logs, so this always returns true. +func (l *testingLogSink) Enabled(level int) bool { + return true +} + +// Info logs a non-error message with the given key/value pairs. +func (l *testingLogSink) Info(level int, msg string, keysAndValues ...interface{}) { + l.t.Helper() + args := l.formatArgs(msg, keysAndValues) + l.t.Log(args...) +} + +// Error logs an error message with the given key/value pairs. +func (l *testingLogSink) Error(err error, msg string, keysAndValues ...interface{}) { + l.t.Helper() + args := l.formatArgs(msg, keysAndValues) + // Prepend the error to the log arguments + allArgs := append([]interface{}{"ERROR", err}, args...) + l.t.Log(allArgs...) +} + +// WithValues returns a new LogSink with additional key/value pairs. +func (l *testingLogSink) WithValues(keysAndValues ...interface{}) logr.LogSink { + newSink := l.clone() + newSink.values = append(newSink.values, keysAndValues...) + return newSink +} + +// WithName returns a new LogSink with an additional name component. +func (l *testingLogSink) WithName(name string) logr.LogSink { + newSink := l.clone() + if len(l.name) > 0 { + newSink.name = l.name + "." + name + } else { + newSink.name = name + } + return newSink +} + +// formatArgs formats the log message and key/value pairs into a slice of interfaces for t.Log. +func (l *testingLogSink) formatArgs(msg string, keysAndValues []interface{}) []interface{} { + l.t.Helper() + + // Start with the logger name if it exists + var namePart string + if l.name != "" { + namePart = "[" + l.name + "] " + } + + // Combine the static values from WithValues with the call-site values + allValues := slices.Clone(l.values) + allValues = append(allValues, keysAndValues...) + + // Format the key-value pairs + var kvParts []string + for i := 0; i < len(allValues); i += 2 { + key := allValues[i] + var val interface{} = "(no-value)" + if i+1 < len(allValues) { + val = allValues[i+1] + } + kvParts = append(kvParts, fmt.Sprintf("%s=%+v", key, val)) + } + + // Combine all parts into a single log line + return []interface{}{ + fmt.Sprintf("%s%s (%s)", namePart, msg, strings.Join(kvParts, " ")), + } +} + +// clone creates a deep copy of the sink. +func (l *testingLogSink) clone() *testingLogSink { + newSink := &testingLogSink{ + t: l.t, + name: l.name, + values: make([]interface{}, len(l.values)), + } + copy(newSink.values, l.values) + return newSink +} diff --git a/test/crds/serving.kserve.io_all_crds.yaml b/test/crds/serving.kserve.io_all_crds.yaml index eaf370dd63b..4dc154eb853 100644 --- a/test/crds/serving.kserve.io_all_crds.yaml +++ b/test/crds/serving.kserve.io_all_crds.yaml @@ -23264,24 +23264,7 @@ spec: lora: properties: adapters: - items: - properties: - framework: - type: string - memory: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - storageUri: - type: string - required: - - framework - - memory - - storageUri - type: object - type: array + x-kubernetes-preserve-unknown-fields: true type: object name: type: string @@ -42761,24 +42744,7 @@ spec: lora: properties: adapters: - items: - properties: - framework: - type: string - memory: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - storageUri: - type: string - required: - - framework - - memory - - storageUri - type: object - type: array + x-kubernetes-preserve-unknown-fields: true type: object name: type: string From 72a9b3af0055d7c7c802262c0ebaffa3adc98772 Mon Sep 17 00:00:00 2001 From: Pierangelo Di Pilato Date: Thu, 28 Aug 2025 22:45:04 +0200 Subject: [PATCH 2/7] [llmisvc] Support cluster-scoped objects in generic CRUD functions (#4664) Signed-off-by: Pierangelo Di Pilato --- .../v1alpha1/llmisvc/lifecycle_crud.go | 38 ++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/pkg/controller/v1alpha1/llmisvc/lifecycle_crud.go b/pkg/controller/v1alpha1/llmisvc/lifecycle_crud.go index 76bac411ca8..221c7328b76 100644 --- a/pkg/controller/v1alpha1/llmisvc/lifecycle_crud.go +++ b/pkg/controller/v1alpha1/llmisvc/lifecycle_crud.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "reflect" + "strings" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -41,17 +42,25 @@ func Create[O client.Object, T client.Object](ctx context.Context, c clientWithR return fmt.Errorf("failed to create %s %s/%s: %w", logLineForObject(expected), expected.GetNamespace(), expected.GetName(), err) } - c.Eventf(owner, corev1.EventTypeNormal, "Created", "Created %s %s/%s", logLineForObject(expected), expected.GetNamespace(), expected.GetName()) + if !reflect.ValueOf(owner).IsNil() { + c.Eventf(owner, corev1.EventTypeNormal, "Created", "Created %s %s/%s", logLineForObject(expected), expected.GetNamespace(), expected.GetName()) + } + return nil } func Delete[O client.Object, T client.Object](ctx context.Context, c clientWithRecorder, owner O, expected T) error { typeLogLine := logLineForObject(expected) - ownerLogLine := logLineForObject(owner) + isOwnerNil := reflect.ValueOf(owner).IsNil() - if isNamespaced, err := apiutil.IsObjectNamespaced(expected, c.Scheme(), c.RESTMapper()); err != nil { + ownerLogLine := "" + if !isOwnerNil { + ownerLogLine = logLineForObject(owner) + } + + if isNamespaced, err := apiutil.IsObjectNamespaced(expected, c.Scheme(), c.RESTMapper()); err != nil && !meta.IsNoMatchError(err) { return fmt.Errorf("failed to resolve if resource is namespaced %s: %w", typeLogLine, err) - } else if isNamespaced { + } else if isNamespaced && !isOwnerNil { if !metav1.IsControlledBy(expected, owner) { return fmt.Errorf("failed to delete %s %s/%s: it is not controlled by %s %s/%s", typeLogLine, @@ -78,7 +87,10 @@ func Delete[O client.Object, T client.Object](ctx context.Context, c clientWithR return nil } - c.Eventf(owner, corev1.EventTypeNormal, "Deleted", "Deleted %s %s/%s", typeLogLine, expected.GetNamespace(), expected.GetName()) + if !isOwnerNil { + c.Eventf(owner, corev1.EventTypeNormal, "Deleted", "Deleted %s %s/%s", typeLogLine, expected.GetNamespace(), expected.GetName()) + } + return nil } @@ -97,11 +109,16 @@ func Reconcile[O client.Object, T client.Object](ctx context.Context, c clientWi func Update[O client.Object, T client.Object](ctx context.Context, c clientWithRecorder, owner O, curr, expected T, isEqual SemanticEqual[T]) error { typeLogLine := logLineForObject(expected) - ownerLogLine := logLineForObject(owner) + isOwnerNil := reflect.ValueOf(owner).IsNil() + + ownerLogLine := "" + if !isOwnerNil { + ownerLogLine = logLineForObject(owner) + } if isNamespaced, err := apiutil.IsObjectNamespaced(expected, c.Scheme(), c.RESTMapper()); err != nil { return fmt.Errorf("failed to resolve if resource is namespaced %s: %w", typeLogLine, err) - } else if isNamespaced { + } else if isNamespaced && !isOwnerNil { if !metav1.IsControlledBy(curr, owner) { return fmt.Errorf("failed to update %s %s/%s: it is not controlled by %s %s/%s", typeLogLine, @@ -129,14 +146,17 @@ func Update[O client.Object, T client.Object](ctx context.Context, c clientWithR if err := c.Update(ctx, expected); err != nil { return fmt.Errorf("failed to update %s %s/%s: %w", typeLogLine, expected.GetNamespace(), expected.GetName(), err) } - c.Eventf(owner, corev1.EventTypeNormal, "Updated", "Updated %s %s/%s", typeLogLine, expected.GetNamespace(), expected.GetName()) + + if !isOwnerNil { + c.Eventf(owner, corev1.EventTypeNormal, "Updated", "Updated %s %s/%s", typeLogLine, expected.GetNamespace(), expected.GetName()) + } return nil } func logLineForObject(obj client.Object) string { // Note: don't use `obj.GetObjectKind()` as it's always empty. - return reflect.TypeOf(obj).String() + return strings.Replace(reflect.TypeOf(obj).String(), "*", "", 1) } type SemanticEqual[T client.Object] func(expected T, curr T) bool From b64bfef60682137194b5b85e2e055021c0337241 Mon Sep 17 00:00:00 2001 From: Sivanantham <90966311+sivanantha321@users.noreply.github.com> Date: Fri, 29 Aug 2025 08:31:18 +0530 Subject: [PATCH 3/7] fix: fix snyk scan sarif file upload (#4660) Signed-off-by: Sivanantham Chinnaiyan --- .github/workflows/scheduled-image-scan.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/scheduled-image-scan.yml b/.github/workflows/scheduled-image-scan.yml index 83385d4a5ce..51a0d68d49e 100644 --- a/.github/workflows/scheduled-image-scan.yml +++ b/.github/workflows/scheduled-image-scan.yml @@ -14,6 +14,7 @@ jobs: name: scan images runs-on: ubuntu-latest strategy: + fail-fast: false matrix: image: [ @@ -53,15 +54,16 @@ jobs: - name: Upload sarif file to Github Code Scanning if: always() - continue-on-error: true #avoid fail the pipeline if the SARIF upload fails. uses: github/codeql-action/upload-sarif@v3 with: sarif_file: application/${{ matrix.image.name }}/docker.snyk.sarif + category: ${{ matrix.image.name }} predictor-image-scan: name: scan predictor images runs-on: ubuntu-latest strategy: + fail-fast: false matrix: image: [ @@ -104,11 +106,13 @@ jobs: uses: github/codeql-action/upload-sarif@v3 with: sarif_file: application/${{ matrix.image.name }}/docker.snyk.sarif + category: ${{ matrix.image.name }} explainer-image-scan: name: scan explainer images runs-on: ubuntu-latest strategy: + fail-fast: false matrix: image: [{ name: art-explainer, file: python/artexplainer.Dockerfile }] @@ -143,3 +147,4 @@ jobs: uses: github/codeql-action/upload-sarif@v3 with: sarif_file: application/${{ matrix.image.name }}/docker.snyk.sarif + category: ${{ matrix.image.name }} From b9c8255fab668e162bdbfea5c8e950c25481cecb Mon Sep 17 00:00:00 2001 From: Bartosz Majsak Date: Fri, 29 Aug 2025 05:57:53 +0200 Subject: [PATCH 4/7] fix: defaults GITHUB_SHA for graph images (#4620) Signed-off-by: Bartosz Majsak Co-authored-by: Jooho Lee --- test/e2e/graph/test_inference_graph.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/test/e2e/graph/test_inference_graph.py b/test/e2e/graph/test_inference_graph.py index c9830499fe3..c5ea16d0811 100644 --- a/test/e2e/graph/test_inference_graph.py +++ b/test/e2e/graph/test_inference_graph.py @@ -25,14 +25,11 @@ from ..common.utils import KSERVE_TEST_NAMESPACE, predict_ig -if os.environ.get("SUCCESS_200_ISVC_IMAGE") is not None: - SUCCESS_ISVC_IMAGE = os.environ.get("SUCCESS_200_ISVC_IMAGE") -else: - SUCCESS_ISVC_IMAGE = "kserve/success-200-isvc:" + os.environ.get("GITHUB_SHA") -if os.environ.get("ERROR_404_ISVC_IMAGE") is not None: - ERROR_ISVC_IMAGE = os.environ.get("ERROR_404_ISVC_IMAGE") -else: - ERROR_ISVC_IMAGE = "kserve/error-404-isvc:" + os.environ.get("GITHUB_SHA") +img_version = os.environ.get("GITHUB_SHA", "latest") + +SUCCESS_ISVC_IMAGE = f"kserve/success-200-isvc:{img_version}" +ERROR_ISVC_IMAGE = f"kserve/error-404-isvc:{img_version}" + IG_TEST_RESOURCES_BASE_LOCATION = "graph/test-resources" From 76ee9f4eda65c71969e63e6bf9d1970bac8dd1a8 Mon Sep 17 00:00:00 2001 From: Filippe Spolti Date: Sat, 30 Aug 2025 18:22:28 -0300 Subject: [PATCH 5/7] Promote new KServe Storage module (#4625) Signed-off-by: Spolti --- .flake8 | 1 + .github/workflows/e2e-test.yml | 12 +- .github/workflows/python-test.yml | 5 + .../alibiexplainer/alibiexplainer/__main__.py | 2 +- .../tests/test_anchor_images.py | 2 +- .../tests/test_anchor_tabular.py | 2 +- python/aiffairness/uv.lock | 8 +- python/artexplainer/uv.lock | 8 +- python/custom_model/uv.lock | 8 +- python/custom_tokenizer/uv.lock | 8 +- python/custom_transformer/uv.lock | 8 +- python/huggingface_server.Dockerfile | 6 + python/huggingface_server_cpu.Dockerfile | 8 + .../huggingfaceserver/__main__.py | 2 +- python/huggingfaceserver/pyproject.toml | 3 +- python/huggingfaceserver/uv.lock | 48 +- python/kserve/Makefile | 6 +- python/kserve/kserve/storage/__init__.py | 17 - python/kserve/kserve/storage/storage.py | 798 ------------------ .../kserve/storage/test/test_azure_storage.py | 387 --------- .../kserve/storage/test/test_gcs_storage.py | 158 ---- .../kserve/storage/test/test_hf_storage.py | 61 -- .../kserve/storage/test/test_s3_storage.py | 607 ------------- .../kserve/storage/test/test_storage.py | 292 ------- python/kserve/pyproject.toml | 20 +- python/kserve/uv.lock | 34 +- python/lgb.Dockerfile | 8 + python/lgbserver/lgbserver/model.py | 2 +- python/lgbserver/pyproject.toml | 3 +- python/lgbserver/uv.lock | 50 +- python/paddle.Dockerfile | 8 + python/paddleserver/paddleserver/model.py | 2 +- python/paddleserver/pyproject.toml | 3 +- python/paddleserver/uv.lock | 50 +- python/pmml.Dockerfile | 8 + python/pmmlserver/pmmlserver/model.py | 2 +- python/pmmlserver/pyproject.toml | 3 +- python/pmmlserver/uv.lock | 50 +- python/sklearn.Dockerfile | 8 + python/sklearnserver/pyproject.toml | 3 +- python/sklearnserver/sklearnserver/model.py | 2 +- python/sklearnserver/uv.lock | 50 +- python/storage-initializer.Dockerfile | 10 +- .../scripts/initializer-entrypoint | 4 +- .../storage/kserve_storage/kserve_storage.py | 50 +- python/storage/pyproject.toml | 5 +- python/storage/test/test_gcs_storage.py | 14 +- python/storage/uv.lock | 636 +++++++------- .../graph/error_404_isvc/uv.lock | 8 +- .../graph/success_200_isvc/uv.lock | 8 +- python/xgb.Dockerfile | 8 + python/xgbserver/pyproject.toml | 3 +- python/xgbserver/uv.lock | 50 +- python/xgbserver/xgbserver/model.py | 2 +- test/e2e/explainer/test_art_explainer.py | 4 +- 55 files changed, 683 insertions(+), 2882 deletions(-) delete mode 100644 python/kserve/kserve/storage/__init__.py delete mode 100644 python/kserve/kserve/storage/storage.py delete mode 100644 python/kserve/kserve/storage/test/test_azure_storage.py delete mode 100644 python/kserve/kserve/storage/test/test_gcs_storage.py delete mode 100644 python/kserve/kserve/storage/test/test_hf_storage.py delete mode 100644 python/kserve/kserve/storage/test/test_s3_storage.py delete mode 100644 python/kserve/kserve/storage/test/test_storage.py diff --git a/.flake8 b/.flake8 index 92520f6d4ff..d7fd175b936 100644 --- a/.flake8 +++ b/.flake8 @@ -15,6 +15,7 @@ exclude = python/kserve/test/__init__.py, python/kserve/test/test_knative*.py, python/kserve/kserve/protocol/grpc/grpc_predict_v2*.py + python/kserve/build/**/*.py python/*_pb2.py docs/**/*.py python/kserve/kserve/protocol/rest/openai/types/openapi.py diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 4ee74697d8f..dd3b0767437 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -415,7 +415,12 @@ jobs: - name: Run E2E tests timeout-minutes: 30 run: | - ./test/scripts/gh-actions/run-e2e-tests.sh "transformer or mms or collocation or explainer" "6" + ./test/scripts/gh-actions/run-e2e-tests.sh "transformer or mms or collocation" "6" + + - name: Run E2E tests - explainer + timeout-minutes: 30 + run: | + ./test/scripts/gh-actions/run-e2e-tests.sh "explainer" "1" - name: Check system status if: always() @@ -583,6 +588,11 @@ jobs: run: | ./test/scripts/gh-actions/run-e2e-tests.sh "path_based_routing" "6" + - name: Run E2E tests with path-based routing - Explainer + timeout-minutes: 30 + run: | + ./test/scripts/gh-actions/run-e2e-tests.sh "explainer" "1" + - name: Check system status if: always() run: | diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index faaa6549204..601a68065d3 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -74,6 +74,11 @@ jobs: cd python source kserve/.venv/bin/activate pytest --cov=kserve ./kserve + - name: Test kserve Storage + run: | + cd python + source kserve/.venv/bin/activate + pytest --cov=storage ./storage # ----------------------------------------Kserve Numpy 1.x Unit Tests-------------------------------------------- - name: Setup kserve numpy 1-x directory diff --git a/docs/samples/explanation/alibi/alibiexplainer/alibiexplainer/__main__.py b/docs/samples/explanation/alibi/alibiexplainer/alibiexplainer/__main__.py index 65b1c72631e..f4a6ecf7ae3 100644 --- a/docs/samples/explanation/alibi/alibiexplainer/alibiexplainer/__main__.py +++ b/docs/samples/explanation/alibi/alibiexplainer/alibiexplainer/__main__.py @@ -22,8 +22,8 @@ import kserve from kserve import logging -from kserve.storage import Storage from kserve.logging import logger +from kserve_storage import Storage EXPLAINER_FILENAME = "explainer.dill" diff --git a/docs/samples/explanation/alibi/alibiexplainer/tests/test_anchor_images.py b/docs/samples/explanation/alibi/alibiexplainer/tests/test_anchor_images.py index 9bcad499356..2cfaae1962a 100644 --- a/docs/samples/explanation/alibi/alibiexplainer/tests/test_anchor_images.py +++ b/docs/samples/explanation/alibi/alibiexplainer/tests/test_anchor_images.py @@ -18,7 +18,7 @@ import json import numpy as np import dill -from kserve.storage import Storage +from kserve_storage import Storage CIFAR10_EXPLAINER_URI = ( diff --git a/docs/samples/explanation/alibi/alibiexplainer/tests/test_anchor_tabular.py b/docs/samples/explanation/alibi/alibiexplainer/tests/test_anchor_tabular.py index f8224d5cd4c..93dd15bf6bd 100644 --- a/docs/samples/explanation/alibi/alibiexplainer/tests/test_anchor_tabular.py +++ b/docs/samples/explanation/alibi/alibiexplainer/tests/test_anchor_tabular.py @@ -20,7 +20,7 @@ import numpy as np import json from .utils import Predictor -from kserve.storage import Storage +from kserve_storage import Storage ADULT_EXPLAINER_URI = "gs://kfserving-examples/models/sklearn/1.3/income/explainer" ADULT_MODEL_URI = "gs://kfserving-examples/models/sklearn/1.3/income/model" diff --git a/python/aiffairness/uv.lock b/python/aiffairness/uv.lock index fe5226dfdac..85d59598cd3 100644 --- a/python/aiffairness/uv.lock +++ b/python/aiffairness/uv.lock @@ -921,18 +921,13 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "asgi-logger", marker = "extra == 'logging'", specifier = ">=0.1.0,<1.0.0" }, - { name = "azure-identity", marker = "extra == 'storage'", specifier = ">=1.15.0,<2.0.0" }, - { name = "azure-storage-blob", marker = "extra == 'storage'", specifier = ">=12.20.0,<13.0.0" }, - { name = "azure-storage-file-share", marker = "extra == 'storage'", specifier = ">=12.16.0,<13.0.0" }, - { name = "boto3", marker = "extra == 'storage'", specifier = ">=1.29.0,<2.0.0" }, { name = "cloudevents", specifier = ">=1.6.2,<2.0.0" }, { name = "fastapi", specifier = ">=0.115.3" }, - { name = "google-cloud-storage", marker = "extra == 'storage'", specifier = ">=2.14.0,<3.0.0" }, { name = "grpc-interceptor", specifier = ">=0.15.4,<1.0.0" }, { name = "grpcio", specifier = ">=1.64.1,<2.0.0" }, { name = "grpcio-tools", specifier = ">=1.64.1,<2.0.0" }, { name = "httpx", specifier = ">=0.27.2,<1.0.0" }, - { name = "huggingface-hub", extras = ["hf-transfer"], marker = "extra == 'storage'", specifier = ">=0.32.0" }, + { name = "kserve-storage", marker = "extra == 'storage'", specifier = "==2" }, { name = "kubernetes", specifier = ">=23.3.0" }, { name = "numpy", specifier = ">=1.26.0,<3.0.0" }, { name = "orjson", specifier = ">=3.9.15,<4.0.0" }, @@ -944,7 +939,6 @@ requires-dist = [ { name = "python-dateutil", specifier = ">=2.8.0,<3.0.0" }, { name = "pyyaml", specifier = ">=6.0.0,<7.0.0" }, { name = "ray", extras = ["serve"], marker = "extra == 'ray'", specifier = ">=2.43.0" }, - { name = "requests", marker = "extra == 'storage'", specifier = ">=2.32.2,<3.0.0" }, { name = "six", specifier = ">=1.16.0,<2.0.0" }, { name = "tabulate", specifier = ">=0.9.0,<1.0.0" }, { name = "timing-asgi", specifier = ">=0.3.0,<1.0.0" }, diff --git a/python/artexplainer/uv.lock b/python/artexplainer/uv.lock index 6befeebe024..15687a5ae6c 100644 --- a/python/artexplainer/uv.lock +++ b/python/artexplainer/uv.lock @@ -664,18 +664,13 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "asgi-logger", marker = "extra == 'logging'", specifier = ">=0.1.0,<1.0.0" }, - { name = "azure-identity", marker = "extra == 'storage'", specifier = ">=1.15.0,<2.0.0" }, - { name = "azure-storage-blob", marker = "extra == 'storage'", specifier = ">=12.20.0,<13.0.0" }, - { name = "azure-storage-file-share", marker = "extra == 'storage'", specifier = ">=12.16.0,<13.0.0" }, - { name = "boto3", marker = "extra == 'storage'", specifier = ">=1.29.0,<2.0.0" }, { name = "cloudevents", specifier = ">=1.6.2,<2.0.0" }, { name = "fastapi", specifier = ">=0.115.3" }, - { name = "google-cloud-storage", marker = "extra == 'storage'", specifier = ">=2.14.0,<3.0.0" }, { name = "grpc-interceptor", specifier = ">=0.15.4,<1.0.0" }, { name = "grpcio", specifier = ">=1.64.1,<2.0.0" }, { name = "grpcio-tools", specifier = ">=1.64.1,<2.0.0" }, { name = "httpx", specifier = ">=0.27.2,<1.0.0" }, - { name = "huggingface-hub", extras = ["hf-transfer"], marker = "extra == 'storage'", specifier = ">=0.32.0" }, + { name = "kserve-storage", marker = "extra == 'storage'", specifier = "==2" }, { name = "kubernetes", specifier = ">=23.3.0" }, { name = "numpy", specifier = ">=1.26.0,<3.0.0" }, { name = "orjson", specifier = ">=3.9.15,<4.0.0" }, @@ -687,7 +682,6 @@ requires-dist = [ { name = "python-dateutil", specifier = ">=2.8.0,<3.0.0" }, { name = "pyyaml", specifier = ">=6.0.0,<7.0.0" }, { name = "ray", extras = ["serve"], marker = "extra == 'ray'", specifier = ">=2.43.0" }, - { name = "requests", marker = "extra == 'storage'", specifier = ">=2.32.2,<3.0.0" }, { name = "six", specifier = ">=1.16.0,<2.0.0" }, { name = "tabulate", specifier = ">=0.9.0,<1.0.0" }, { name = "timing-asgi", specifier = ">=0.3.0,<1.0.0" }, diff --git a/python/custom_model/uv.lock b/python/custom_model/uv.lock index 8cb85f7873c..29e463ff5fe 100644 --- a/python/custom_model/uv.lock +++ b/python/custom_model/uv.lock @@ -815,18 +815,13 @@ ray = [ [package.metadata] requires-dist = [ { name = "asgi-logger", marker = "extra == 'logging'", specifier = ">=0.1.0,<1.0.0" }, - { name = "azure-identity", marker = "extra == 'storage'", specifier = ">=1.15.0,<2.0.0" }, - { name = "azure-storage-blob", marker = "extra == 'storage'", specifier = ">=12.20.0,<13.0.0" }, - { name = "azure-storage-file-share", marker = "extra == 'storage'", specifier = ">=12.16.0,<13.0.0" }, - { name = "boto3", marker = "extra == 'storage'", specifier = ">=1.29.0,<2.0.0" }, { name = "cloudevents", specifier = ">=1.6.2,<2.0.0" }, { name = "fastapi", specifier = ">=0.115.3" }, - { name = "google-cloud-storage", marker = "extra == 'storage'", specifier = ">=2.14.0,<3.0.0" }, { name = "grpc-interceptor", specifier = ">=0.15.4,<1.0.0" }, { name = "grpcio", specifier = ">=1.64.1,<2.0.0" }, { name = "grpcio-tools", specifier = ">=1.64.1,<2.0.0" }, { name = "httpx", specifier = ">=0.27.2,<1.0.0" }, - { name = "huggingface-hub", extras = ["hf-transfer"], marker = "extra == 'storage'", specifier = ">=0.32.0" }, + { name = "kserve-storage", marker = "extra == 'storage'", specifier = "==2" }, { name = "kubernetes", specifier = ">=23.3.0" }, { name = "numpy", specifier = ">=1.26.0,<3.0.0" }, { name = "orjson", specifier = ">=3.9.15,<4.0.0" }, @@ -838,7 +833,6 @@ requires-dist = [ { name = "python-dateutil", specifier = ">=2.8.0,<3.0.0" }, { name = "pyyaml", specifier = ">=6.0.0,<7.0.0" }, { name = "ray", extras = ["serve"], marker = "extra == 'ray'", specifier = ">=2.43.0" }, - { name = "requests", marker = "extra == 'storage'", specifier = ">=2.32.2,<3.0.0" }, { name = "six", specifier = ">=1.16.0,<2.0.0" }, { name = "tabulate", specifier = ">=0.9.0,<1.0.0" }, { name = "timing-asgi", specifier = ">=0.3.0,<1.0.0" }, diff --git a/python/custom_tokenizer/uv.lock b/python/custom_tokenizer/uv.lock index 325cf448575..2f3efa7d020 100644 --- a/python/custom_tokenizer/uv.lock +++ b/python/custom_tokenizer/uv.lock @@ -503,18 +503,13 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "asgi-logger", marker = "extra == 'logging'", specifier = ">=0.1.0,<1.0.0" }, - { name = "azure-identity", marker = "extra == 'storage'", specifier = ">=1.15.0,<2.0.0" }, - { name = "azure-storage-blob", marker = "extra == 'storage'", specifier = ">=12.20.0,<13.0.0" }, - { name = "azure-storage-file-share", marker = "extra == 'storage'", specifier = ">=12.16.0,<13.0.0" }, - { name = "boto3", marker = "extra == 'storage'", specifier = ">=1.29.0,<2.0.0" }, { name = "cloudevents", specifier = ">=1.6.2,<2.0.0" }, { name = "fastapi", specifier = ">=0.115.3" }, - { name = "google-cloud-storage", marker = "extra == 'storage'", specifier = ">=2.14.0,<3.0.0" }, { name = "grpc-interceptor", specifier = ">=0.15.4,<1.0.0" }, { name = "grpcio", specifier = ">=1.64.1,<2.0.0" }, { name = "grpcio-tools", specifier = ">=1.64.1,<2.0.0" }, { name = "httpx", specifier = ">=0.27.2,<1.0.0" }, - { name = "huggingface-hub", extras = ["hf-transfer"], marker = "extra == 'storage'", specifier = ">=0.32.0" }, + { name = "kserve-storage", marker = "extra == 'storage'", specifier = "==2" }, { name = "kubernetes", specifier = ">=23.3.0" }, { name = "numpy", specifier = ">=1.26.0,<3.0.0" }, { name = "orjson", specifier = ">=3.9.15,<4.0.0" }, @@ -526,7 +521,6 @@ requires-dist = [ { name = "python-dateutil", specifier = ">=2.8.0,<3.0.0" }, { name = "pyyaml", specifier = ">=6.0.0,<7.0.0" }, { name = "ray", extras = ["serve"], marker = "extra == 'ray'", specifier = ">=2.43.0" }, - { name = "requests", marker = "extra == 'storage'", specifier = ">=2.32.2,<3.0.0" }, { name = "six", specifier = ">=1.16.0,<2.0.0" }, { name = "tabulate", specifier = ">=0.9.0,<1.0.0" }, { name = "timing-asgi", specifier = ">=0.3.0,<1.0.0" }, diff --git a/python/custom_transformer/uv.lock b/python/custom_transformer/uv.lock index cc2d080b4ac..d4dfe22391e 100644 --- a/python/custom_transformer/uv.lock +++ b/python/custom_transformer/uv.lock @@ -541,18 +541,13 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "asgi-logger", marker = "extra == 'logging'", specifier = ">=0.1.0,<1.0.0" }, - { name = "azure-identity", marker = "extra == 'storage'", specifier = ">=1.15.0,<2.0.0" }, - { name = "azure-storage-blob", marker = "extra == 'storage'", specifier = ">=12.20.0,<13.0.0" }, - { name = "azure-storage-file-share", marker = "extra == 'storage'", specifier = ">=12.16.0,<13.0.0" }, - { name = "boto3", marker = "extra == 'storage'", specifier = ">=1.29.0,<2.0.0" }, { name = "cloudevents", specifier = ">=1.6.2,<2.0.0" }, { name = "fastapi", specifier = ">=0.115.3" }, - { name = "google-cloud-storage", marker = "extra == 'storage'", specifier = ">=2.14.0,<3.0.0" }, { name = "grpc-interceptor", specifier = ">=0.15.4,<1.0.0" }, { name = "grpcio", specifier = ">=1.64.1,<2.0.0" }, { name = "grpcio-tools", specifier = ">=1.64.1,<2.0.0" }, { name = "httpx", specifier = ">=0.27.2,<1.0.0" }, - { name = "huggingface-hub", extras = ["hf-transfer"], marker = "extra == 'storage'", specifier = ">=0.32.0" }, + { name = "kserve-storage", marker = "extra == 'storage'", specifier = "==2" }, { name = "kubernetes", specifier = ">=23.3.0" }, { name = "numpy", specifier = ">=1.26.0,<3.0.0" }, { name = "orjson", specifier = ">=3.9.15,<4.0.0" }, @@ -564,7 +559,6 @@ requires-dist = [ { name = "python-dateutil", specifier = ">=2.8.0,<3.0.0" }, { name = "pyyaml", specifier = ">=6.0.0,<7.0.0" }, { name = "ray", extras = ["serve"], marker = "extra == 'ray'", specifier = ">=2.43.0" }, - { name = "requests", marker = "extra == 'storage'", specifier = ">=2.32.2,<3.0.0" }, { name = "six", specifier = ">=1.16.0,<2.0.0" }, { name = "tabulate", specifier = ">=0.9.0,<1.0.0" }, { name = "timing-asgi", specifier = ">=0.3.0,<1.0.0" }, diff --git a/python/huggingface_server.Dockerfile b/python/huggingface_server.Dockerfile index cc9afca789a..0a266101ad9 100644 --- a/python/huggingface_server.Dockerfile +++ b/python/huggingface_server.Dockerfile @@ -73,6 +73,11 @@ RUN --mount=type=cache,target=/root/.cache/uv cd kserve && uv sync --active --no COPY kserve kserve RUN --mount=type=cache,target=/root/.cache/uv cd kserve && uv sync --active --no-cache +COPY storage/pyproject.toml storage/uv.lock storage/ +RUN --mount=type=cache,target=/root/.cache/uv cd storage && uv sync --active --no-cache +COPY storage storage +RUN --mount=type=cache,target=/root/.cache/uv cd storage && uv pip install . --no-cache + COPY huggingfaceserver/pyproject.toml huggingfaceserver/uv.lock huggingfaceserver/health_check.py huggingfaceserver/ RUN --mount=type=cache,target=/root/.cache/uv cd huggingfaceserver && uv sync --active --no-cache COPY huggingfaceserver huggingfaceserver @@ -147,6 +152,7 @@ RUN useradd kserve -m -u 1000 -d /home/kserve COPY --from=build --chown=kserve:kserve ${WORKSPACE_DIR}/third_party third_party COPY --from=build --chown=kserve:kserve ${WORKSPACE_DIR}/$VENV_PATH $VENV_PATH COPY --from=build ${WORKSPACE_DIR}/kserve kserve +COPY --from=build ${WORKSPACE_DIR}/storage storage COPY --from=build ${WORKSPACE_DIR}/huggingfaceserver huggingfaceserver # Set a writable Hugging Face home folder to avoid permission issue. See https://github.com/kserve/kserve/issues/3562 diff --git a/python/huggingface_server_cpu.Dockerfile b/python/huggingface_server_cpu.Dockerfile index c533183aa25..104176b2a05 100644 --- a/python/huggingface_server_cpu.Dockerfile +++ b/python/huggingface_server_cpu.Dockerfile @@ -64,6 +64,13 @@ RUN cd kserve && \ uv cache clean && \ rm -rf ~/.cache/uv + # Copy and install dependencies for kserve-storage using uv +COPY storage/pyproject.toml storage/uv.lock storage/ +RUN cd storage && uv sync --active --no-cache + +COPY storage storage +RUN cd storage && uv pip install . --no-cache + # Install huggingfaceserver using UV COPY huggingfaceserver huggingfaceserver RUN cd huggingfaceserver && \ @@ -134,6 +141,7 @@ COPY --from=builder --chown=kserve:kserve third_party third_party COPY --from=builder --chown=kserve:kserve $VIRTUAL_ENV $VIRTUAL_ENV COPY --from=builder --chown=kserve:kserve huggingfaceserver huggingfaceserver COPY --from=builder --chown=kserve:kserve kserve kserve +COPY --from=builder --chown=kserve:kserve storage storage RUN df -hT diff --git a/python/huggingfaceserver/huggingfaceserver/__main__.py b/python/huggingfaceserver/huggingfaceserver/__main__.py index 7936418388b..c0c81e93c82 100644 --- a/python/huggingfaceserver/huggingfaceserver/__main__.py +++ b/python/huggingfaceserver/huggingfaceserver/__main__.py @@ -21,7 +21,7 @@ from huggingfaceserver.request_logger import RequestLogger from kserve import logging from kserve.logging import logger -from kserve.storage import Storage +from kserve_storage import Storage from transformers import AutoConfig diff --git a/python/huggingfaceserver/pyproject.toml b/python/huggingfaceserver/pyproject.toml index ad32b7cbe0e..06ba6870657 100644 --- a/python/huggingfaceserver/pyproject.toml +++ b/python/huggingfaceserver/pyproject.toml @@ -5,7 +5,8 @@ authors = [ license = {text = "Apache-2.0"} requires-python = "<3.13,>=3.9" dependencies = [ - "kserve[llm,storage] @ file:///${PROJECT_ROOT}/../kserve", + "kserve[llm] @ file:///${PROJECT_ROOT}/../kserve", + "kserve-storage @ file:///${PROJECT_ROOT}/../storage", "transformers>=4.51.2", "accelerate<2.0.0,>=1.6.0", "torch>=2.7.0", diff --git a/python/huggingfaceserver/uv.lock b/python/huggingfaceserver/uv.lock index 5708c63c3ac..49bbd04a6a8 100644 --- a/python/huggingfaceserver/uv.lock +++ b/python/huggingfaceserver/uv.lock @@ -1371,7 +1371,8 @@ source = { virtual = "." } dependencies = [ { name = "accelerate" }, { name = "bitsandbytes" }, - { name = "kserve", extra = ["llm", "storage"] }, + { name = "kserve", extra = ["llm"] }, + { name = "kserve-storage" }, { name = "modelscope" }, { name = "setuptools" }, { name = "torch" }, @@ -1399,7 +1400,8 @@ test = [ requires-dist = [ { name = "accelerate", specifier = ">=1.6.0,<2.0.0" }, { name = "bitsandbytes", specifier = ">=0.45.3" }, - { name = "kserve", extras = ["llm", "storage"], directory = "../kserve" }, + { name = "kserve", extras = ["llm"], directory = "../kserve" }, + { name = "kserve-storage", directory = "../storage" }, { name = "modelscope", specifier = ">=1.16.0,<2.0.0" }, { name = "setuptools", specifier = ">=70.0.0" }, { name = "torch", specifier = ">=2.7.0" }, @@ -1605,31 +1607,17 @@ llm = [ { name = "transformers" }, { name = "vllm" }, ] -storage = [ - { name = "azure-identity" }, - { name = "azure-storage-blob" }, - { name = "azure-storage-file-share" }, - { name = "boto3" }, - { name = "google-cloud-storage" }, - { name = "huggingface-hub", extra = ["hf-transfer"] }, - { name = "requests" }, -] [package.metadata] requires-dist = [ { name = "asgi-logger", marker = "extra == 'logging'", specifier = ">=0.1.0,<1.0.0" }, - { name = "azure-identity", marker = "extra == 'storage'", specifier = ">=1.15.0,<2.0.0" }, - { name = "azure-storage-blob", marker = "extra == 'storage'", specifier = ">=12.20.0,<13.0.0" }, - { name = "azure-storage-file-share", marker = "extra == 'storage'", specifier = ">=12.16.0,<13.0.0" }, - { name = "boto3", marker = "extra == 'storage'", specifier = ">=1.29.0,<2.0.0" }, { name = "cloudevents", specifier = ">=1.6.2,<2.0.0" }, { name = "fastapi", specifier = ">=0.115.3" }, - { name = "google-cloud-storage", marker = "extra == 'storage'", specifier = ">=2.14.0,<3.0.0" }, { name = "grpc-interceptor", specifier = ">=0.15.4,<1.0.0" }, { name = "grpcio", specifier = ">=1.64.1,<2.0.0" }, { name = "grpcio-tools", specifier = ">=1.64.1,<2.0.0" }, { name = "httpx", specifier = ">=0.27.2,<1.0.0" }, - { name = "huggingface-hub", extras = ["hf-transfer"], marker = "extra == 'storage'", specifier = ">=0.32.0" }, + { name = "kserve-storage", marker = "extra == 'storage'", specifier = "==2" }, { name = "kubernetes", specifier = ">=23.3.0" }, { name = "numpy", specifier = ">=1.26.0,<3.0.0" }, { name = "orjson", specifier = ">=3.9.15,<4.0.0" }, @@ -1641,7 +1629,6 @@ requires-dist = [ { name = "python-dateutil", specifier = ">=2.8.0,<3.0.0" }, { name = "pyyaml", specifier = ">=6.0.0,<7.0.0" }, { name = "ray", extras = ["serve"], marker = "extra == 'ray'", specifier = ">=2.43.0" }, - { name = "requests", marker = "extra == 'storage'", specifier = ">=2.32.2,<3.0.0" }, { name = "six", specifier = ">=1.16.0,<2.0.0" }, { name = "tabulate", specifier = ">=0.9.0,<1.0.0" }, { name = "timing-asgi", specifier = ">=0.3.0,<1.0.0" }, @@ -1668,6 +1655,31 @@ test = [ { name = "tomlkit", specifier = ">=0.12.0,<1.0.0" }, ] +[[package]] +name = "kserve-storage" +version = "0.15.2" +source = { directory = "../storage" } +dependencies = [ + { name = "azure-identity" }, + { name = "azure-storage-blob" }, + { name = "azure-storage-file-share" }, + { name = "boto3" }, + { name = "google-cloud-storage" }, + { name = "huggingface-hub", extra = ["hf-transfer"] }, + { name = "requests" }, +] + +[package.metadata] +requires-dist = [ + { name = "azure-identity", specifier = ">=1.15.0,<2.0.0" }, + { name = "azure-storage-blob", specifier = ">=12.20.0,<13.0.0" }, + { name = "azure-storage-file-share", specifier = ">=12.16.0,<13.0.0" }, + { name = "boto3", specifier = ">=1.29.0,<2.0.0" }, + { name = "google-cloud-storage", specifier = ">=2.14.0,<3.0.0" }, + { name = "huggingface-hub", extras = ["hf-transfer"], specifier = ">=0.32.0" }, + { name = "requests", specifier = ">=2.32.2,<3.0.0" }, +] + [[package]] name = "kubernetes" version = "32.0.1" diff --git a/python/kserve/Makefile b/python/kserve/Makefile index 5ebdb7d0e5a..da31cd1d54d 100644 --- a/python/kserve/Makefile +++ b/python/kserve/Makefile @@ -1,10 +1,12 @@ .PHONY: test dev_install: - uv sync --active --group test --extra storage --extra ray --extra llm + uv sync --active --group test --extra ray --extra llm + uv pip install ../storage --no-cache install_dependencies: - uv sync --active --group test --extra storage --extra ray --extra llm + uv sync --active --group test --extra ray --extra llm + uv pip install ../storage --no-cache test: cd ../ && pytest -W ignore kserve/test diff --git a/python/kserve/kserve/storage/__init__.py b/python/kserve/kserve/storage/__init__.py deleted file mode 100644 index f6686f24dc4..00000000000 --- a/python/kserve/kserve/storage/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright 2023 The KServe Authors. -# -# 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. - -# flake8: noqa - -from kserve.storage.storage import Storage diff --git a/python/kserve/kserve/storage/storage.py b/python/kserve/kserve/storage/storage.py deleted file mode 100644 index d75f937cc37..00000000000 --- a/python/kserve/kserve/storage/storage.py +++ /dev/null @@ -1,798 +0,0 @@ -# Copyright 2023 The KServe Authors. -# -# 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. - -import base64 -import glob -import gzip -import json -import mimetypes -import os -import re -import shutil -import tarfile -import tempfile -import time -import zipfile -from pathlib import Path -from typing import Dict -from urllib.parse import urlparse -import requests - -from ..logging import logger - -MODEL_MOUNT_DIRS = "/mnt/models" - -_GCS_PREFIX = "gs://" -_S3_PREFIX = "s3://" -_HDFS_PREFIX = "hdfs://" -_WEBHDFS_PREFIX = "webhdfs://" -_AZURE_BLOB_RE = [ - "https://(.+?).blob.core.windows.net/(.+)", - "https://(.+?).z[0-9]{1,2}.blob.storage.azure.net/(.+)", -] -_AZURE_FILE_RE = [ - "https://(.+?).file.core.windows.net/(.+)", - "https://(.+?).z[0-9]{1,2}.file.storage.azure.net/(.+)", -] -_LOCAL_PREFIX = "file://" -_URI_RE = "https?://(.+)/(.+)" -_HTTP_PREFIX = "http(s)://" -_HEADERS_SUFFIX = "-headers" -_PVC_PREFIX = "/mnt/pvc" -_HF_PREFIX = "hf://" - -_HDFS_SECRET_DIRECTORY = "/var/secrets/kserve-hdfscreds" -_HDFS_FILE_SECRETS = ["KERBEROS_KEYTAB", "TLS_CERT", "TLS_KEY", "TLS_CA"] - - -class Storage(object): - @staticmethod - def download(uri: str, out_dir: str = None) -> str: - start = time.monotonic() - Storage._update_with_storage_spec() - logger.info("Copying contents of %s to local", uri) - - if uri.startswith(_PVC_PREFIX) and not os.path.exists(uri): - raise Exception(f"Cannot locate source uri {uri} for PVC") - - is_local = uri.startswith(_LOCAL_PREFIX) or os.path.exists(uri) - if is_local: - if out_dir is None: - # noop if out_dir is not set and the path is local - model_dir = Storage._download_local(uri) - else: - if not os.path.exists(out_dir): - os.mkdir(out_dir) - model_dir = Storage._download_local(uri, out_dir) - else: - if out_dir is None: - out_dir = tempfile.mkdtemp() - elif not os.path.exists(out_dir): - os.mkdir(out_dir) - - if uri.startswith(MODEL_MOUNT_DIRS): - # Don't need to download models if this InferenceService is running in the multi-model - # serving mode. The model agent will download models. - model_dir = out_dir - elif uri.startswith(_GCS_PREFIX): - model_dir = Storage._download_gcs(uri, out_dir) - elif uri.startswith(_S3_PREFIX): - model_dir = Storage._download_s3(uri, out_dir) - elif uri.startswith(_HDFS_PREFIX) or uri.startswith(_WEBHDFS_PREFIX): - model_dir = Storage._download_hdfs(uri, out_dir) - elif any(re.search(pattern, uri) for pattern in _AZURE_BLOB_RE): - model_dir = Storage._download_azure_blob(uri, out_dir) - elif any(re.search(pattern, uri) for pattern in _AZURE_FILE_RE): - model_dir = Storage._download_azure_file_share(uri, out_dir) - elif re.search(_URI_RE, uri): - model_dir = Storage._download_from_uri(uri, out_dir) - elif uri.startswith(_HF_PREFIX): - model_dir = Storage._download_hf(uri, out_dir) - else: - raise Exception( - "Cannot recognize storage type for " - + uri - + "\n'%s', '%s', '%s', '%s' and '%s' are the current available storage type." - % (_GCS_PREFIX, _S3_PREFIX, _LOCAL_PREFIX, _HTTP_PREFIX, _HF_PREFIX) - ) - - logger.info("Successfully copied %s to %s", uri, out_dir) - logger.info(f"Model downloaded in {time.monotonic() - start} seconds.") - return model_dir - - @staticmethod - def _update_with_storage_spec(): - storage_secret_json = json.loads(os.environ.get("STORAGE_CONFIG", "{}")) - storage_secret_override_params = json.loads( - os.environ.get("STORAGE_OVERRIDE_CONFIG", "{}") - ) - if storage_secret_override_params: - for key, value in storage_secret_override_params.items(): - storage_secret_json[key] = value - - if storage_secret_json.get("type", "") == "s3": - for env_var, key in ( - ("AWS_ENDPOINT_URL", "endpoint_url"), - ("AWS_ACCESS_KEY_ID", "access_key_id"), - ("AWS_SECRET_ACCESS_KEY", "secret_access_key"), - ("AWS_DEFAULT_REGION", "region"), - ("AWS_CA_BUNDLE", "ca_bundle"), - ("S3_VERIFY_SSL", "verify_ssl"), - ("awsAnonymousCredential", "anonymous"), - ): - if key in storage_secret_json: - os.environ[env_var] = storage_secret_json.get(key) - - if ( - storage_secret_json.get("type", "") == "hdfs" - or storage_secret_json.get("type", "") == "webhdfs" - ): - temp_dir = tempfile.mkdtemp() - os.environ["HDFS_SECRET_DIR"] = temp_dir - for key, value in storage_secret_json.items(): - mode = "w" - - # If the secret is supposed to be a file, then it was base64 encoded in the json - if key in _HDFS_FILE_SECRETS: - value = base64.b64decode(value) - mode = "wb" - - with open(f"{temp_dir}/{key}", mode) as f: - f.write(value) - f.flush() - - @staticmethod - def get_S3_config(): - from botocore import UNSIGNED - from botocore.client import Config - - # default s3 config - c = Config() - - # anon environment variable defined in s3_secret.go - anon = "true" == os.getenv("awsAnonymousCredential", "false").lower() - # S3UseVirtualBucket environment variable defined in s3_secret.go - # use virtual hosted-style URLs if enabled - virtual = "true" == os.getenv("S3_USER_VIRTUAL_BUCKET", "false").lower() - # S3UseAccelerate environment variable defined in s3_secret.go - # use transfer acceleration if enabled - accelerate = "true" == os.getenv("S3_USE_ACCELERATE", "false").lower() - - if anon: - c = c.merge(Config(signature_version=UNSIGNED)) - if virtual: - c = c.merge(Config(s3={"addressing_style": "virtual"})) - if accelerate: - c = c.merge(Config(s3={"use_accelerate_endpoint": accelerate})) - - # NOTE: If endpoint_url provided is legacy ("https://s3.amazonaws.com") and region is not global (us-east-1), set to virtual addressing style - # So that request would not return PermanentRedirect due to region not in the endpoint url - # AWS SDK retries under the hood to set the correct region when the valid virtual addressing style endpoint url is provided - endpoint_url = os.getenv("AWS_ENDPOINT_URL") - region = os.getenv("AWS_DEFAULT_REGION") - if endpoint_url == "https://s3.amazonaws.com" and region not in ( - None, - "us-east-1", - ): - c = c.merge(Config(s3={"addressing_style": "virtual"})) - - return c - - @staticmethod - def _download_s3(uri, temp_dir: str) -> str: - import boto3 - - # Boto3 looks at various configuration locations until it finds configuration values. - # lookup order: - # 1. Config object passed in as the config parameter when creating S3 resource - # if awsAnonymousCredential env var true, passed in via config - # 2. Environment variables - # 3. ~/.aws/config file - kwargs = {"config": Storage.get_S3_config()} - endpoint_url = os.getenv("AWS_ENDPOINT_URL") - if endpoint_url: - kwargs.update({"endpoint_url": endpoint_url}) - verify_ssl = os.getenv("S3_VERIFY_SSL") - if verify_ssl: - verify_ssl = not verify_ssl.lower() in ["0", "false"] - kwargs.update({"verify": verify_ssl}) - else: - verify_ssl = True - - # If verify_ssl is true, then check there is custom ca bundle cert - # The CA bundle can be any local file in the container under the path - # set in the AWS_CA_BUNDLE environment variable. - # It can also be coming from a ConfigMap, in which case the filename - # is cabundle.crt. - if verify_ssl: - global_ca_bundle_configmap = os.getenv("CA_BUNDLE_CONFIGMAP_NAME") - isvc_aws_ca_bundle_path = os.getenv("AWS_CA_BUNDLE") - ca_bundle_set = False - if isvc_aws_ca_bundle_path and isvc_aws_ca_bundle_path != "": - ca_bundle_set = True - ca_bundle_full_path = isvc_aws_ca_bundle_path - elif global_ca_bundle_configmap: - ca_bundle_set = True - global_ca_bundle_volume_mount_path = os.getenv( - "CA_BUNDLE_VOLUME_MOUNT_POINT" - ) - ca_bundle_full_path = os.path.join( - global_ca_bundle_volume_mount_path, "cabundle.crt" - ) - if ca_bundle_set: - if os.path.exists(ca_bundle_full_path): - logger.info("ca bundle file(%s) exists." % (ca_bundle_full_path)) - kwargs.update({"verify": ca_bundle_full_path}) - else: - raise RuntimeError( - "Failed to find ca bundle file(%s)." % ca_bundle_full_path - ) - s3 = boto3.resource("s3", **kwargs) - parsed = urlparse(uri, scheme="s3") - bucket_name = parsed.netloc - bucket_path = parsed.path.lstrip("/") - - file_count = 0 - exact_obj_found = False - bucket = s3.Bucket(bucket_name) - for obj in bucket.objects.filter(Prefix=bucket_path): - # Skip where boto3 lists the directory as an object - if obj.key.endswith("/"): - continue - # In the case where bucket_path points to a single object, set the target key to bucket_path - # Otherwise, remove the bucket_path prefix, strip any extra slashes, then prepend the target_dir - # Example: - # s3://test-bucket - # Objects: /a/b/c/model.bin /a/model.bin /model.bin - # - # If 'uri' is set to "s3://test-bucket", then the downloader will - # download all the objects listed above, re-creating their subpaths - # under the temp_dir. - # If 'uri' is set to "s3://test-bucket/a", then the downloader will - # add to temp_dir: b/c/model.bin and model.bin. - # If 'uri' is set to "s3://test-bucket/a/b/c/model.bin", then - # the downloader will add to temp dir: model.bin - # (without any subpaths). - # If the bucket path is s3://test/models - # Objects: churn, churn-pickle, churn-pickle-logs - - if bucket_path == obj.key: - target_key = obj.key.rsplit("/", 1)[-1] - exact_obj_found = True - - else: - target_key = re.sub(r"^" + re.escape(bucket_path) + r"/?", "", obj.key) - - target = f"{temp_dir}/{target_key}" - if not os.path.exists(os.path.dirname(target)): - os.makedirs(os.path.dirname(target), exist_ok=True) - bucket.download_file(obj.key, target) - logger.info("Downloaded object %s to %s" % (obj.key, target)) - file_count += 1 - - # If the exact object is found, then it is sufficient to download that and break the loop - if exact_obj_found: - break - if file_count == 0: - raise RuntimeError( - "Failed to fetch model. No model found in %s." % bucket_path - ) - - # Unpack compressed file, supports .tgz, tar.gz and zip file formats. - if file_count == 1: - mimetype, _ = mimetypes.guess_type(target) - if mimetype in ["application/x-tar", "application/zip"]: - temp_dir = Storage._unpack_archive_file(target, mimetype, temp_dir) - return temp_dir - - @staticmethod - def _download_hf(uri, temp_dir: str) -> str: - from huggingface_hub import snapshot_download - - components = uri[len(_HF_PREFIX) :].split("/") - - # Validate that the URI has two parts: repo and model (optional hash) - if len(components) != 2: - raise ValueError( - "URI must contain exactly one '/' separating the repo and model name" - ) - - repo = components[0] - model_part = components[1] - - if not repo: - raise ValueError("Repository name cannot be empty") - if not model_part: - raise ValueError("Model name cannot be empty") - - model, _, hash_value = model_part.partition(":") - # Ensure model is non-empty - if not model: - raise ValueError("Model name cannot be empty") - - revision = hash_value if hash_value else None - - snapshot_download( - repo_id=f"{repo}/{model}", revision=revision, local_dir=temp_dir - ) - return temp_dir - - @staticmethod - def _download_gcs(uri, temp_dir: str) -> str: - from google.auth import exceptions - from google.cloud import storage - import copy - - try: - storage_client = storage.Client() - except exceptions.DefaultCredentialsError: - storage_client = storage.Client.create_anonymous_client() - bucket_args = uri.replace(_GCS_PREFIX, "", 1).split("/", 1) - bucket_name = bucket_args[0] - bucket_path = bucket_args[1] if len(bucket_args) > 1 else "" - bucket = storage_client.bucket(bucket_name) - prefix = bucket_path - if not prefix.endswith("/"): - prefix = prefix + "/" - blobs = bucket.list_blobs(prefix=prefix) - file_count = 0 - - # Shallow copy, otherwise Iterator has already started - shallow_blobs = copy.copy(blobs) - blob = bucket.blob(bucket_path) - # checks if the blob is a file or a directory - if blob.name == bucket_path and len(list(shallow_blobs)) == 0: - dest_path = os.path.join(temp_dir, os.path.basename(bucket_path)) - logger.info("Downloading single file to: %s", dest_path) - blob.download_to_filename(dest_path) - file_count = 1 - - else: - for blob in blobs: - # Replace any prefix from the object key with temp_dir - subdir_object_key = blob.name.replace(bucket_path, "", 1).lstrip("/") - # Create necessary subdirectory to store the object locally - if "/" in subdir_object_key: - local_object_dir = os.path.join( - temp_dir, subdir_object_key.rsplit("/", 1)[0] - ) - if not os.path.isdir(local_object_dir): - os.makedirs(local_object_dir, exist_ok=True) - if subdir_object_key.strip() != "" and not subdir_object_key.endswith( - "/" - ): - dest_path = os.path.join(temp_dir, subdir_object_key) - logger.info("Downloading: %s", dest_path) - blob.download_to_filename(dest_path) - file_count += 1 - - if file_count == 0: - raise RuntimeError("Failed to fetch model. No model found in %s." % uri) - - # Unpack compressed file, supports .tgz, tar.gz and zip file formats. - if file_count == 1: - mimetype, _ = mimetypes.guess_type(blob.name) - if mimetype in ["application/x-tar", "application/zip"]: - temp_dir = Storage._unpack_archive_file(dest_path, mimetype, temp_dir) - return temp_dir - - @staticmethod - def _load_hdfs_configuration() -> Dict: - config = { - "HDFS_NAMENODE": None, - "USER_PROXY": None, - "HDFS_ROOTPATH": None, - "TLS_CERT": None, - "TLS_KEY": None, - "TLS_CA": None, - "TLS_SKIP_VERIFY": "false", - "HEADERS": None, - "N_THREADS": "2", - "KERBEROS_KEYTAB": None, - "KERBEROS_PRINCIPAL": None, - } - - secret_dir = _HDFS_SECRET_DIRECTORY - if os.environ.get("HDFS_SECRET_DIR"): - secret_dir = os.environ["HDFS_SECRET_DIR"] - - for filename in os.listdir(secret_dir): - if filename not in config: - continue - - # We don't read files which are supposed to be files, just save their path - if filename in _HDFS_FILE_SECRETS: - config[filename] = f"{secret_dir}/{filename}" - continue - - # Read file and save value in config dict - with open(f"{secret_dir}/{filename}") as f: - config[filename] = f.read() - - return config - - @staticmethod - def _download_hdfs(uri, out_dir: str) -> str: - from krbcontext.context import krbContext - from hdfs.ext.kerberos import Client, KerberosClient - - config = Storage._load_hdfs_configuration() - - logger.info(f"Using the following hdfs config\n{config}") - - # Remove hdfs:// or webhdfs:// from the uri to get just the path - # e.g. hdfs://user/me/model -> user/me/model - if uri.startswith(_HDFS_PREFIX): - path = uri[len(_HDFS_PREFIX) :] - else: - path = uri[len(_WEBHDFS_PREFIX) :] - - if not config["HDFS_ROOTPATH"]: - path = "/" + path - - s = requests.Session() - - if config["TLS_CERT"]: - s.cert = (config["TLS_CERT"], config["TLS_KEY"]) - # s.verify = , True, False, or CA PATH - if config["TLS_CA"]: - s.verify = config["TLS_CA"] - if config["TLS_SKIP_VERIFY"].lower() == "true": - s.verify = False - - if config["HEADERS"]: - headers = json.loads(config["HEADERS"]) - s.headers.update(headers) - - if config["KERBEROS_PRINCIPAL"]: - context = krbContext( - using_keytab=True, - principal=config["KERBEROS_PRINCIPAL"], - keytab_file=config["KERBEROS_KEYTAB"], - ) - context.init_with_keytab() - client = KerberosClient( - config["HDFS_NAMENODE"], - proxy=config["USER_PROXY"], - root=config["HDFS_ROOTPATH"], - session=s, - ) - else: - client = Client( - config["HDFS_NAMENODE"], - proxy=config["USER_PROXY"], - root=config["HDFS_ROOTPATH"], - session=s, - ) - file_count = 0 - dest_file_path = "" - - # Check path exists and get path status - # Raises HdfsError when path does not exist - status = client.status(path) - - if status["type"] == "FILE": - client.download(path, out_dir, n_threads=1) - file_count += 1 - file_name = path.rsplit("/", 1)[-1] - dest_file_path = f"{out_dir}/{file_name}" - else: - files = client.list(path) - file_count += len(files) - for f in files: - client.download( - f"{path}/{f}", out_dir, n_threads=int(config["N_THREADS"]) - ) - dest_file_path = f"{out_dir}/{f}" - - if file_count == 1: - mimetype, _ = mimetypes.guess_type(dest_file_path) - if mimetype in ["application/x-tar", "application/zip"]: - out_dir = Storage._unpack_archive_file( - dest_file_path, mimetype, out_dir - ) - return out_dir - - @staticmethod - def _download_azure_blob( - uri, out_dir: str - ) -> str: # pylint: disable=too-many-locals - from azure.storage.blob import BlobServiceClient - from azure.storage.blob._list_blobs_helper import BlobPrefix - - account_name, account_url, container_name, prefix = Storage._parse_azure_uri( - uri - ) - logger.info( - "Connecting to BLOB account: [%s], container: [%s], prefix: [%s]", - account_name, - container_name, - prefix, - ) - token = ( - Storage._get_azure_storage_token() - or Storage._get_azure_storage_access_key() - ) - if token is None: - logger.warning( - "Azure credentials or shared access signature token not found, retrying anonymous access" - ) - - blob_service_client = BlobServiceClient(account_url, credential=token) - container_client = blob_service_client.get_container_client(container_name) - file_count = 0 - blobs = [] - max_depth = 5 - stack = [(prefix, max_depth)] - while stack: - curr_prefix, depth = stack.pop() - if depth < 0: - continue - for item in container_client.walk_blobs(name_starts_with=curr_prefix): - if isinstance(item, BlobPrefix): - stack.append((item.name, depth - 1)) - else: - blobs += container_client.list_blobs( - name_starts_with=item.name, include=["snapshots"] - ) - for blob in blobs: - file_name = blob.name.replace(prefix, "", 1).lstrip("/") - if not file_name: - file_name = os.path.basename(prefix) - dest_path = os.path.join(out_dir, file_name) - Path(os.path.dirname(dest_path)).mkdir(parents=True, exist_ok=True) - logger.info("Downloading: %s to %s", blob.name, dest_path) - downloader = container_client.download_blob(blob.name) - with open(dest_path, "wb+") as f: - f.write(downloader.readall()) - file_count += 1 - if file_count == 0: - raise RuntimeError("Failed to fetch model. No model found in %s." % (uri)) - - # Unpack compressed file, supports .tgz, tar.gz and zip file formats. - if file_count == 1: - mimetype, _ = mimetypes.guess_type(dest_path) - if mimetype in ["application/x-tar", "application/zip"]: - out_dir = Storage._unpack_archive_file(dest_path, mimetype, out_dir) - return out_dir - - @staticmethod - def _download_azure_file_share( - uri, out_dir: str - ) -> str: # pylint: disable=too-many-locals - from azure.storage.fileshare import ShareServiceClient - - account_name, account_url, share_name, prefix = Storage._parse_azure_uri(uri) - logger.info( - "Connecting to file share account: [%s], container: [%s], prefix: [%s]", - account_name, - share_name, - prefix, - ) - access_key = Storage._get_azure_storage_access_key() - if access_key is None: - logger.warning( - "Azure storage access key not found, retrying anonymous access" - ) - - share_service_client = ShareServiceClient(account_url, credential=access_key) - share_client = share_service_client.get_share_client(share_name) - file_count = 0 - share_files = [] - max_depth = 5 - stack = [(prefix, max_depth)] - while stack: - curr_prefix, depth = stack.pop() - if depth < 0: - continue - for item in share_client.list_directories_and_files( - directory_name=curr_prefix - ): - if item.is_directory: - stack.append( - ("/".join([curr_prefix, item.name]).strip("/"), depth - 1) - ) - else: - share_files.append((curr_prefix, item)) - for prefix, file_item in share_files: - parts = [prefix] if prefix else [] - parts.append(file_item.name) - file_path = "/".join(parts).lstrip("/") - dest_path = os.path.join(out_dir, file_path) - Path(os.path.dirname(dest_path)).mkdir(parents=True, exist_ok=True) - logger.info("Downloading: %s to %s", file_item.name, dest_path) - file_client = share_client.get_file_client(file_path) - with open(dest_path, "wb+") as f: - data = file_client.download_file() - data.readinto(f) - file_count += 1 - if file_count == 0: - raise RuntimeError("Failed to fetch model. No model found in %s." % (uri)) - - # Unpack compressed file, supports .tgz, tar.gz and zip file formats. - if file_count == 1: - mimetype, _ = mimetypes.guess_type(dest_path) - if mimetype in ["application/x-tar", "application/zip"]: - out_dir = Storage._unpack_archive_file(dest_path, mimetype, out_dir) - return out_dir - - @staticmethod - def _parse_azure_uri(uri): # pylint: disable=too-many-locals - parsed = urlparse(uri) - account_name = parsed.netloc.split(".")[0] - account_url = "https://{}{}".format( - parsed.netloc, "?" + parsed.query if parsed.query else "" - ) - object_name, prefix = parsed.path.lstrip("/").split("/", 1) - prefix = prefix.strip("/") - return account_name, account_url, object_name, prefix - - @staticmethod - def _get_azure_storage_token(): - tenant_id = os.getenv("AZ_TENANT_ID", "") - client_id = os.getenv("AZ_CLIENT_ID", "") - client_secret = os.getenv("AZ_CLIENT_SECRET", "") - - # convert old environment variable to conform Azure defaults - # see azure/identity/_constants.py - if tenant_id: - os.environ["AZURE_TENANT_ID"] = tenant_id - if client_id: - os.environ["AZURE_CLIENT_ID"] = client_id - if client_secret: - os.environ["AZURE_CLIENT_SECRET"] = client_secret - - client_id = os.getenv("AZURE_CLIENT_ID", "") - if not client_id: - return None - - # note the SP must have "Storage Blob Data Owner" perms for this to work - from azure.identity import DefaultAzureCredential - - token_credential = DefaultAzureCredential() - - logger.info("Retrieved SP token credential for client_id: %s", client_id) - return token_credential - - @staticmethod - def _get_azure_storage_access_key(): - return os.getenv("AZURE_STORAGE_ACCESS_KEY") - - @staticmethod - def _download_local(uri, out_dir=None) -> str: - local_path = uri.replace(_LOCAL_PREFIX, "", 1) - if not os.path.exists(local_path): - raise RuntimeError("Local path %s does not exist." % (uri)) - - if out_dir is None: - return local_path - elif not os.path.isdir(out_dir): - os.makedirs(out_dir, exist_ok=True) - - if os.path.isdir(local_path): - local_path = os.path.join(local_path, "*") - - file_count = 0 - for src in glob.glob(local_path): - _, tail = os.path.split(src) - dest_path = os.path.join(out_dir, tail) - logger.info("Linking: %s to %s", src, dest_path) - if not os.path.exists(dest_path): - os.symlink(src, dest_path) - else: - logger.info("File %s already exist", dest_path) - file_count += 1 - if file_count == 0: - raise RuntimeError("Failed to fetch model. No model found in %s." % (uri)) - # Unpack compressed file, supports .tgz, tar.gz and zip file formats. - if file_count == 1: - mimetype, _ = mimetypes.guess_type(dest_path) - if mimetype in ["application/x-tar", "application/zip"]: - out_dir = Storage._unpack_archive_file(dest_path, mimetype, out_dir) - return out_dir - - @staticmethod - def _download_from_uri(uri, out_dir=None) -> str: - url = urlparse(uri) - filename = os.path.basename(url.path) - # Determine if the symbol '?' exists in the path - if mimetypes.guess_type(url.path)[0] is None and url.query != "": - mimetype, encoding = mimetypes.guess_type(url.query) - else: - mimetype, encoding = mimetypes.guess_type(url.path) - local_path = os.path.join(out_dir, filename) - - if filename == "": - raise ValueError("No filename contained in URI: %s" % (uri)) - - # Get header information from host url - headers = {} - host_uri = url.hostname - - headers_json = os.getenv(host_uri + _HEADERS_SUFFIX, "{}") - headers = json.loads(headers_json) - - with requests.get(uri, stream=True, headers=headers) as response: - if response.status_code != 200: - raise RuntimeError( - "URI: %s returned a %s response code." % (uri, response.status_code) - ) - zip_content_types = ( - "application/x-zip-compressed", - "application/zip", - "application/zip-compressed", - ) - if mimetype == "application/zip" and not response.headers.get( - "Content-Type", "" - ).startswith(zip_content_types): - raise RuntimeError( - "URI: %s did not respond with any of following 'Content-Type': " - % uri - + ", ".join(zip_content_types) - ) - tar_content_types = ( - "application/x-tar", - "application/x-gtar", - "application/x-gzip", - "application/gzip", - ) - if mimetype == "application/x-tar" and not response.headers.get( - "Content-Type", "" - ).startswith(tar_content_types): - raise RuntimeError( - "URI: %s did not respond with any of following 'Content-Type': " - % uri - + ", ".join(tar_content_types) - ) - if ( - mimetype != "application/zip" and mimetype != "application/x-tar" - ) and not response.headers.get("Content-Type", "").startswith( - "application/octet-stream" - ): - raise RuntimeError( - "URI: %s did not respond with 'Content-Type': 'application/octet-stream'" - % uri - ) - - if encoding == "gzip": - stream = gzip.GzipFile(fileobj=response.raw) - local_path = os.path.join(out_dir, f"{filename}.tar") - else: - stream = response.raw - with open(local_path, "wb") as out: - shutil.copyfileobj(stream, out) - - if mimetype in ["application/x-tar", "application/zip"]: - out_dir = Storage._unpack_archive_file(local_path, mimetype, out_dir) - return out_dir - - @staticmethod - def _unpack_archive_file(file_path, mimetype, target_dir=None) -> str: - if not target_dir: - target_dir = os.path.dirname(file_path) - - try: - logger.info("Unpacking: %s", file_path) - if mimetype == "application/x-tar": - with tarfile.open(file_path, "r", encoding="utf-8") as archive: - archive.extractall(target_dir, filter="data") - else: - with zipfile.ZipFile(file_path, "r") as archive: - archive.extractall(target_dir) - except (tarfile.TarError, zipfile.BadZipfile) as e: - raise RuntimeError( - "Failed to unpack archive file. The file format is not valid." - ) from e - os.remove(file_path) - return target_dir diff --git a/python/kserve/kserve/storage/test/test_azure_storage.py b/python/kserve/kserve/storage/test/test_azure_storage.py deleted file mode 100644 index 4cd5a986b4f..00000000000 --- a/python/kserve/kserve/storage/test/test_azure_storage.py +++ /dev/null @@ -1,387 +0,0 @@ -# Copyright 2021 The KServe Authors. -# -# 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. - -import unittest.mock as mock -import pytest -import shutil - -from kserve.storage import Storage - -STORAGE_MODULE = "kserve.storage.storage" - - -def create_mock_item(path): - mock_obj = mock.MagicMock() - mock_obj.name = path - mock_obj.readall.return_value = b"test" - return mock_obj - - -def create_mock_blob(mock_storage, paths): - mock_objs = [create_mock_item(path) for path in paths] - mock_container = mock.MagicMock() - mock_container.walk_blobs.return_value = mock_objs - mock_container.list_blobs.return_value = mock_objs - mock_container.download_blob.return_value = mock_objs[0] - mock_svc = mock.MagicMock() - mock_svc.get_container_client.return_value = mock_container - mock_storage.return_value = mock_svc - return mock_storage, mock_container - - -def create_mock_dir(name): - mock_file = mock.MagicMock() - mock_file.name = name - mock_file.is_directory = True - return mock_file - - -def create_mock_file(name): - mock_file = mock.MagicMock() - mock_file.name = name - mock_file.is_directory = False - return mock_file - - -def create_mock_objects_for_file_share(mock_storage, mock_file_items): - mock_share = mock.MagicMock() - mock_share.list_directories_and_files.side_effect = mock_file_items - mock_file = mock.MagicMock() - mock_share.get_file_client.return_value = mock_file - mock_data = mock.MagicMock() - mock_file.download_file.return_value = mock_data - mock_svc = mock.MagicMock() - mock_svc.get_share_client.return_value = mock_share - mock_storage.return_value = mock_svc - return mock_storage, mock_share, mock_data - - -def get_call_args(call_args_list): - arg_list = [] - for call in call_args_list: - args, _ = call - arg_list.append(args) - return arg_list - - -# pylint: disable=protected-access -@pytest.fixture(scope="session", autouse=True) -def test_cleanup(): - yield None - # Will be executed after the last test - shutil.rmtree("some", ignore_errors=True) - shutil.rmtree("dest_path", ignore_errors=True) - - -@mock.patch(STORAGE_MODULE + ".os.makedirs") -@mock.patch("azure.storage.blob.BlobServiceClient") -def test_blob(mock_storage, mock_makedirs): # pylint: disable=unused-argument - - # given - blob_path = "https://kfserving.blob.core.windows.net/triton/simple_string/" - paths = ["simple_string/1/model.graphdef", "simple_string/config.pbtxt"] - mock_blob, mock_container = create_mock_blob(mock_storage, paths) - - # when - Storage._download_azure_blob(blob_path, "dest_path") - - # then - arg_list = get_call_args(mock_container.download_blob.call_args_list) - assert set(arg_list) == set( - [("simple_string/1/model.graphdef",), ("simple_string/config.pbtxt",)] - ) - - mock_storage.assert_called_with( - "https://kfserving.blob.core.windows.net", credential=None - ) - - -@mock.patch(STORAGE_MODULE + ".os.makedirs") -@mock.patch("azure.storage.blob.BlobServiceClient") -def test_blob_file_direct( - mock_storage, mock_makedirs -): # pylint: disable=unused-argument - - # given - blob_path = "https://accountname.blob.core.windows.net/container/somefile.text" - paths = ["somefile.text"] - mock_blob, mock_container = create_mock_blob(mock_storage, paths) - - # when - Storage._download_azure_blob(blob_path, "dest_path") - - # then - arg_list = get_call_args(mock_container.download_blob.call_args_list) - assert arg_list == [("somefile.text",)] - mock_storage.assert_called_with( - "https://accountname.blob.core.windows.net", credential=None - ) - - -@mock.patch(STORAGE_MODULE + ".os.makedirs") -@mock.patch(STORAGE_MODULE + ".Storage._get_azure_storage_token") -@mock.patch("azure.storage.blob.BlobServiceClient") -def test_secure_blob( - mock_storage, mock_get_token, mock_makedirs -): # pylint: disable=unused-argument - - # given - blob_path = "https://kfsecured.blob.core.windows.net/triton/simple_string/" - mock_get_token.return_value = "some_token" - - # when - with pytest.raises(RuntimeError): - Storage._download_azure_blob(blob_path, "dest_path") - - # then - mock_get_token.assert_called() - arg_list = [] - for call in mock_storage.call_args_list: - _, kwargs = call - arg_list.append(kwargs) - assert arg_list == [{"credential": "some_token"}] - - -@mock.patch(STORAGE_MODULE + ".os.makedirs") -@mock.patch("azure.storage.blob.BlobServiceClient") -def test_deep_blob(mock_storage, mock_makedirs): # pylint: disable=unused-argument - - # given - blob_path = ( - "https://accountname.blob.core.windows.net/container/some/deep/blob/path" - ) - paths = ["f1", "f2", "d1/f11", "d1/d2/f21", "d1/d2/d3/f1231", "d4/f41"] - fq_item_paths = ["some/deep/blob/path/" + p for p in paths] - expected_calls = [(f,) for f in fq_item_paths] - - # when - mock_blob, mock_container = create_mock_blob(mock_storage, fq_item_paths) - try: - Storage._download_azure_blob(blob_path, "some/dest/path") - except OSError: # Permissions Error Handling - pass - - # then - actual_calls = get_call_args(mock_container.download_blob.call_args_list) - assert set(actual_calls) == set(expected_calls) - - -@mock.patch(STORAGE_MODULE + ".os.makedirs") -@mock.patch("azure.storage.blob.BlobServiceClient") -def test_blob_file(mock_storage, mock_makedirs): # pylint: disable=unused-argument - - # given - blob_path = "https://accountname.blob.core.windows.net/container/somefile.text" - paths = ["somefile"] - fq_item_paths = paths - expected_calls = [(f,) for f in fq_item_paths] - - # when - mock_blob, mock_container = create_mock_blob(mock_storage, paths) - Storage._download_azure_blob(blob_path, "some/dest/path") - - # then - actual_calls = get_call_args(mock_container.download_blob.call_args_list) - assert actual_calls == expected_calls - - -@mock.patch(STORAGE_MODULE + ".os.makedirs") -@mock.patch("azure.storage.blob.BlobServiceClient") -def test_blob_fq_file(mock_storage, mock_makedirs): # pylint: disable=unused-argument - - # given - blob_path = ( - "https://accountname.blob.core.windows.net/container/folder/somefile.text" - ) - paths = ["somefile"] - fq_item_paths = ["folder/" + p for p in paths] - expected_calls = [(f,) for f in fq_item_paths] - - # when - mock_blob, mock_container = create_mock_blob(mock_storage, fq_item_paths) - Storage._download_azure_blob(blob_path, "some/dest/path") - - # then - actual_calls = get_call_args(mock_container.download_blob.call_args_list) - assert actual_calls == expected_calls - - -@mock.patch(STORAGE_MODULE + ".os.makedirs") -@mock.patch("azure.storage.blob.BlobServiceClient") -def test_blob_no_prefix(mock_storage, mock_makedirs): # pylint: disable=unused-argument - - # given - blob_path = "https://accountname.blob.core.windows.net/container/" - paths = ["somefile.text", "somefolder/somefile.text"] - fq_item_paths = ["" + p for p in paths] - expected_calls = [(f,) for f in fq_item_paths] - - # when - mock_blob, mock_container = create_mock_blob(mock_storage, fq_item_paths) - Storage._download_azure_blob(blob_path, "some/dest/path") - - # then - actual_calls = get_call_args(mock_container.download_blob.call_args_list) - assert set(actual_calls) == set(expected_calls) - - -@mock.patch(STORAGE_MODULE + ".os.makedirs") -@mock.patch(STORAGE_MODULE + ".Storage._get_azure_storage_access_key") -@mock.patch("azure.storage.fileshare.ShareServiceClient") -def test_file_share( - mock_storage, mock_get_access_key, mock_makedirs -): # pylint: disable=unused-argument - - # given - file_share_path = "https://kfserving.file.core.windows.net/triton/simple_string/" - mock_get_access_key.return_value = "some_token" - - mock_file_share, mock_file, mock_data = create_mock_objects_for_file_share( - mock_storage, - [ - [create_mock_dir("1"), create_mock_file("config.pbtxt")], - [create_mock_file("model.graphdef")], - [], - ], - ) - - # when - Storage._download_azure_file_share(file_share_path, "dest_path") - - # then - arg_list = get_call_args(mock_file.get_file_client.call_args_list) - assert set(arg_list) == set( - [("simple_string/1/model.graphdef",), ("simple_string/config.pbtxt",)] - ) - - # then - mock_get_access_key.assert_called() - mock_storage.assert_called_with( - "https://kfserving.file.core.windows.net", credential="some_token" - ) - - -@mock.patch(STORAGE_MODULE + ".os.makedirs") -@mock.patch(STORAGE_MODULE + ".Storage._get_azure_storage_access_key") -@mock.patch("azure.storage.fileshare.ShareServiceClient") -def test_deep_file_share( - mock_storage, mock_get_access_key, mock_makedirs -): # pylint: disable=unused-argument - - file_share_path = ( - "https://accountname.file.core.windows.net/container/some/deep/blob/path" - ) - paths = ["f1", "f2", "d1/f11", "d1/d2/f21", "d1/d2/d3/f1231", "d4/f41"] - fq_item_paths = ["some/deep/blob/path/" + p for p in paths] - expected_calls = [(f,) for f in fq_item_paths] - mock_get_access_key.return_value = "some_token" - - # when - mock_file_share, mock_file, mock_data = create_mock_objects_for_file_share( - mock_storage, - [ - [ - create_mock_dir("d1"), - create_mock_dir("d4"), - create_mock_file("f1"), - create_mock_file("f2"), - ], - [create_mock_file("f41")], - [create_mock_dir("d2"), create_mock_file("f11")], - [create_mock_dir("d3"), create_mock_file("f21")], - [create_mock_file("f1231")], - [], - ], - ) - try: - Storage._download_azure_file_share(file_share_path, "some/dest/path") - except OSError: # Permissions Error Handling - pass - - # then - actual_calls = get_call_args(mock_file.get_file_client.call_args_list) - assert set(actual_calls) == set(expected_calls) - - # then - mock_get_access_key.assert_called() - mock_storage.assert_called_with( - "https://accountname.file.core.windows.net", credential="some_token" - ) - - -@mock.patch(STORAGE_MODULE + ".os.makedirs") -@mock.patch(STORAGE_MODULE + ".Storage._get_azure_storage_access_key") -@mock.patch("azure.storage.fileshare.ShareServiceClient") -def test_file_share_fq_file( - mock_storage, mock_get_access_key, mock_makedirs -): # pylint: disable=unused-argument - - # given - file_share_path = "https://accountname.file.core.windows.net/container/folder/" - paths = ["somefile.text"] - fq_item_paths = ["folder/" + p for p in paths] - expected_calls = [(f,) for f in fq_item_paths] - - # when - mock_get_access_key.return_value = "some_token" - mock_file_share, mock_file, mock_data = create_mock_objects_for_file_share( - mock_storage, [[create_mock_file("somefile.text")], []] - ) - Storage._download_azure_file_share(file_share_path, "some/dest/path") - - # then - actual_calls = get_call_args(mock_file.get_file_client.call_args_list) - assert actual_calls == expected_calls - - # then - mock_get_access_key.assert_called() - mock_storage.assert_called_with( - "https://accountname.file.core.windows.net", credential="some_token" - ) - - -@mock.patch(STORAGE_MODULE + ".os.makedirs") -@mock.patch(STORAGE_MODULE + ".Storage._get_azure_storage_access_key") -@mock.patch("azure.storage.fileshare.ShareServiceClient") -def test_file_share_no_prefix( - mock_storage, mock_get_access_key, mock_makedirs -): # pylint: disable=unused-argument - - # given - file_share_path = "https://accountname.file.core.windows.net/container/" - paths = ["somefile.text", "somefolder/somefile.text"] - fq_item_paths = ["" + p for p in paths] - expected_calls = [(f,) for f in fq_item_paths] - - # when - mock_get_access_key.return_value = "some_token" - mock_file_share, mock_file, mock_data = create_mock_objects_for_file_share( - mock_storage, - [ - [create_mock_dir("somefolder"), create_mock_file("somefile.text")], - [create_mock_file("somefile.text")], - [], - ], - ) - Storage._download_azure_file_share(file_share_path, "some/dest/path") - - # then - arg_list = get_call_args(mock_file.get_file_client.call_args_list) - assert set(arg_list) == set(expected_calls) - - # then - mock_get_access_key.assert_called() - mock_storage.assert_called_with( - "https://accountname.file.core.windows.net", credential="some_token" - ) diff --git a/python/kserve/kserve/storage/test/test_gcs_storage.py b/python/kserve/kserve/storage/test/test_gcs_storage.py deleted file mode 100644 index 0ad09573e1d..00000000000 --- a/python/kserve/kserve/storage/test/test_gcs_storage.py +++ /dev/null @@ -1,158 +0,0 @@ -# Copyright 2024 The KServe Authors. -# -# 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. - -import unittest.mock as mock -import pytest -from kserve.storage import Storage - -STORAGE_MODULE = "kserve.storage.storage" - - -def get_call_args(call_args_list): - arg_list = [] - for call in call_args_list: - args, _ = call - arg_list.append(args) - return arg_list - - -def create_mock_dir(name): - mock_dir = mock.MagicMock() - mock_dir.name = name - return mock_dir - - -def create_mock_dir_with_file(dir_name, file_name): - mock_obj = mock.MagicMock() - mock_obj.name = f"{dir_name}/{file_name}" - return mock_obj - - -@mock.patch("google.cloud.storage.Client") -def test_gcs_with_empty_dir(mock_client): - gcs_path = "gs://foo/bar" - mock_bucket = mock.MagicMock() - - mock_bucket.list_blobs().__iter__.return_value = [create_mock_dir("bar/")] - mock_client.return_value.bucket.return_value = mock_bucket - - with pytest.raises(Exception): - Storage.download(gcs_path) - - -@mock.patch("google.cloud.storage.Client") -def test_mock_gcs(mock_client): - gcs_path = "gs://foo/bar" - mock_root_dir = create_mock_dir("bar/") - mock_file = create_mock_dir_with_file("bar", "mock.object") - - mock_bucket = mock.MagicMock() - mock_bucket.list_blobs().__iter__.return_value = [mock_root_dir, mock_file] - mock_client.return_value.bucket.return_value = mock_bucket - assert Storage.download(gcs_path) - - -@mock.patch("google.cloud.storage.Client") -def test_gcs_with_nested_sub_dir(mock_client): - gcs_path = "gs://foo/bar/test" - - mock_root_dir = create_mock_dir("bar/") - mock_sub_dir = create_mock_dir("test/") - mock_file = create_mock_dir_with_file("test", "mock.object") - - mock_bucket = mock.MagicMock() - mock_bucket.list_blobs().__iter__.return_value = [ - mock_root_dir, - mock_sub_dir, - mock_file, - ] - mock_client.return_value.bucket.return_value = mock_bucket - - Storage.download(gcs_path) - - arg_list = get_call_args(mock_file.download_to_filename.call_args_list) - assert "test/mock.object" in arg_list[0][0] - - -@mock.patch("google.cloud.storage.Client") -def test_download_model_from_gcs(mock_client): - gcs_path = "gs://foo/bar" - - mock_dir = create_mock_dir("bar/") - mock_file = create_mock_dir_with_file("bar", "mock.object") - - mock_bucket = mock.MagicMock() - mock_bucket.list_blobs().__iter__.return_value = [ - mock_dir, - mock_file, - ] - mock_client.return_value.bucket.return_value = mock_bucket - - Storage.download(gcs_path) - - arg_list = get_call_args(mock_file.download_to_filename.call_args_list) - assert "/mock.object" in arg_list[0][0] - - -@mock.patch("google.cloud.storage.Client") -def test_download_model_from_gcs_as_single_file(mock_client): - gcs_path = "gs://foo/bar/mock.object" - mock_file = create_mock_dir_with_file("bar", "mock.object") - - mock_bucket = mock.MagicMock() - mock_bucket.blob.return_value = mock_file - mock_file.exists.return_value = True - mock_client.return_value.bucket.return_value = mock_bucket - - Storage.download(gcs_path) - arg_list = get_call_args(mock_file.download_to_filename.call_args_list) - - assert "/mock.object" in arg_list[0][0] - - -@mock.patch("os.remove") -@mock.patch("os.mkdir") -@mock.patch("zipfile.ZipFile") -@mock.patch("google.cloud.storage.Client") -def test_gcs_model_unpack_archive_file( - mock_client, MockZipFile, mock_create, mock_remove -): - gcs_path = "gs://foo/bar" - output_dir = "test/out_dir" - - mock_dir = create_mock_dir("bar/") - mock_file = create_mock_dir_with_file("bar", "mock.zip") - - # Properly set up the mock zipfile context manager - mock_zipfile = mock.MagicMock() - MockZipFile.return_value = mock_zipfile - mock_zipfile.__enter__.return_value = mock_zipfile - - mock_bucket = mock.MagicMock() - mock_bucket.list_blobs().__iter__.return_value = [ - mock_dir, - mock_file, - ] - mock_client.return_value.bucket.return_value = mock_bucket - - Storage.download(gcs_path, output_dir) - - download_arg_list = get_call_args(mock_file.download_to_filename.call_args_list) - - # Check that extractall was called on the zipfile mock - assert len(download_arg_list) > 0, "download_to_filename was never called" - assert "/mock.zip" in download_arg_list[0][0] - assert mock_zipfile.extractall.called - assert output_dir == mock_zipfile.extractall.call_args[0][0] - assert mock_remove.called diff --git a/python/kserve/kserve/storage/test/test_hf_storage.py b/python/kserve/kserve/storage/test/test_hf_storage.py deleted file mode 100644 index 270a4bec981..00000000000 --- a/python/kserve/kserve/storage/test/test_hf_storage.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright 2023 The KServe Authors. -# -# 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. - -import unittest.mock as mock -import pytest - -from kserve.storage import Storage - - -@mock.patch("huggingface_hub.snapshot_download") -def test_download_model(mock_snapshot_download): - uri = "hf://example.com/model:hash_value" - repo = "example.com" - model = "model" - revision = "hash_value" - - Storage.download(uri) - - mock_snapshot_download.assert_called_once_with( - repo_id=f"{repo}/{model}", - revision=revision, - local_dir=mock.ANY, - ) - - -@mock.patch("huggingface_hub.snapshot_download") -@pytest.mark.parametrize( - "invalid_uri, error_message", - [ - ( - "hf://", - "URI must contain exactly one '/' separating", - ), # Missing repo and model - ( - "hf://repo_only", - "URI must contain exactly one '/' separating", - ), # Missing model - ("hf:///model_only", "Repository name cannot be empty"), # Missing repo - ( - "hf://repo/:hash_value", - "Model name cannot be empty", - ), # Missing model name, hash exists - ], -) -def test_invalid_uri(mock_snapshot_download, invalid_uri, error_message): - with pytest.raises(ValueError, match=error_message): - Storage.download(invalid_uri) - - # Ensure that snapshot_download was never called - mock_snapshot_download.assert_not_called() diff --git a/python/kserve/kserve/storage/test/test_s3_storage.py b/python/kserve/kserve/storage/test/test_s3_storage.py deleted file mode 100644 index 90a7a40c07c..00000000000 --- a/python/kserve/kserve/storage/test/test_s3_storage.py +++ /dev/null @@ -1,607 +0,0 @@ -# Copyright 2021 The KServe Authors. -# -# 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. - -import os -import json -import pytest -import botocore -import tempfile -import unittest.mock as mock - -from botocore.client import Config -from botocore import UNSIGNED -from kserve.storage import Storage - - -def create_mock_obj(path): - mock_obj = mock.MagicMock() - mock_obj.key = path - mock_obj.is_dir = False - return mock_obj - - -def create_mock_boto3_bucket(mock_storage, paths): - mock_s3_resource = mock.MagicMock() - mock_s3_bucket = mock.MagicMock() - mock_s3_bucket.objects.filter.return_value = [create_mock_obj(p) for p in paths] - - mock_s3_resource.Bucket.return_value = mock_s3_bucket - mock_storage.return_value = mock_s3_resource - - return mock_s3_bucket - - -def get_call_args(call_args_list): - arg_list = [] - for call in call_args_list: - args, _ = call - arg_list.append(args) - return arg_list - - -def expected_call_args_list_single_obj(dest, path): - return [(f"{path}".strip("/"), f'{dest}/{path.rsplit("/", 1)[-1]}'.strip("/"))] - - -def expected_call_args_list(parent_key, dest, paths): - return [(f"{parent_key}/{p}".strip("/"), f"{dest}/{p}".strip("/")) for p in paths] - - -# pylint: disable=protected-access - - -@mock.patch("boto3.resource") -def test_parent_key(mock_storage): - # given - bucket_name = "foo" - paths = ["models/weights.pt", "0002.h5", "a/very/long/path/config.json"] - object_paths = ["bar/" + p for p in paths] - - # when - mock_boto3_bucket = create_mock_boto3_bucket(mock_storage, object_paths) - Storage._download_s3(f"s3://{bucket_name}/bar", "dest_path") - - # then - arg_list = get_call_args(mock_boto3_bucket.download_file.call_args_list) - assert arg_list == expected_call_args_list("bar", "dest_path", paths) - - mock_boto3_bucket.objects.filter.assert_called_with(Prefix="bar") - - -@mock.patch("boto3.resource") -def test_no_key(mock_storage): - # given - bucket_name = "foo" - object_paths = ["models/weights.pt", "0002.h5", "a/very/long/path/config.json"] - - # when - mock_boto3_bucket = create_mock_boto3_bucket(mock_storage, object_paths) - Storage._download_s3(f"s3://{bucket_name}/", "dest_path") - - # then - arg_list = get_call_args(mock_boto3_bucket.download_file.call_args_list) - assert arg_list == expected_call_args_list("", "dest_path", object_paths) - - mock_boto3_bucket.objects.filter.assert_called_with(Prefix="") - - -@mock.patch("boto3.resource") -def test_storage_s3_exception(mock_resource): - path = "s3://foo/bar" - # Create mock client - mock_s3_resource = mock.MagicMock() - mock_s3_resource.Bucket.side_effect = Exception() - mock_resource.return_value = mock_s3_resource - - with pytest.raises(Exception): - Storage.download(path) - - -@mock.patch("boto3.resource") -@mock.patch("urllib3.PoolManager") -def test_no_permission_buckets(mock_connection, mock_resource): - bad_s3_path = "s3://random/path" - # Access private buckets without credentials - mock_s3_resource = mock.MagicMock() - mock_s3_bucket = mock.MagicMock() - mock_s3_bucket.objects.filter.return_value = [mock.MagicMock()] - mock_s3_bucket.objects.filter.side_effect = botocore.exceptions.ClientError( - {}, "GetObject" - ) - mock_s3_resource.Bucket.return_value = mock_s3_bucket - mock_resource.return_value = mock_s3_resource - - with pytest.raises(botocore.exceptions.ClientError): - Storage.download(bad_s3_path) - - -@mock.patch("boto3.resource") -def test_full_name_key(mock_storage): - # given - bucket_name = "foo" - object_key = "path/to/model/name.pt" - - # when - mock_boto3_bucket = create_mock_boto3_bucket(mock_storage, [object_key]) - Storage._download_s3(f"s3://{bucket_name}/{object_key}", "dest_path") - - # then - arg_list = get_call_args(mock_boto3_bucket.download_file.call_args_list) - assert arg_list == expected_call_args_list_single_obj("dest_path", object_key) - - mock_boto3_bucket.objects.filter.assert_called_with(Prefix=object_key) - - -@mock.patch("boto3.resource") -def test_full_name_key_root_bucket_dir(mock_resource): - # given - bucket_name = "foo" - object_key = "name.pt" - - # when - mock_boto3_bucket = create_mock_boto3_bucket(mock_resource, [object_key]) - Storage._download_s3(f"s3://{bucket_name}/{object_key}", "dest_path") - - # then - arg_list = get_call_args(mock_boto3_bucket.download_file.call_args_list) - assert arg_list == expected_call_args_list_single_obj("dest_path", object_key) - - mock_boto3_bucket.objects.filter.assert_called_with(Prefix=object_key) - - -AWS_TEST_CREDENTIALS = { - "AWS_ACCESS_KEY_ID": "testing", - "AWS_SECRET_ACCESS_KEY": "testing", - "AWS_SECURITY_TOKEN": "testing", - "AWS_SESSION_TOKEN": "testing", -} - - -@mock.patch("boto3.resource") -def test_multikey(mock_storage): - # given - bucket_name = "foo" - paths = ["b/model.bin"] - object_paths = ["test/a/" + p for p in paths] - - # when - mock_boto3_bucket = create_mock_boto3_bucket(mock_storage, object_paths) - Storage._download_s3(f"s3://{bucket_name}/test/a", "dest_path") - - # then - arg_list = get_call_args(mock_boto3_bucket.download_file.call_args_list) - assert arg_list == expected_call_args_list("test/a", "dest_path", paths) - - mock_boto3_bucket.objects.filter.assert_called_with(Prefix="test/a") - - -@mock.patch("boto3.resource") -def test_files_with_no_extension(mock_storage): - - # given - bucket_name = "foo" - paths = ["churn-pickle", "churn-pickle-logs", "churn-pickle-report"] - object_paths = ["test/" + p for p in paths] - - # when - mock_boto3_bucket = create_mock_boto3_bucket(mock_storage, object_paths) - Storage._download_s3(f"s3://{bucket_name}/test/churn-pickle", "dest_path") - - # then - arg_list = get_call_args(mock_boto3_bucket.download_file.call_args_list) - - # Download only the exact file if found; otherwise, download all files with the given prefix - assert arg_list[0] == expected_call_args_list("test", "dest_path", paths)[0] - - mock_boto3_bucket.objects.filter.assert_called_with(Prefix="test/churn-pickle") - - -def test_get_S3_config(): - DEFAULT_CONFIG = Config() - ANON_CONFIG = Config(signature_version=UNSIGNED) - VIRTUAL_CONFIG = Config(s3={"addressing_style": "virtual"}) - USE_ACCELERATE_CONFIG = Config(s3={"use_accelerate_endpoint": True}) - - with mock.patch.dict(os.environ, {}): - config1 = Storage.get_S3_config() - assert vars(config1) == vars(DEFAULT_CONFIG) - - with mock.patch.dict(os.environ, {"awsAnonymousCredential": "False"}): - config2 = Storage.get_S3_config() - assert vars(config2) == vars(DEFAULT_CONFIG) - - with mock.patch.dict(os.environ, AWS_TEST_CREDENTIALS): - config3 = Storage.get_S3_config() - assert vars(config3) == vars(DEFAULT_CONFIG) - - with mock.patch.dict(os.environ, {"awsAnonymousCredential": "True"}): - config4 = Storage.get_S3_config() - assert config4.signature_version == ANON_CONFIG.signature_version - - # assuming Python 3.5 or greater for joining dictionaries - credentials_and_anon = {**AWS_TEST_CREDENTIALS, "awsAnonymousCredential": "True"} - with mock.patch.dict(os.environ, credentials_and_anon): - config5 = Storage.get_S3_config() - assert config5.signature_version == ANON_CONFIG.signature_version - - with mock.patch.dict(os.environ, {"S3_USER_VIRTUAL_BUCKET": "False"}): - config6 = Storage.get_S3_config() - assert vars(config6) == vars(DEFAULT_CONFIG) - - with mock.patch.dict(os.environ, {"S3_USER_VIRTUAL_BUCKET": "True"}): - config7 = Storage.get_S3_config() - assert config7.s3["addressing_style"] == VIRTUAL_CONFIG.s3["addressing_style"] - - with mock.patch.dict(os.environ, {"S3_USE_ACCELERATE": "False"}): - config6 = Storage.get_S3_config() - assert vars(config6) == vars(DEFAULT_CONFIG) - - with mock.patch.dict(os.environ, {"S3_USE_ACCELERATE": "True"}): - config7 = Storage.get_S3_config() - assert ( - config7.s3["use_accelerate_endpoint"] - == USE_ACCELERATE_CONFIG.s3["use_accelerate_endpoint"] - ) - - # tests legacy endpoint url - with mock.patch.dict( - os.environ, - { - "AWS_ENDPOINT_URL": "https://s3.amazonaws.com", - "AWS_DEFAULT_REGION": "eu-west-1", - }, - ): - config8 = Storage.get_S3_config() - assert config8.s3["addressing_style"] == VIRTUAL_CONFIG.s3["addressing_style"] - - -def test_update_with_storage_spec_s3(monkeypatch): - # save the environment and restore it after the test to avoid mutating it - # since _update_with_storage_spec modifies it - previous_env = os.environ.copy() - - monkeypatch.setenv("STORAGE_CONFIG", '{"type": "s3"}') - Storage._update_with_storage_spec() - - for var in ( - "AWS_ENDPOINT_URL", - "AWS_ACCESS_KEY_ID", - "AWS_SECRET_ACCESS_KEY", - "AWS_DEFAULT_REGION", - "AWS_CA_BUNDLE", - "S3_VERIFY_SSL", - "awsAnonymousCredential", - ): - assert os.getenv(var) is None - - storage_config = { - "access_key_id": "xxxxxxxxxxxxxxxxxxxx", - "bucket": "abucketname", - "default_bucket": "abucketname", - "endpoint_url": "https://s3.us-east-2.amazonaws.com/", - "region": "us-east-2", - "secret_access_key": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", - "type": "s3", - "ca_bundle": "/path/to/cabundle.crt", - "verify_ssl": "false", - "anonymous": "True", - } - - monkeypatch.setenv("STORAGE_CONFIG", json.dumps(storage_config)) - Storage._update_with_storage_spec() - - assert os.getenv("AWS_ENDPOINT_URL") == storage_config["endpoint_url"] - assert os.getenv("AWS_ACCESS_KEY_ID") == storage_config["access_key_id"] - assert os.getenv("AWS_SECRET_ACCESS_KEY") == storage_config["secret_access_key"] - assert os.getenv("AWS_DEFAULT_REGION") == storage_config["region"] - assert os.getenv("AWS_CA_BUNDLE") == storage_config["ca_bundle"] - assert os.getenv("S3_VERIFY_SSL") == storage_config["verify_ssl"] - assert os.getenv("awsAnonymousCredential") == storage_config["anonymous"] - - # revert changes - os.environ = previous_env - - -@mock.patch("boto3.resource") -def test_target_startswith_parent_folder_name(mock_storage): - bucket_name = "foo" - paths = ["model.pkl", "a/model.pkl", "conda.yaml"] - object_paths = ["test/artifacts/model/" + p for p in paths] - - # when - mock_boto3_bucket = create_mock_boto3_bucket(mock_storage, object_paths) - Storage._download_s3(f"s3://{bucket_name}/test/artifacts/model", "dest_path") - - # then - arg_list = get_call_args(mock_boto3_bucket.download_file.call_args_list) - assert ( - arg_list[0] - == expected_call_args_list("test/artifacts/model", "dest_path", paths)[0] - ) - mock_boto3_bucket.objects.filter.assert_called_with(Prefix="test/artifacts/model") - - -@mock.patch("boto3.resource") -def test_file_name_preservation(mock_storage): - # given - bucket_name = "local-model" - paths = ["MLmodel"] - object_paths = ["model/" + p for p in paths] - expected_file_name = "MLmodel" # Expected file name after download - - # when - mock_boto3_bucket = create_mock_boto3_bucket(mock_storage, object_paths) - Storage._download_s3(f"s3://{bucket_name}/model", "dest_path") - - # then - arg_list = get_call_args(mock_boto3_bucket.download_file.call_args_list) - assert len(arg_list) == 1 # Ensure only one file was downloaded - downloaded_source, downloaded_target = arg_list[0] - - # Check if the source S3 key matches the original object key - assert ( - downloaded_source == object_paths[0] - ), f"Expected {object_paths[0]}, got {downloaded_source}" - - # Check if the target file path ends with the expected file name - assert downloaded_target.endswith( - expected_file_name - ), f"Expected file name to end with {expected_file_name}, got {downloaded_target}" - - mock_boto3_bucket.objects.filter.assert_called_with(Prefix="model") - - -@mock.patch("boto3.resource") -def test_target_download_path_and_name(mock_storage): - bucket_name = "foo" - paths = ["model.pkl", "a/model.pkl", "conda.yaml"] - object_paths = ["model/" + p for p in paths] - - # when - mock_boto3_bucket = create_mock_boto3_bucket(mock_storage, object_paths) - Storage._download_s3(f"s3://{bucket_name}/model", "dest_path") - - # then - arg_list = get_call_args(mock_boto3_bucket.download_file.call_args_list) - assert arg_list[0] == expected_call_args_list("model", "dest_path", paths)[0] - assert arg_list[1] == expected_call_args_list("model", "dest_path", paths)[1] - - mock_boto3_bucket.objects.filter.assert_called_with(Prefix="model") - - -@mock.patch("boto3.resource") -def test_ca_bundle_with_aws_ca_bundle_only(mock_storage): - """Test that AWS_CA_BUNDLE can be used independently without CA_BUNDLE_CONFIGMAP_NAME""" - bucket_name = "foo" - object_key = "model.pkl" - - # Create a temporary CA bundle file - with tempfile.NamedTemporaryFile( - mode="w", delete=False, suffix=".crt" - ) as temp_ca_file: - temp_ca_file.write( - "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----" - ) - ca_bundle_path = temp_ca_file.name - - try: - # Mock the boto3 resource and bucket - create_mock_boto3_bucket(mock_storage, [object_key]) - - # Set only AWS_CA_BUNDLE environment variable (no CA_BUNDLE_CONFIGMAP_NAME) - with mock.patch.dict( - os.environ, - {"AWS_CA_BUNDLE": ca_bundle_path, "S3_VERIFY_SSL": "true"}, - clear=True, - ): - Storage._download_s3(f"s3://{bucket_name}/{object_key}", "dest_path") - - # Verify that boto3.resource was called with the correct verify parameter - mock_storage.assert_called_once() - call_args = mock_storage.call_args - assert call_args[1]["verify"] == ca_bundle_path - - finally: - # Clean up the temporary file - os.unlink(ca_bundle_path) - - -@mock.patch("boto3.resource") -def test_ca_bundle_with_configmap_only(mock_storage): - """Test that CA bundle works with ConfigMap when AWS_CA_BUNDLE is not set""" - bucket_name = "foo" - object_key = "model.pkl" - - # Create a temporary CA bundle file in the expected ConfigMap location - with tempfile.TemporaryDirectory() as temp_dir: - ca_bundle_path = os.path.join(temp_dir, "cabundle.crt") - with open(ca_bundle_path, "w") as f: - f.write("-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----") - - # Mock the boto3 resource and bucket - create_mock_boto3_bucket(mock_storage, [object_key]) - - # Set ConfigMap environment variables only - with mock.patch.dict( - os.environ, - { - "CA_BUNDLE_CONFIGMAP_NAME": "test-configmap", - "CA_BUNDLE_VOLUME_MOUNT_POINT": temp_dir, - "S3_VERIFY_SSL": "true", - }, - clear=True, - ): - Storage._download_s3(f"s3://{bucket_name}/{object_key}", "dest_path") - - # Verify that boto3.resource was called with the correct verify parameter - mock_storage.assert_called_once() - call_args = mock_storage.call_args - assert call_args[1]["verify"] == ca_bundle_path - - -@mock.patch("boto3.resource") -def test_ca_bundle_aws_ca_bundle_takes_precedence(mock_storage): - """Test that AWS_CA_BUNDLE takes precedence over ConfigMap when both are set""" - bucket_name = "foo" - object_key = "model.pkl" - - # Create temporary CA bundle files - with tempfile.NamedTemporaryFile( - mode="w", delete=False, suffix=".crt" - ) as aws_ca_file: - aws_ca_file.write( - "-----BEGIN CERTIFICATE-----\naws_ca\n-----END CERTIFICATE-----" - ) - aws_ca_bundle_path = aws_ca_file.name - - with tempfile.TemporaryDirectory() as temp_dir: - configmap_ca_bundle_path = os.path.join(temp_dir, "cabundle.crt") - with open(configmap_ca_bundle_path, "w") as f: - f.write( - "-----BEGIN CERTIFICATE-----\nconfigmap_ca\n-----END CERTIFICATE-----" - ) - - try: - # Mock the boto3 resource and bucket - create_mock_boto3_bucket(mock_storage, [object_key]) - - # Set both AWS_CA_BUNDLE and ConfigMap environment variables - with mock.patch.dict( - os.environ, - { - "AWS_CA_BUNDLE": aws_ca_bundle_path, - "CA_BUNDLE_CONFIGMAP_NAME": "test-configmap", - "CA_BUNDLE_VOLUME_MOUNT_POINT": temp_dir, - "S3_VERIFY_SSL": "true", - }, - clear=True, - ): - Storage._download_s3(f"s3://{bucket_name}/{object_key}", "dest_path") - - # Verify that boto3.resource was called with AWS_CA_BUNDLE path (takes precedence) - mock_storage.assert_called_once() - call_args = mock_storage.call_args - assert call_args[1]["verify"] == aws_ca_bundle_path - - finally: - # Clean up the temporary file - os.unlink(aws_ca_bundle_path) - - -@mock.patch("boto3.resource") -def test_ca_bundle_file_not_found_aws_ca_bundle(mock_storage): - """Test that RuntimeError is raised when AWS_CA_BUNDLE file doesn't exist""" - bucket_name = "foo" - object_key = "model.pkl" - non_existent_path = "/tmp/non_existent_ca_bundle.crt" - - # Mock the boto3 resource and bucket - create_mock_boto3_bucket(mock_storage, [object_key]) - - # Set AWS_CA_BUNDLE to a non-existent file - with mock.patch.dict( - os.environ, - {"AWS_CA_BUNDLE": non_existent_path, "S3_VERIFY_SSL": "true"}, - clear=True, - ): - with pytest.raises( - RuntimeError, - match=f"Failed to find ca bundle file\\({non_existent_path}\\)", - ): - Storage._download_s3(f"s3://{bucket_name}/{object_key}", "dest_path") - - -@mock.patch("boto3.resource") -def test_ca_bundle_file_not_found_configmap(mock_storage): - """Test that RuntimeError is raised when ConfigMap CA bundle file doesn't exist""" - bucket_name = "foo" - object_key = "model.pkl" - - # Mock the boto3 resource and bucket - create_mock_boto3_bucket(mock_storage, [object_key]) - - # Set ConfigMap environment variables with non-existent directory - with mock.patch.dict( - os.environ, - { - "CA_BUNDLE_CONFIGMAP_NAME": "test-configmap", - "CA_BUNDLE_VOLUME_MOUNT_POINT": "/tmp/non_existent_dir", - "S3_VERIFY_SSL": "true", - }, - clear=True, - ): - expected_path = "/tmp/non_existent_dir/cabundle.crt" - with pytest.raises( - RuntimeError, match=f"Failed to find ca bundle file\\({expected_path}\\)" - ): - Storage._download_s3(f"s3://{bucket_name}/{object_key}", "dest_path") - - -@mock.patch("boto3.resource") -def test_ca_bundle_verify_ssl_false_no_ca_bundle_check(mock_storage): - """Test that CA bundle is not checked when S3_VERIFY_SSL is false""" - bucket_name = "foo" - object_key = "model.pkl" - non_existent_path = "/tmp/non_existent_ca_bundle.crt" - - # Mock the boto3 resource and bucket - create_mock_boto3_bucket(mock_storage, [object_key]) - - # Set AWS_CA_BUNDLE to a non-existent file but disable SSL verification - with mock.patch.dict( - os.environ, - {"AWS_CA_BUNDLE": non_existent_path, "S3_VERIFY_SSL": "false"}, - clear=True, - ): - # This should not raise an error because verify_ssl is False - Storage._download_s3(f"s3://{bucket_name}/{object_key}", "dest_path") - - # Verify that boto3.resource was called with verify=False - mock_storage.assert_called_once() - call_args = mock_storage.call_args - assert call_args[1]["verify"] is False - - -@mock.patch("boto3.resource") -def test_ca_bundle_empty_aws_ca_bundle_uses_configmap(mock_storage): - """Test that empty AWS_CA_BUNDLE falls back to ConfigMap""" - bucket_name = "foo" - object_key = "model.pkl" - - # Create a temporary CA bundle file in the expected ConfigMap location - with tempfile.TemporaryDirectory() as temp_dir: - ca_bundle_path = os.path.join(temp_dir, "cabundle.crt") - with open(ca_bundle_path, "w") as f: - f.write("-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----") - - # Mock the boto3 resource and bucket - create_mock_boto3_bucket(mock_storage, [object_key]) - - # Set empty AWS_CA_BUNDLE and ConfigMap environment variables - with mock.patch.dict( - os.environ, - { - "AWS_CA_BUNDLE": "", - "CA_BUNDLE_CONFIGMAP_NAME": "test-configmap", - "CA_BUNDLE_VOLUME_MOUNT_POINT": temp_dir, - "S3_VERIFY_SSL": "true", - }, - clear=True, - ): - Storage._download_s3(f"s3://{bucket_name}/{object_key}", "dest_path") - - # Verify that boto3.resource was called with the ConfigMap path - mock_storage.assert_called_once() - call_args = mock_storage.call_args - assert call_args[1]["verify"] == ca_bundle_path diff --git a/python/kserve/kserve/storage/test/test_storage.py b/python/kserve/kserve/storage/test/test_storage.py deleted file mode 100644 index 921ca18ff46..00000000000 --- a/python/kserve/kserve/storage/test/test_storage.py +++ /dev/null @@ -1,292 +0,0 @@ -# Copyright 2021 The KServe Authors. -# -# 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. - -import io -import os -import tempfile -import binascii -import unittest.mock as mock -import mimetypes -from pathlib import Path - -import pytest - -from kserve.storage import Storage - -STORAGE_MODULE = "kserve.storage.storage" -HTTPS_URI_TARGZ = "https://foo.bar/model.tar.gz" -HTTPS_URI_TARGZ_WITH_QUERY = HTTPS_URI_TARGZ + "?foo=bar" - -# *.tar.gz contains a single empty file model.pth -FILE_TAR_GZ_RAW = binascii.unhexlify( - "1f8b0800bac550600003cbcd4f49cdd12b28c960a01d3030303033315100d1e666a660dac008c28" - "701054313a090a189919981998281a1b1b1a1118382010ddd0407a5c525894540a754656466e464e" - "2560754969686c71ca83fe0f4281805a360140c7200009f7e1bb400060000" -) -# *.zip contains a single empty file model.pth -FILE_ZIP_RAW = binascii.unhexlify( - "504b030414000800080035b67052000000000000000000000000090020006d6f64656c2e70746855540" - "d000786c5506086c5506086c5506075780b000104f501000004140000000300504b07080000000002000" - "00000000000504b0102140314000800080035b6705200000000020000000000000009002000000000000" - "0000000a481000000006d6f64656c2e70746855540d000786c5506086c5506086c5506075780b000104f" - "50100000414000000504b0506000000000100010057000000590000000000" -) - - -def test_storage_local_path(): - abs_path = "file:///" - relative_path = "file://." - assert Storage.download(abs_path) == abs_path.replace("file://", "", 1) - assert Storage.download(relative_path) == relative_path.replace("file://", "", 1) - - -def test_storage_local_path_exception(): - not_exist_path = "file:///some/random/path" - with pytest.raises(Exception): - Storage.download(not_exist_path) - - -def test_no_prefix_local_path(): - abs_path = "/" - relative_path = "." - assert Storage.download(abs_path) == abs_path - assert Storage.download(relative_path) == relative_path - - -def test_local_path_with_out_dir_exist(): - abs_path = "file:///tmp" - out_dir = "/tmp" - assert Storage.download(abs_path, out_dir=out_dir) == out_dir - - -def test_local_path_with_out_dir_not_exist(): - abs_path = "file:///tmp" - out_dir = "/tmp/test-abc" - assert Storage.download(abs_path, out_dir=out_dir) == out_dir - - -class MockHttpResponse(object): - def __init__(self, status_code=404, raw=b"", content_type=""): - self.status_code = status_code - self.raw = io.BytesIO(raw) - self.headers = {"Content-Type": content_type} - - def __enter__(self): - return self - - def __exit__(self, ex_type, ex_val, traceback): - pass - - -@mock.patch( - "requests.get", - return_value=MockHttpResponse( - status_code=200, content_type="application/octet-stream" - ), -) -def test_http_uri_path(_): - http_uri = "http://foo.bar/model.joblib" - http_with_query_uri = "http://foo.bar/model.joblib?foo=bar" - out_dir = "." - assert Storage.download(http_uri, out_dir=out_dir) == out_dir - assert Storage.download(http_with_query_uri, out_dir=out_dir) == out_dir - os.remove("./model.joblib") - - -@mock.patch( - "requests.get", - return_value=MockHttpResponse( - status_code=200, content_type="application/octet-stream" - ), -) -def test_https_uri_path(_): - https_uri = "https://foo.bar/model.joblib" - https_with_query_uri = "https://foo.bar/model.joblib?foo=bar" - out_dir = "." - assert Storage.download(https_uri, out_dir=out_dir) == out_dir - assert Storage.download(https_with_query_uri, out_dir=out_dir) == out_dir - os.remove("./model.joblib") - - -http_uri_path_testparams = [ - ( - HTTPS_URI_TARGZ, - MockHttpResponse(200, FILE_TAR_GZ_RAW, "application/x-tar"), - None, - ), - ( - HTTPS_URI_TARGZ, - MockHttpResponse(200, FILE_TAR_GZ_RAW, "application/x-gtar"), - None, - ), - ( - HTTPS_URI_TARGZ, - MockHttpResponse(200, FILE_TAR_GZ_RAW, "application/x-gzip"), - None, - ), - (HTTPS_URI_TARGZ, MockHttpResponse(200, FILE_TAR_GZ_RAW, "application/gzip"), None), - ( - HTTPS_URI_TARGZ, - MockHttpResponse(200, FILE_TAR_GZ_RAW, "application/zip"), - RuntimeError, - ), - ( - HTTPS_URI_TARGZ_WITH_QUERY, - MockHttpResponse(200, FILE_TAR_GZ_RAW, "application/x-tar"), - None, - ), - ( - HTTPS_URI_TARGZ_WITH_QUERY, - MockHttpResponse(200, FILE_TAR_GZ_RAW, "application/x-gtar"), - None, - ), - ( - HTTPS_URI_TARGZ_WITH_QUERY, - MockHttpResponse(200, FILE_TAR_GZ_RAW, "application/x-gzip"), - None, - ), - ( - HTTPS_URI_TARGZ_WITH_QUERY, - MockHttpResponse(200, FILE_TAR_GZ_RAW, "application/gzip"), - None, - ), - ( - "https://foo.bar/model.zip", - MockHttpResponse(200, FILE_ZIP_RAW, "application/zip"), - None, - ), - ( - "https://foo.bar/model.zip", - MockHttpResponse(200, FILE_ZIP_RAW, "application/x-zip-compressed"), - None, - ), - ( - "https://foo.bar/model.zip", - MockHttpResponse(200, FILE_ZIP_RAW, "application/zip-compressed"), - None, - ), - ( - "https://foo.bar/model.zip?foo=bar", - MockHttpResponse(200, FILE_ZIP_RAW, "application/zip"), - None, - ), - ( - "https://foo.bar/model.zip?foo=bar", - MockHttpResponse(200, FILE_ZIP_RAW, "application/x-zip-compressed"), - None, - ), - ( - "https://foo.bar/model.zip?foo=bar", - MockHttpResponse(200, FILE_ZIP_RAW, "application/zip-compressed"), - None, - ), - ("https://theabyss.net/model.joblib", MockHttpResponse(404), RuntimeError), - ( - "https://some.site.com/test.model", - MockHttpResponse(status_code=200, content_type="text/html"), - RuntimeError, - ), - ("https://foo.bar/test/", MockHttpResponse(200), ValueError), - ( - "https://foo.bar/download?path=/20210530/model.zip", - MockHttpResponse(200, FILE_ZIP_RAW, "application/zip"), - None, - ), - ( - "https://foo.bar/download?path=/20210530/model.zip", - MockHttpResponse(200, FILE_ZIP_RAW, "application/x-zip" "-compressed"), - None, - ), - ( - "https://foo.bar/download?path=/20210530/model.zip", - MockHttpResponse(200, FILE_ZIP_RAW, "application/zip" "-compressed"), - None, - ), -] - - -@pytest.mark.parametrize("uri,response,expected_error", http_uri_path_testparams) -def test_http_uri_paths(uri, response, expected_error): - if expected_error: - - def test(_): - with pytest.raises(expected_error): - Storage.download(uri) - - else: - - def test(_): - with tempfile.TemporaryDirectory() as out_dir: - assert Storage.download(uri, out_dir=out_dir) == out_dir - assert os.path.exists(os.path.join(out_dir, "model.pth")) - - mock.patch("requests.get", return_value=response)(test)() - - -def test_storage_blob_exception(): - blob_path = "https://accountname.blob.core.windows.net/container/some/blob/" - with pytest.raises(Exception): - Storage.download(blob_path) - - -def test_unpack_tar_file(): - out_dir = "." - tar_file = os.path.join(out_dir, "model.tgz") - Path(tar_file).write_bytes(FILE_TAR_GZ_RAW) - mimetype, _ = mimetypes.guess_type(tar_file) - Storage._unpack_archive_file(tar_file, mimetype, out_dir) - assert os.path.exists(os.path.join(out_dir, "model.pth")) - os.remove(os.path.join(out_dir, "model.pth")) - - -def test_unpack_zip_file(): - out_dir = "." - tar_file = os.path.join(out_dir, "model.zip") - Path(tar_file).write_bytes(FILE_ZIP_RAW) - mimetype, _ = mimetypes.guess_type(tar_file) - Storage._unpack_archive_file(tar_file, mimetype, out_dir) - assert os.path.exists(os.path.join(out_dir, "model.pth")) - os.remove(os.path.join(out_dir, "model.pth")) - - -@mock.patch(STORAGE_MODULE + ".Storage._download_azure_blob") -def test_download_azure_blob_called_with_matching_uri(mock_download_azure_blob): - azure_blob_uris = [ - "https://accountname.blob.core.windows.net/container/some/blob/", - "https://accountname.z20.blob.storage.azure.net/container/some/blob/", - "https://accountname.z2.blob.storage.azure.net/container/some/blob/", - ] - - for uri in azure_blob_uris: - Storage.download(uri, out_dir="dest_path") - - expected_calls = [mock.call(uri, "dest_path") for uri in azure_blob_uris] - mock_download_azure_blob.assert_has_calls(expected_calls) - - -@mock.patch(STORAGE_MODULE + ".Storage._download_azure_file_share") -def test_download_azure_file_share_called_with_matching_uri( - mock_download_azure_file_share, -): - azure_file_uris = [ - "https://accountname.file.core.windows.net/container/some/blob", - "https://accountname.z20.file.storage.azure.net/container/some/blob", - "https://accountname.z2.file.storage.azure.net/container/some/blob", - ] - - for uri in azure_file_uris: - Storage.download(uri, out_dir="dest_path") - - expected_calls = [mock.call(uri, "dest_path") for uri in azure_file_uris] - mock_download_azure_file_share.assert_has_calls(expected_calls) diff --git a/python/kserve/pyproject.toml b/python/kserve/pyproject.toml index 5f8ccef2629..656c82e5522 100644 --- a/python/kserve/pyproject.toml +++ b/python/kserve/pyproject.toml @@ -55,13 +55,14 @@ repository = "https://github.com/kserve/kserve/tree/master/python/kserve" [project.optional-dependencies] storage = [ - "requests<3.0.0,>=2.32.2", - "google-cloud-storage<3.0.0,>=2.14.0", - "azure-storage-blob<13.0.0,>=12.20.0", - "azure-storage-file-share<13.0.0,>=12.16.0", - "azure-identity<2.0.0,>=1.15.0", - "boto3<2.0.0,>=1.29.0", - "huggingface-hub[hf-transfer]>=0.32.0", + "kserve-storage==2" +# "requests<3.0.0,>=2.32.2", +# "google-cloud-storage<3.0.0,>=2.14.0", +# "azure-storage-blob<13.0.0,>=12.20.0", +# "azure-storage-file-share<13.0.0,>=12.16.0", +# "azure-identity<2.0.0,>=1.15.0", +# "boto3<2.0.0,>=1.29.0", +# "huggingface-hub[hf-transfer]>=0.32.0", ] logging = [ "asgi-logger<1.0.0,>=0.1.0", @@ -93,6 +94,11 @@ dev = [ "black[colorama]~=24.3.0", ] +[tool.uv] +# TODO remove later +index-url = "https://test.pypi.org/simple/" +extra-index-url = ["https://pypi.org/simple"] + [build-system] requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/python/kserve/uv.lock b/python/kserve/uv.lock index 9e7042c0338..ff0cd5d2b28 100644 --- a/python/kserve/uv.lock +++ b/python/kserve/uv.lock @@ -1665,13 +1665,7 @@ ray = [ { name = "ray", extra = ["serve"] }, ] storage = [ - { name = "azure-identity" }, - { name = "azure-storage-blob" }, - { name = "azure-storage-file-share" }, - { name = "boto3" }, - { name = "google-cloud-storage" }, - { name = "huggingface-hub", extra = ["hf-transfer"] }, - { name = "requests" }, + { name = "kserve-storage" }, ] [package.dev-dependencies] @@ -1696,18 +1690,13 @@ test = [ [package.metadata] requires-dist = [ { name = "asgi-logger", marker = "extra == 'logging'", specifier = ">=0.1.0,<1.0.0" }, - { name = "azure-identity", marker = "extra == 'storage'", specifier = ">=1.15.0,<2.0.0" }, - { name = "azure-storage-blob", marker = "extra == 'storage'", specifier = ">=12.20.0,<13.0.0" }, - { name = "azure-storage-file-share", marker = "extra == 'storage'", specifier = ">=12.16.0,<13.0.0" }, - { name = "boto3", marker = "extra == 'storage'", specifier = ">=1.29.0,<2.0.0" }, { name = "cloudevents", specifier = ">=1.6.2,<2.0.0" }, { name = "fastapi", specifier = ">=0.115.3" }, - { name = "google-cloud-storage", marker = "extra == 'storage'", specifier = ">=2.14.0,<3.0.0" }, { name = "grpc-interceptor", specifier = ">=0.15.4,<1.0.0" }, { name = "grpcio", specifier = ">=1.64.1,<2.0.0" }, { name = "grpcio-tools", specifier = ">=1.64.1,<2.0.0" }, { name = "httpx", specifier = ">=0.27.2,<1.0.0" }, - { name = "huggingface-hub", extras = ["hf-transfer"], marker = "extra == 'storage'", specifier = ">=0.32.0" }, + { name = "kserve-storage", marker = "extra == 'storage'", specifier = "==2" }, { name = "kubernetes", specifier = ">=23.3.0" }, { name = "numpy", specifier = ">=1.26.0,<3.0.0" }, { name = "orjson", specifier = ">=3.9.15,<4.0.0" }, @@ -1719,7 +1708,6 @@ requires-dist = [ { name = "python-dateutil", specifier = ">=2.8.0,<3.0.0" }, { name = "pyyaml", specifier = ">=6.0.0,<7.0.0" }, { name = "ray", extras = ["serve"], marker = "extra == 'ray'", specifier = ">=2.43.0" }, - { name = "requests", marker = "extra == 'storage'", specifier = ">=2.32.2,<3.0.0" }, { name = "six", specifier = ">=1.16.0,<2.0.0" }, { name = "tabulate", specifier = ">=0.9.0,<1.0.0" }, { name = "timing-asgi", specifier = ">=0.3.0,<1.0.0" }, @@ -1746,6 +1734,24 @@ test = [ { name = "tomlkit", specifier = ">=0.12.0,<1.0.0" }, ] +[[package]] +name = "kserve-storage" +version = "2" +source = { registry = "https://test.pypi.org/simple/" } +dependencies = [ + { name = "azure-identity" }, + { name = "azure-storage-blob" }, + { name = "azure-storage-file-share" }, + { name = "boto3" }, + { name = "google-cloud-storage" }, + { name = "huggingface-hub", extra = ["hf-transfer"] }, + { name = "requests" }, +] +sdist = { url = "https://test-files.pythonhosted.org/packages/0e/20/6cbf6c3d0bfc0922eb992078bed1445acb2e8c158f244369843cec447a6c/kserve_storage-2.tar.gz", hash = "sha256:f5b61e494d6fa313fc6ef427e8c449d0e6b25bc50dbd61ee4c01e6c2b1fa13e1", size = 21733, upload-time = "2025-07-28T20:32:46.506Z" } +wheels = [ + { url = "https://test-files.pythonhosted.org/packages/c1/5a/51a3fce2b0f2588abde75f7aa10a3dbf181b40eed27bd130f389eeaec3e1/kserve_storage-2-py3-none-any.whl", hash = "sha256:a65e1cb3d4176c3e5d9dd24f3d2ae98d7581e3856a7abdee74e7fb605af191ad", size = 13202, upload-time = "2025-07-28T20:32:44.779Z" }, +] + [[package]] name = "kubernetes" version = "33.1.0" diff --git a/python/lgb.Dockerfile b/python/lgb.Dockerfile index 61e92166ef3..3e969daeffa 100644 --- a/python/lgb.Dockerfile +++ b/python/lgb.Dockerfile @@ -25,6 +25,13 @@ RUN cd kserve && uv sync --active --no-cache COPY kserve kserve RUN cd kserve && uv sync --active --no-cache +# Copy and install dependencies for kserve-storage using uv +COPY storage/pyproject.toml storage/uv.lock storage/ +RUN cd storage && uv sync --active --no-cache + +COPY storage storage +RUN cd storage && uv pip install . --no-cache + # Install dependencies for lgbserver using uv COPY lgbserver/pyproject.toml lgbserver/uv.lock lgbserver/ RUN cd lgbserver && uv sync --active --no-cache @@ -60,6 +67,7 @@ RUN useradd kserve -m -u 1000 -d /home/kserve COPY --from=builder --chown=kserve:kserve third_party third_party COPY --from=builder --chown=kserve:kserve $VIRTUAL_ENV $VIRTUAL_ENV COPY --from=builder kserve kserve +COPY --from=builder storage storage COPY --from=builder lgbserver lgbserver USER 1000 diff --git a/python/lgbserver/lgbserver/model.py b/python/lgbserver/lgbserver/model.py index b927a8554d8..419b9dd738d 100644 --- a/python/lgbserver/lgbserver/model.py +++ b/python/lgbserver/lgbserver/model.py @@ -20,7 +20,7 @@ from kserve import Model from kserve.errors import InferenceError, ModelMissingError -from kserve.storage import Storage +from kserve_storage import Storage from kserve.protocol.infer_type import InferRequest, InferResponse from kserve.utils.utils import get_predict_input, get_predict_response diff --git a/python/lgbserver/pyproject.toml b/python/lgbserver/pyproject.toml index 59046dc0819..3418a8d828e 100644 --- a/python/lgbserver/pyproject.toml +++ b/python/lgbserver/pyproject.toml @@ -5,7 +5,8 @@ authors = [ license = {text = "Apache-2.0"} requires-python = "<3.13,>=3.9" dependencies = [ - "kserve[storage] @ file:///${PROJECT_ROOT}/../kserve", + "kserve @ file:///${PROJECT_ROOT}/../kserve", + "kserve-storage @ file:///${PROJECT_ROOT}/../storage", "lightgbm[scikit-learn]~=4.5.0", "pandas<3.0.0,>=2.2.0", ] diff --git a/python/lgbserver/uv.lock b/python/lgbserver/uv.lock index fe465edf2d3..f8e4d12dc60 100644 --- a/python/lgbserver/uv.lock +++ b/python/lgbserver/uv.lock @@ -929,32 +929,16 @@ dependencies = [ { name = "uvicorn", extra = ["standard"] }, ] -[package.optional-dependencies] -storage = [ - { name = "azure-identity" }, - { name = "azure-storage-blob" }, - { name = "azure-storage-file-share" }, - { name = "boto3" }, - { name = "google-cloud-storage" }, - { name = "huggingface-hub", extra = ["hf-transfer"] }, - { name = "requests" }, -] - [package.metadata] requires-dist = [ { name = "asgi-logger", marker = "extra == 'logging'", specifier = ">=0.1.0,<1.0.0" }, - { name = "azure-identity", marker = "extra == 'storage'", specifier = ">=1.15.0,<2.0.0" }, - { name = "azure-storage-blob", marker = "extra == 'storage'", specifier = ">=12.20.0,<13.0.0" }, - { name = "azure-storage-file-share", marker = "extra == 'storage'", specifier = ">=12.16.0,<13.0.0" }, - { name = "boto3", marker = "extra == 'storage'", specifier = ">=1.29.0,<2.0.0" }, { name = "cloudevents", specifier = ">=1.6.2,<2.0.0" }, { name = "fastapi", specifier = ">=0.115.3" }, - { name = "google-cloud-storage", marker = "extra == 'storage'", specifier = ">=2.14.0,<3.0.0" }, { name = "grpc-interceptor", specifier = ">=0.15.4,<1.0.0" }, { name = "grpcio", specifier = ">=1.64.1,<2.0.0" }, { name = "grpcio-tools", specifier = ">=1.64.1,<2.0.0" }, { name = "httpx", specifier = ">=0.27.2,<1.0.0" }, - { name = "huggingface-hub", extras = ["hf-transfer"], marker = "extra == 'storage'", specifier = ">=0.32.0" }, + { name = "kserve-storage", marker = "extra == 'storage'", specifier = "==2" }, { name = "kubernetes", specifier = ">=23.3.0" }, { name = "numpy", specifier = ">=1.26.0,<3.0.0" }, { name = "orjson", specifier = ">=3.9.15,<4.0.0" }, @@ -966,7 +950,6 @@ requires-dist = [ { name = "python-dateutil", specifier = ">=2.8.0,<3.0.0" }, { name = "pyyaml", specifier = ">=6.0.0,<7.0.0" }, { name = "ray", extras = ["serve"], marker = "extra == 'ray'", specifier = ">=2.43.0" }, - { name = "requests", marker = "extra == 'storage'", specifier = ">=2.32.2,<3.0.0" }, { name = "six", specifier = ">=1.16.0,<2.0.0" }, { name = "tabulate", specifier = ">=0.9.0,<1.0.0" }, { name = "timing-asgi", specifier = ">=0.3.0,<1.0.0" }, @@ -993,6 +976,31 @@ test = [ { name = "tomlkit", specifier = ">=0.12.0,<1.0.0" }, ] +[[package]] +name = "kserve-storage" +version = "0.15.2" +source = { directory = "../storage" } +dependencies = [ + { name = "azure-identity" }, + { name = "azure-storage-blob" }, + { name = "azure-storage-file-share" }, + { name = "boto3" }, + { name = "google-cloud-storage" }, + { name = "huggingface-hub", extra = ["hf-transfer"] }, + { name = "requests" }, +] + +[package.metadata] +requires-dist = [ + { name = "azure-identity", specifier = ">=1.15.0,<2.0.0" }, + { name = "azure-storage-blob", specifier = ">=12.20.0,<13.0.0" }, + { name = "azure-storage-file-share", specifier = ">=12.16.0,<13.0.0" }, + { name = "boto3", specifier = ">=1.29.0,<2.0.0" }, + { name = "google-cloud-storage", specifier = ">=2.14.0,<3.0.0" }, + { name = "huggingface-hub", extras = ["hf-transfer"], specifier = ">=0.32.0" }, + { name = "requests", specifier = ">=2.32.2,<3.0.0" }, +] + [[package]] name = "kubernetes" version = "32.0.1" @@ -1021,7 +1029,8 @@ name = "lgbserver" version = "0.15.2" source = { virtual = "." } dependencies = [ - { name = "kserve", extra = ["storage"] }, + { name = "kserve" }, + { name = "kserve-storage" }, { name = "lightgbm", extra = ["scikit-learn"] }, { name = "pandas" }, ] @@ -1039,7 +1048,8 @@ test = [ [package.metadata] requires-dist = [ - { name = "kserve", extras = ["storage"], directory = "../kserve" }, + { name = "kserve", directory = "../kserve" }, + { name = "kserve-storage", directory = "../storage" }, { name = "lightgbm", extras = ["scikit-learn"], specifier = "~=4.5.0" }, { name = "pandas", specifier = ">=2.2.0,<3.0.0" }, ] diff --git a/python/paddle.Dockerfile b/python/paddle.Dockerfile index 375930d1dd4..85b85e38342 100644 --- a/python/paddle.Dockerfile +++ b/python/paddle.Dockerfile @@ -27,6 +27,13 @@ RUN cd kserve && uv sync --active --no-cache COPY kserve kserve RUN cd kserve && uv sync --active --no-cache +# Copy and install dependencies for kserve-storage using uv +COPY storage/pyproject.toml storage/uv.lock storage/ +RUN cd storage && uv sync --active --no-cache + +COPY storage storage +RUN cd storage && uv pip install . --no-cache + # Install paddleserver dependencies using uv COPY paddleserver/pyproject.toml paddleserver/uv.lock paddleserver/ RUN cd paddleserver && uv sync --active --no-cache @@ -60,6 +67,7 @@ RUN useradd kserve -m -u 1000 -d /home/kserve COPY --from=builder --chown=kserve:kserve third_party third_party COPY --from=builder --chown=kserve:kserve $VIRTUAL_ENV $VIRTUAL_ENV COPY --from=builder kserve kserve +COPY --from=builder storage storage COPY --from=builder paddleserver paddleserver USER 1000 diff --git a/python/paddleserver/paddleserver/model.py b/python/paddleserver/paddleserver/model.py index 2923ea4f0dc..f7ca6ce4fef 100644 --- a/python/paddleserver/paddleserver/model.py +++ b/python/paddleserver/paddleserver/model.py @@ -18,7 +18,7 @@ from paddle import inference from kserve import Model from kserve.errors import InferenceError -from kserve.storage import Storage +from kserve_storage import Storage from typing import Dict, Union from kserve.protocol.infer_type import InferRequest, InferResponse diff --git a/python/paddleserver/pyproject.toml b/python/paddleserver/pyproject.toml index ad4eb034efa..6e75c72d345 100644 --- a/python/paddleserver/pyproject.toml +++ b/python/paddleserver/pyproject.toml @@ -5,7 +5,8 @@ authors = [ license = {text = "Apache-2.0"} requires-python = "<3.13,>=3.9" dependencies = [ - "kserve[storage] @ file:///${PROJECT_ROOT}/../kserve", + "kserve @ file:///${PROJECT_ROOT}/../kserve", + "kserve-storage @ file:///${PROJECT_ROOT}/../storage", "paddlepaddle>=2.6.1,<=3.0.0; platform_system != 'Windows'", "setuptools<71.0.0,>=70.0.0", ] diff --git a/python/paddleserver/uv.lock b/python/paddleserver/uv.lock index 22e64413baa..16afd5a1112 100644 --- a/python/paddleserver/uv.lock +++ b/python/paddleserver/uv.lock @@ -946,32 +946,16 @@ dependencies = [ { name = "uvicorn", extra = ["standard"] }, ] -[package.optional-dependencies] -storage = [ - { name = "azure-identity" }, - { name = "azure-storage-blob" }, - { name = "azure-storage-file-share" }, - { name = "boto3" }, - { name = "google-cloud-storage" }, - { name = "huggingface-hub", extra = ["hf-transfer"] }, - { name = "requests" }, -] - [package.metadata] requires-dist = [ { name = "asgi-logger", marker = "extra == 'logging'", specifier = ">=0.1.0,<1.0.0" }, - { name = "azure-identity", marker = "extra == 'storage'", specifier = ">=1.15.0,<2.0.0" }, - { name = "azure-storage-blob", marker = "extra == 'storage'", specifier = ">=12.20.0,<13.0.0" }, - { name = "azure-storage-file-share", marker = "extra == 'storage'", specifier = ">=12.16.0,<13.0.0" }, - { name = "boto3", marker = "extra == 'storage'", specifier = ">=1.29.0,<2.0.0" }, { name = "cloudevents", specifier = ">=1.6.2,<2.0.0" }, { name = "fastapi", specifier = ">=0.115.3" }, - { name = "google-cloud-storage", marker = "extra == 'storage'", specifier = ">=2.14.0,<3.0.0" }, { name = "grpc-interceptor", specifier = ">=0.15.4,<1.0.0" }, { name = "grpcio", specifier = ">=1.64.1,<2.0.0" }, { name = "grpcio-tools", specifier = ">=1.64.1,<2.0.0" }, { name = "httpx", specifier = ">=0.27.2,<1.0.0" }, - { name = "huggingface-hub", extras = ["hf-transfer"], marker = "extra == 'storage'", specifier = ">=0.32.0" }, + { name = "kserve-storage", marker = "extra == 'storage'", specifier = "==2" }, { name = "kubernetes", specifier = ">=23.3.0" }, { name = "numpy", specifier = ">=1.26.0,<3.0.0" }, { name = "orjson", specifier = ">=3.9.15,<4.0.0" }, @@ -983,7 +967,6 @@ requires-dist = [ { name = "python-dateutil", specifier = ">=2.8.0,<3.0.0" }, { name = "pyyaml", specifier = ">=6.0.0,<7.0.0" }, { name = "ray", extras = ["serve"], marker = "extra == 'ray'", specifier = ">=2.43.0" }, - { name = "requests", marker = "extra == 'storage'", specifier = ">=2.32.2,<3.0.0" }, { name = "six", specifier = ">=1.16.0,<2.0.0" }, { name = "tabulate", specifier = ">=0.9.0,<1.0.0" }, { name = "timing-asgi", specifier = ">=0.3.0,<1.0.0" }, @@ -1010,6 +993,31 @@ test = [ { name = "tomlkit", specifier = ">=0.12.0,<1.0.0" }, ] +[[package]] +name = "kserve-storage" +version = "0.15.2" +source = { directory = "../storage" } +dependencies = [ + { name = "azure-identity" }, + { name = "azure-storage-blob" }, + { name = "azure-storage-file-share" }, + { name = "boto3" }, + { name = "google-cloud-storage" }, + { name = "huggingface-hub", extra = ["hf-transfer"] }, + { name = "requests" }, +] + +[package.metadata] +requires-dist = [ + { name = "azure-identity", specifier = ">=1.15.0,<2.0.0" }, + { name = "azure-storage-blob", specifier = ">=12.20.0,<13.0.0" }, + { name = "azure-storage-file-share", specifier = ">=12.16.0,<13.0.0" }, + { name = "boto3", specifier = ">=1.29.0,<2.0.0" }, + { name = "google-cloud-storage", specifier = ">=2.14.0,<3.0.0" }, + { name = "huggingface-hub", extras = ["hf-transfer"], specifier = ">=0.32.0" }, + { name = "requests", specifier = ">=2.32.2,<3.0.0" }, +] + [[package]] name = "kubernetes" version = "32.0.1" @@ -1395,7 +1403,8 @@ name = "paddleserver" version = "0.15.2" source = { virtual = "." } dependencies = [ - { name = "kserve", extra = ["storage"] }, + { name = "kserve" }, + { name = "kserve-storage" }, { name = "paddlepaddle", marker = "sys_platform != 'win32'" }, { name = "setuptools" }, ] @@ -1413,7 +1422,8 @@ test = [ [package.metadata] requires-dist = [ - { name = "kserve", extras = ["storage"], directory = "../kserve" }, + { name = "kserve", directory = "../kserve" }, + { name = "kserve-storage", directory = "../storage" }, { name = "paddlepaddle", marker = "sys_platform != 'win32'", specifier = ">=2.6.1,<=3.0.0" }, { name = "setuptools", specifier = ">=70.0.0,<71.0.0" }, ] diff --git a/python/pmml.Dockerfile b/python/pmml.Dockerfile index e1feb7937c8..31cce3d7c06 100644 --- a/python/pmml.Dockerfile +++ b/python/pmml.Dockerfile @@ -35,6 +35,13 @@ RUN cd kserve && uv sync --active --no-cache COPY kserve kserve RUN cd kserve && uv sync --active --no-cache +# Copy and install dependencies for kserve-storage using uv +COPY storage/pyproject.toml storage/uv.lock storage/ +RUN cd storage && uv sync --active --no-cache + +COPY storage storage +RUN cd storage && uv pip install . --no-cache + # Install dependencies for pmmlserver using uv COPY pmmlserver/pyproject.toml pmmlserver/uv.lock pmmlserver/ RUN cd pmmlserver && uv sync --active --no-cache @@ -71,6 +78,7 @@ RUN useradd kserve -m -u 1000 -d /home/kserve COPY --from=builder --chown=kserve:kserve third_party third_party COPY --from=builder --chown=kserve:kserve $VIRTUAL_ENV $VIRTUAL_ENV COPY --from=builder kserve kserve +COPY --from=builder storage storage COPY --from=builder pmmlserver pmmlserver USER 1000 diff --git a/python/pmmlserver/pmmlserver/model.py b/python/pmmlserver/pmmlserver/model.py index 67aa2c59107..6c7bbf5632e 100644 --- a/python/pmmlserver/pmmlserver/model.py +++ b/python/pmmlserver/pmmlserver/model.py @@ -19,7 +19,7 @@ from jpmml_evaluator import make_evaluator from jpmml_evaluator.py4j import Py4JBackend from kserve.errors import ModelMissingError, InferenceError -from kserve.storage import Storage +from kserve_storage import Storage from kserve import Model from kserve.utils.utils import get_predict_input, get_predict_response from kserve.protocol.infer_type import InferRequest, InferResponse diff --git a/python/pmmlserver/pyproject.toml b/python/pmmlserver/pyproject.toml index bdfacabbece..0e6b7160a07 100644 --- a/python/pmmlserver/pyproject.toml +++ b/python/pmmlserver/pyproject.toml @@ -5,7 +5,8 @@ authors = [ license = {text = "Apache-2.0"} requires-python = "<3.13,>=3.9" dependencies = [ - "kserve[storage] @ file:///${PROJECT_ROOT}/../kserve", + "kserve @ file:///${PROJECT_ROOT}/../kserve", + "kserve-storage @ file:///${PROJECT_ROOT}/../storage", "jpmml-evaluator~=0.10.3", "setuptools<71.0.0,>=70.0.0", ] diff --git a/python/pmmlserver/uv.lock b/python/pmmlserver/uv.lock index 2042aa70502..f2da65e3bff 100644 --- a/python/pmmlserver/uv.lock +++ b/python/pmmlserver/uv.lock @@ -963,32 +963,16 @@ dependencies = [ { name = "uvicorn", extra = ["standard"] }, ] -[package.optional-dependencies] -storage = [ - { name = "azure-identity" }, - { name = "azure-storage-blob" }, - { name = "azure-storage-file-share" }, - { name = "boto3" }, - { name = "google-cloud-storage" }, - { name = "huggingface-hub", extra = ["hf-transfer"] }, - { name = "requests" }, -] - [package.metadata] requires-dist = [ { name = "asgi-logger", marker = "extra == 'logging'", specifier = ">=0.1.0,<1.0.0" }, - { name = "azure-identity", marker = "extra == 'storage'", specifier = ">=1.15.0,<2.0.0" }, - { name = "azure-storage-blob", marker = "extra == 'storage'", specifier = ">=12.20.0,<13.0.0" }, - { name = "azure-storage-file-share", marker = "extra == 'storage'", specifier = ">=12.16.0,<13.0.0" }, - { name = "boto3", marker = "extra == 'storage'", specifier = ">=1.29.0,<2.0.0" }, { name = "cloudevents", specifier = ">=1.6.2,<2.0.0" }, { name = "fastapi", specifier = ">=0.115.3" }, - { name = "google-cloud-storage", marker = "extra == 'storage'", specifier = ">=2.14.0,<3.0.0" }, { name = "grpc-interceptor", specifier = ">=0.15.4,<1.0.0" }, { name = "grpcio", specifier = ">=1.64.1,<2.0.0" }, { name = "grpcio-tools", specifier = ">=1.64.1,<2.0.0" }, { name = "httpx", specifier = ">=0.27.2,<1.0.0" }, - { name = "huggingface-hub", extras = ["hf-transfer"], marker = "extra == 'storage'", specifier = ">=0.32.0" }, + { name = "kserve-storage", marker = "extra == 'storage'", specifier = "==2" }, { name = "kubernetes", specifier = ">=23.3.0" }, { name = "numpy", specifier = ">=1.26.0,<3.0.0" }, { name = "orjson", specifier = ">=3.9.15,<4.0.0" }, @@ -1000,7 +984,6 @@ requires-dist = [ { name = "python-dateutil", specifier = ">=2.8.0,<3.0.0" }, { name = "pyyaml", specifier = ">=6.0.0,<7.0.0" }, { name = "ray", extras = ["serve"], marker = "extra == 'ray'", specifier = ">=2.43.0" }, - { name = "requests", marker = "extra == 'storage'", specifier = ">=2.32.2,<3.0.0" }, { name = "six", specifier = ">=1.16.0,<2.0.0" }, { name = "tabulate", specifier = ">=0.9.0,<1.0.0" }, { name = "timing-asgi", specifier = ">=0.3.0,<1.0.0" }, @@ -1027,6 +1010,31 @@ test = [ { name = "tomlkit", specifier = ">=0.12.0,<1.0.0" }, ] +[[package]] +name = "kserve-storage" +version = "0.15.2" +source = { directory = "../storage" } +dependencies = [ + { name = "azure-identity" }, + { name = "azure-storage-blob" }, + { name = "azure-storage-file-share" }, + { name = "boto3" }, + { name = "google-cloud-storage" }, + { name = "huggingface-hub", extra = ["hf-transfer"] }, + { name = "requests" }, +] + +[package.metadata] +requires-dist = [ + { name = "azure-identity", specifier = ">=1.15.0,<2.0.0" }, + { name = "azure-storage-blob", specifier = ">=12.20.0,<13.0.0" }, + { name = "azure-storage-file-share", specifier = ">=12.16.0,<13.0.0" }, + { name = "boto3", specifier = ">=1.29.0,<2.0.0" }, + { name = "google-cloud-storage", specifier = ">=2.14.0,<3.0.0" }, + { name = "huggingface-hub", extras = ["hf-transfer"], specifier = ">=0.32.0" }, + { name = "requests", specifier = ">=2.32.2,<3.0.0" }, +] + [[package]] name = "kubernetes" version = "32.0.1" @@ -1374,7 +1382,8 @@ version = "0.15.2" source = { virtual = "." } dependencies = [ { name = "jpmml-evaluator" }, - { name = "kserve", extra = ["storage"] }, + { name = "kserve" }, + { name = "kserve-storage" }, { name = "setuptools" }, ] @@ -1392,7 +1401,8 @@ test = [ [package.metadata] requires-dist = [ { name = "jpmml-evaluator", specifier = "~=0.10.3" }, - { name = "kserve", extras = ["storage"], directory = "../kserve" }, + { name = "kserve", directory = "../kserve" }, + { name = "kserve-storage", directory = "../storage" }, { name = "setuptools", specifier = ">=70.0.0,<71.0.0" }, ] diff --git a/python/sklearn.Dockerfile b/python/sklearn.Dockerfile index 44c6f48196b..fed2f8f8f2b 100644 --- a/python/sklearn.Dockerfile +++ b/python/sklearn.Dockerfile @@ -25,6 +25,13 @@ RUN cd kserve && uv sync --active --no-cache COPY kserve kserve RUN cd kserve && uv sync --active --no-cache +# ========== Install kserve storage dependencies ========== +COPY storage/pyproject.toml storage/uv.lock storage/ +RUN cd storage && uv sync --active --no-cache + +COPY storage storage +RUN cd storage && uv pip install . --no-cache + # ========== Install sklearnserver dependencies ========== COPY sklearnserver/pyproject.toml sklearnserver/uv.lock sklearnserver/ RUN cd sklearnserver && uv sync --active --no-cache @@ -55,6 +62,7 @@ RUN useradd kserve -m -u 1000 -d /home/kserve COPY --from=builder --chown=kserve:kserve third_party third_party COPY --from=builder --chown=kserve:kserve $VIRTUAL_ENV $VIRTUAL_ENV COPY --from=builder kserve kserve +COPY --from=builder storage storage COPY --from=builder sklearnserver sklearnserver USER 1000 diff --git a/python/sklearnserver/pyproject.toml b/python/sklearnserver/pyproject.toml index 73f81f4236b..6d345c4a7d2 100644 --- a/python/sklearnserver/pyproject.toml +++ b/python/sklearnserver/pyproject.toml @@ -5,7 +5,8 @@ authors = [ license = {text = "Apache-2.0"} requires-python = "<3.13,>=3.9" dependencies = [ - "kserve[storage] @ file:///${PROJECT_ROOT}/../kserve", + "kserve @ file:///${PROJECT_ROOT}/../kserve", + "kserve-storage @ file:///${PROJECT_ROOT}/../storage", "scikit-learn~=1.5.1", "joblib<2.0.0,>=1.4.0", "pandas<3.0.0,>=2.2.0", diff --git a/python/sklearnserver/sklearnserver/model.py b/python/sklearnserver/sklearnserver/model.py index c0d189cd015..6603997c169 100644 --- a/python/sklearnserver/sklearnserver/model.py +++ b/python/sklearnserver/sklearnserver/model.py @@ -17,7 +17,7 @@ from typing import Dict, Union from kserve.errors import InferenceError, ModelMissingError -from kserve.storage import Storage +from kserve_storage import Storage import joblib from kserve.protocol.infer_type import InferRequest, InferResponse diff --git a/python/sklearnserver/uv.lock b/python/sklearnserver/uv.lock index 32956ddc2ee..f7f26cb2d7b 100644 --- a/python/sklearnserver/uv.lock +++ b/python/sklearnserver/uv.lock @@ -929,32 +929,16 @@ dependencies = [ { name = "uvicorn", extra = ["standard"] }, ] -[package.optional-dependencies] -storage = [ - { name = "azure-identity" }, - { name = "azure-storage-blob" }, - { name = "azure-storage-file-share" }, - { name = "boto3" }, - { name = "google-cloud-storage" }, - { name = "huggingface-hub", extra = ["hf-transfer"] }, - { name = "requests" }, -] - [package.metadata] requires-dist = [ { name = "asgi-logger", marker = "extra == 'logging'", specifier = ">=0.1.0,<1.0.0" }, - { name = "azure-identity", marker = "extra == 'storage'", specifier = ">=1.15.0,<2.0.0" }, - { name = "azure-storage-blob", marker = "extra == 'storage'", specifier = ">=12.20.0,<13.0.0" }, - { name = "azure-storage-file-share", marker = "extra == 'storage'", specifier = ">=12.16.0,<13.0.0" }, - { name = "boto3", marker = "extra == 'storage'", specifier = ">=1.29.0,<2.0.0" }, { name = "cloudevents", specifier = ">=1.6.2,<2.0.0" }, { name = "fastapi", specifier = ">=0.115.3" }, - { name = "google-cloud-storage", marker = "extra == 'storage'", specifier = ">=2.14.0,<3.0.0" }, { name = "grpc-interceptor", specifier = ">=0.15.4,<1.0.0" }, { name = "grpcio", specifier = ">=1.64.1,<2.0.0" }, { name = "grpcio-tools", specifier = ">=1.64.1,<2.0.0" }, { name = "httpx", specifier = ">=0.27.2,<1.0.0" }, - { name = "huggingface-hub", extras = ["hf-transfer"], marker = "extra == 'storage'", specifier = ">=0.32.0" }, + { name = "kserve-storage", marker = "extra == 'storage'", specifier = "==2" }, { name = "kubernetes", specifier = ">=23.3.0" }, { name = "numpy", specifier = ">=1.26.0,<3.0.0" }, { name = "orjson", specifier = ">=3.9.15,<4.0.0" }, @@ -966,7 +950,6 @@ requires-dist = [ { name = "python-dateutil", specifier = ">=2.8.0,<3.0.0" }, { name = "pyyaml", specifier = ">=6.0.0,<7.0.0" }, { name = "ray", extras = ["serve"], marker = "extra == 'ray'", specifier = ">=2.43.0" }, - { name = "requests", marker = "extra == 'storage'", specifier = ">=2.32.2,<3.0.0" }, { name = "six", specifier = ">=1.16.0,<2.0.0" }, { name = "tabulate", specifier = ">=0.9.0,<1.0.0" }, { name = "timing-asgi", specifier = ">=0.3.0,<1.0.0" }, @@ -993,6 +976,31 @@ test = [ { name = "tomlkit", specifier = ">=0.12.0,<1.0.0" }, ] +[[package]] +name = "kserve-storage" +version = "0.15.2" +source = { directory = "../storage" } +dependencies = [ + { name = "azure-identity" }, + { name = "azure-storage-blob" }, + { name = "azure-storage-file-share" }, + { name = "boto3" }, + { name = "google-cloud-storage" }, + { name = "huggingface-hub", extra = ["hf-transfer"] }, + { name = "requests" }, +] + +[package.metadata] +requires-dist = [ + { name = "azure-identity", specifier = ">=1.15.0,<2.0.0" }, + { name = "azure-storage-blob", specifier = ">=12.20.0,<13.0.0" }, + { name = "azure-storage-file-share", specifier = ">=12.16.0,<13.0.0" }, + { name = "boto3", specifier = ">=1.29.0,<2.0.0" }, + { name = "google-cloud-storage", specifier = ">=2.14.0,<3.0.0" }, + { name = "huggingface-hub", extras = ["hf-transfer"], specifier = ">=0.32.0" }, + { name = "requests", specifier = ">=2.32.2,<3.0.0" }, +] + [[package]] name = "kubernetes" version = "32.0.1" @@ -1846,7 +1854,8 @@ version = "0.15.2" source = { virtual = "." } dependencies = [ { name = "joblib" }, - { name = "kserve", extra = ["storage"] }, + { name = "kserve" }, + { name = "kserve-storage" }, { name = "pandas" }, { name = "scikit-learn" }, ] @@ -1865,7 +1874,8 @@ test = [ [package.metadata] requires-dist = [ { name = "joblib", specifier = ">=1.4.0,<2.0.0" }, - { name = "kserve", extras = ["storage"], directory = "../kserve" }, + { name = "kserve", directory = "../kserve" }, + { name = "kserve-storage", directory = "../storage" }, { name = "pandas", specifier = ">=2.2.0,<3.0.0" }, { name = "scikit-learn", specifier = "~=1.5.1" }, ] diff --git a/python/storage-initializer.Dockerfile b/python/storage-initializer.Dockerfile index 4d4588f5b9b..141a4bf545b 100644 --- a/python/storage-initializer.Dockerfile +++ b/python/storage-initializer.Dockerfile @@ -32,11 +32,11 @@ RUN uv venv $VIRTUAL_ENV ENV PATH="$VIRTUAL_ENV/bin:$PATH" # Install Python dependencies -COPY kserve/pyproject.toml kserve/uv.lock kserve/ -RUN cd kserve && uv sync --extra storage --active --no-cache +COPY storage/pyproject.toml storage/uv.lock storage/ +RUN cd storage && uv sync --active --no-cache -COPY kserve kserve -RUN cd kserve && uv sync --extra storage --active --no-cache +COPY storage storage +RUN cd storage && uv pip install . --no-cache # Install Kerberos-related packages RUN uv pip install --no-cache-dir krbcontext==0.10 hdfs~=2.6.0 requests-kerberos==0.14.0 @@ -65,7 +65,7 @@ RUN useradd kserve -m -u 1000 -d /home/kserve COPY --from=builder --chown=kserve:kserve third_party third_party COPY --from=builder --chown=kserve:kserve $VIRTUAL_ENV $VIRTUAL_ENV -COPY --from=builder kserve kserve +COPY --from=builder storage storage COPY ./storage-initializer /storage-initializer RUN chmod +x /storage-initializer/scripts/initializer-entrypoint diff --git a/python/storage-initializer/scripts/initializer-entrypoint b/python/storage-initializer/scripts/initializer-entrypoint index 270304e306f..2d3f6450270 100644 --- a/python/storage-initializer/scripts/initializer-entrypoint +++ b/python/storage-initializer/scripts/initializer-entrypoint @@ -1,8 +1,8 @@ #!/usr/bin/env python3 import sys -from kserve.storage import Storage -from kserve.logging import configure_logging, logger +from kserve_storage import Storage +from kserve_storage.logging import configure_logging, logger configure_logging() diff --git a/python/storage/kserve_storage/kserve_storage.py b/python/storage/kserve_storage/kserve_storage.py index 2117d0e5ebe..a60f779c1a7 100644 --- a/python/storage/kserve_storage/kserve_storage.py +++ b/python/storage/kserve_storage/kserve_storage.py @@ -29,7 +29,7 @@ from urllib.parse import urlparse import requests -from .logging import logger +from kserve_storage.logging import logger MODEL_MOUNT_DIRS = "/mnt/models" @@ -103,8 +103,8 @@ def download(uri: str, out_dir: str = None) -> str: raise Exception( "Cannot recognize storage type for " + uri - + "\n'%s', '%s', '%s', and '%s' are the current available storage type." - % (_GCS_PREFIX, _S3_PREFIX, _LOCAL_PREFIX, _HTTP_PREFIX) + + "\n'%s', '%s', '%s', '%s' and '%s' are the current available storage type." + % (_GCS_PREFIX, _S3_PREFIX, _LOCAL_PREFIX, _HTTP_PREFIX, _HF_PREFIX) ) logger.info("Successfully copied %s to %s", uri, out_dir) @@ -211,19 +211,26 @@ def _download_s3(uri, temp_dir: str) -> str: verify_ssl = True # If verify_ssl is true, then check there is custom ca bundle cert + # The CA bundle can be any local file in the container under the path + # set in the AWS_CA_BUNDLE environment variable. + # It can also be coming from a ConfigMap, in which case the filename + # is cabundle.crt. if verify_ssl: global_ca_bundle_configmap = os.getenv("CA_BUNDLE_CONFIGMAP_NAME") - if global_ca_bundle_configmap: - isvc_aws_ca_bundle_path = os.getenv("AWS_CA_BUNDLE") - if isvc_aws_ca_bundle_path and isvc_aws_ca_bundle_path != "": - ca_bundle_full_path = isvc_aws_ca_bundle_path - else: - global_ca_bundle_volume_mount_path = os.getenv( - "CA_BUNDLE_VOLUME_MOUNT_POINT" - ) - ca_bundle_full_path = os.path.join( - global_ca_bundle_volume_mount_path, "cabundle.crt" - ) + isvc_aws_ca_bundle_path = os.getenv("AWS_CA_BUNDLE") + ca_bundle_set = False + if isvc_aws_ca_bundle_path and isvc_aws_ca_bundle_path != "": + ca_bundle_set = True + ca_bundle_full_path = isvc_aws_ca_bundle_path + elif global_ca_bundle_configmap: + ca_bundle_set = True + global_ca_bundle_volume_mount_path = os.getenv( + "CA_BUNDLE_VOLUME_MOUNT_POINT" + ) + ca_bundle_full_path = os.path.join( + global_ca_bundle_volume_mount_path, "cabundle.crt" + ) + if ca_bundle_set: if os.path.exists(ca_bundle_full_path): logger.info("ca bundle file(%s) exists." % (ca_bundle_full_path)) kwargs.update({"verify": ca_bundle_full_path}) @@ -778,15 +785,14 @@ def _unpack_archive_file(file_path, mimetype, target_dir=None) -> str: try: logger.info("Unpacking: %s", file_path) if mimetype == "application/x-tar": - archive = tarfile.open(file_path, "r", encoding="utf-8") + with tarfile.open(file_path, "r", encoding="utf-8") as archive: + archive.extractall(target_dir, filter="data") else: - archive = zipfile.ZipFile(file_path, "r") - archive.extractall(target_dir) - archive.close() - except (tarfile.TarError, zipfile.BadZipfile): + with zipfile.ZipFile(file_path, "r") as archive: + archive.extractall(target_dir) + except (tarfile.TarError, zipfile.BadZipfile) as e: raise RuntimeError( - "Failed to unpack archive file. \ -The file format is not valid." - ) + "Failed to unpack archive file. The file format is not valid." + ) from e os.remove(file_path) return target_dir diff --git a/python/storage/pyproject.toml b/python/storage/pyproject.toml index 2c8a315bb0c..a75bc3244e1 100644 --- a/python/storage/pyproject.toml +++ b/python/storage/pyproject.toml @@ -12,10 +12,10 @@ dependencies = [ "azure-storage-file-share<13.0.0,>=12.16.0", "azure-identity<2.0.0,>=1.15.0", "boto3<2.0.0,>=1.29.0", - "huggingface-hub[hf-transfer]<1.0.0,>=0.30.0", + "huggingface-hub[hf-transfer]>=0.32.0", ] name = "kserve-storage" -version = "00001" +version = "0.15.2" readme = "README.md" description = "KServe Storage Handler. This module is responsible to download the models from the provided source" classifiers = [ @@ -24,3 +24,4 @@ classifiers = [ "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", ] + diff --git a/python/storage/test/test_gcs_storage.py b/python/storage/test/test_gcs_storage.py index 9a20a2b084a..5679c2a5328 100644 --- a/python/storage/test/test_gcs_storage.py +++ b/python/storage/test/test_gcs_storage.py @@ -133,7 +133,11 @@ def test_gcs_model_unpack_archive_file( mock_dir = create_mock_dir("bar/") mock_file = create_mock_dir_with_file("bar", "mock.zip") - MockZipFile.return_value = mock_file + + # Properly set up the mock zipfile context manager + mock_zipfile = mock.MagicMock() + MockZipFile.return_value = mock_zipfile + mock_zipfile.__enter__.return_value = mock_zipfile mock_bucket = mock.MagicMock() mock_bucket.list_blobs().__iter__.return_value = [ @@ -146,9 +150,9 @@ def test_gcs_model_unpack_archive_file( download_arg_list = get_call_args(mock_file.download_to_filename.call_args_list) - extract_arg_list = get_call_args(mock_file.extractall.call_args_list) - + # Check that extractall was called on the zipfile mock + assert len(download_arg_list) > 0, "download_to_filename was never called" assert "/mock.zip" in download_arg_list[0][0] - assert output_dir == extract_arg_list[0][0] - assert mock_file.close.called + assert mock_zipfile.extractall.called + assert output_dir == mock_zipfile.extractall.call_args[0][0] assert mock_remove.called diff --git a/python/storage/uv.lock b/python/storage/uv.lock index 1bd0e40962f..7b03aa146c9 100644 --- a/python/storage/uv.lock +++ b/python/storage/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 1 +revision = 2 requires-python = ">=3.9, <3.13" resolution-markers = [ "python_full_version >= '3.10'", @@ -15,9 +15,9 @@ dependencies = [ { name = "six" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/29/ff7a519a315e41c85bab92a7478c6acd1cf0b14353139a08caee4c691f77/azure_core-1.34.0.tar.gz", hash = "sha256:bdb544989f246a0ad1c85d72eeb45f2f835afdcbc5b45e43f0dbde7461c81ece", size = 297999 } +sdist = { url = "https://files.pythonhosted.org/packages/c9/29/ff7a519a315e41c85bab92a7478c6acd1cf0b14353139a08caee4c691f77/azure_core-1.34.0.tar.gz", hash = "sha256:bdb544989f246a0ad1c85d72eeb45f2f835afdcbc5b45e43f0dbde7461c81ece", size = 297999, upload-time = "2025-05-01T23:17:27.59Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/9e/5c87b49f65bb16571599bc789857d0ded2f53014d3392bc88a5d1f3ad779/azure_core-1.34.0-py3-none-any.whl", hash = "sha256:0615d3b756beccdb6624d1c0ae97284f38b78fb59a2a9839bf927c66fbbdddd6", size = 207409 }, + { url = "https://files.pythonhosted.org/packages/84/9e/5c87b49f65bb16571599bc789857d0ded2f53014d3392bc88a5d1f3ad779/azure_core-1.34.0-py3-none-any.whl", hash = "sha256:0615d3b756beccdb6624d1c0ae97284f38b78fb59a2a9839bf927c66fbbdddd6", size = 207409, upload-time = "2025-05-01T23:17:29.818Z" }, ] [[package]] @@ -31,9 +31,9 @@ dependencies = [ { name = "msal-extensions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/41/52/458c1be17a5d3796570ae2ed3c6b7b55b134b22d5ef8132b4f97046a9051/azure_identity-1.23.0.tar.gz", hash = "sha256:d9cdcad39adb49d4bb2953a217f62aec1f65bbb3c63c9076da2be2a47e53dde4", size = 265280 } +sdist = { url = "https://files.pythonhosted.org/packages/41/52/458c1be17a5d3796570ae2ed3c6b7b55b134b22d5ef8132b4f97046a9051/azure_identity-1.23.0.tar.gz", hash = "sha256:d9cdcad39adb49d4bb2953a217f62aec1f65bbb3c63c9076da2be2a47e53dde4", size = 265280, upload-time = "2025-05-14T00:18:30.408Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/16/a51d47780f41e4b87bb2d454df6aea90a44a346e918ac189d3700f3d728d/azure_identity-1.23.0-py3-none-any.whl", hash = "sha256:dbbeb64b8e5eaa81c44c565f264b519ff2de7ff0e02271c49f3cb492762a50b0", size = 186097 }, + { url = "https://files.pythonhosted.org/packages/07/16/a51d47780f41e4b87bb2d454df6aea90a44a346e918ac189d3700f3d728d/azure_identity-1.23.0-py3-none-any.whl", hash = "sha256:dbbeb64b8e5eaa81c44c565f264b519ff2de7ff0e02271c49f3cb492762a50b0", size = 186097, upload-time = "2025-05-14T00:18:32.734Z" }, ] [[package]] @@ -46,9 +46,9 @@ dependencies = [ { name = "isodate" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/f3/f764536c25cc3829d36857167f03933ce9aee2262293179075439f3cd3ad/azure_storage_blob-12.25.1.tar.gz", hash = "sha256:4f294ddc9bc47909ac66b8934bd26b50d2000278b10ad82cc109764fdc6e0e3b", size = 570541 } +sdist = { url = "https://files.pythonhosted.org/packages/8b/f3/f764536c25cc3829d36857167f03933ce9aee2262293179075439f3cd3ad/azure_storage_blob-12.25.1.tar.gz", hash = "sha256:4f294ddc9bc47909ac66b8934bd26b50d2000278b10ad82cc109764fdc6e0e3b", size = 570541, upload-time = "2025-03-27T17:13:05.424Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/57/33/085d9352d416e617993821b9d9488222fbb559bc15c3641d6cbd6d16d236/azure_storage_blob-12.25.1-py3-none-any.whl", hash = "sha256:1f337aab12e918ec3f1b638baada97550673911c4ceed892acc8e4e891b74167", size = 406990 }, + { url = "https://files.pythonhosted.org/packages/57/33/085d9352d416e617993821b9d9488222fbb559bc15c3641d6cbd6d16d236/azure_storage_blob-12.25.1-py3-none-any.whl", hash = "sha256:1f337aab12e918ec3f1b638baada97550673911c4ceed892acc8e4e891b74167", size = 406990, upload-time = "2025-03-27T17:13:06.879Z" }, ] [[package]] @@ -61,9 +61,9 @@ dependencies = [ { name = "isodate" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/ef/40d6a0ee632725a2f1508b6ab18fa510802f850edf94147292e0ad1bea7d/azure_storage_file_share-12.21.0.tar.gz", hash = "sha256:db42bf6b43b3c0c27c9152202955277dfc26a59f7fad26c058431a6ae99580ce", size = 353616 } +sdist = { url = "https://files.pythonhosted.org/packages/da/ef/40d6a0ee632725a2f1508b6ab18fa510802f850edf94147292e0ad1bea7d/azure_storage_file_share-12.21.0.tar.gz", hash = "sha256:db42bf6b43b3c0c27c9152202955277dfc26a59f7fad26c058431a6ae99580ce", size = 353616, upload-time = "2025-03-11T19:11:45.911Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/e9/4856cdaba192955108f9bc9e80c772a7c608a24065c3866d058ced5a4f12/azure_storage_file_share-12.21.0-py3-none-any.whl", hash = "sha256:0875c7ee13d9a750d8a9b8ddd93d6502edd26cf40f44a390d7ae2637779300da", size = 290624 }, + { url = "https://files.pythonhosted.org/packages/e6/e9/4856cdaba192955108f9bc9e80c772a7c608a24065c3866d058ced5a4f12/azure_storage_file_share-12.21.0-py3-none-any.whl", hash = "sha256:0875c7ee13d9a750d8a9b8ddd93d6502edd26cf40f44a390d7ae2637779300da", size = 290624, upload-time = "2025-03-11T19:11:47.374Z" }, ] [[package]] @@ -75,9 +75,9 @@ dependencies = [ { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ad/95/99046c55799732d97b0f9a0bb99b64760f07dd55ac793393a6c4e847d8d6/boto3-1.38.33.tar.gz", hash = "sha256:6467909c1ae01ff67981f021bb2568592211765ec8a9a1d2c4529191e46c3541", size = 111825 } +sdist = { url = "https://files.pythonhosted.org/packages/ad/95/99046c55799732d97b0f9a0bb99b64760f07dd55ac793393a6c4e847d8d6/boto3-1.38.33.tar.gz", hash = "sha256:6467909c1ae01ff67981f021bb2568592211765ec8a9a1d2c4529191e46c3541", size = 111825, upload-time = "2025-06-09T19:24:16.167Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/7e/b3eb354e82f5a2a8ae1977f983f0cb6005b2e5fae79b7d06bd5a14b4e9c6/boto3-1.38.33-py3-none-any.whl", hash = "sha256:25d0717489c658f7ae6c3c7f0f7e1b4d611b30b2f08f0fcef6455ac6864a8768", size = 139937 }, + { url = "https://files.pythonhosted.org/packages/96/7e/b3eb354e82f5a2a8ae1977f983f0cb6005b2e5fae79b7d06bd5a14b4e9c6/boto3-1.38.33-py3-none-any.whl", hash = "sha256:25d0717489c658f7ae6c3c7f0f7e1b4d611b30b2f08f0fcef6455ac6864a8768", size = 139937, upload-time = "2025-06-09T19:24:12.774Z" }, ] [[package]] @@ -90,27 +90,27 @@ dependencies = [ { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "urllib3", version = "2.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/aa/1521d7e1dcb76af8dca81539eec141ee3581a32e0dc1f31d092b59feb06a/botocore-1.38.33.tar.gz", hash = "sha256:dbe8fea9d0426c644c89ef2018ead886ccbcc22901a02b377b4e65ce1cb69a2b", size = 13953431 } +sdist = { url = "https://files.pythonhosted.org/packages/9d/aa/1521d7e1dcb76af8dca81539eec141ee3581a32e0dc1f31d092b59feb06a/botocore-1.38.33.tar.gz", hash = "sha256:dbe8fea9d0426c644c89ef2018ead886ccbcc22901a02b377b4e65ce1cb69a2b", size = 13953431, upload-time = "2025-06-09T19:24:03.916Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/5a/253b707c29283a5d8516448da4febe017bcec0b7ed109981250d1b5f5347/botocore-1.38.33-py3-none-any.whl", hash = "sha256:ad25233e93dcebe95809b1f9393c1f11a639696327793d166295fb78dd7bc597", size = 13614262 }, + { url = "https://files.pythonhosted.org/packages/c1/5a/253b707c29283a5d8516448da4febe017bcec0b7ed109981250d1b5f5347/botocore-1.38.33-py3-none-any.whl", hash = "sha256:ad25233e93dcebe95809b1f9393c1f11a639696327793d166295fb78dd7bc597", size = 13614262, upload-time = "2025-06-09T19:24:00.393Z" }, ] [[package]] name = "cachetools" version = "5.5.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380 } +sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380, upload-time = "2025-02-20T21:01:19.524Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080 }, + { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" }, ] [[package]] name = "certifi" version = "2025.4.26" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705 } +sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618 }, + { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" }, ] [[package]] @@ -120,125 +120,125 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191 }, - { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592 }, - { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 }, - { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 }, - { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 }, - { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 }, - { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 }, - { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 }, - { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 }, - { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 }, - { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804 }, - { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299 }, - { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, - { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, - { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, - { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, - { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, - { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, - { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, - { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, - { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, - { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, - { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, - { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, - { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, - { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, - { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, - { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, - { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, - { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, - { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, - { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, - { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, - { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, - { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, - { url = "https://files.pythonhosted.org/packages/b9/ea/8bb50596b8ffbc49ddd7a1ad305035daa770202a6b782fc164647c2673ad/cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", size = 182220 }, - { url = "https://files.pythonhosted.org/packages/ae/11/e77c8cd24f58285a82c23af484cf5b124a376b32644e445960d1a4654c3a/cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", size = 178605 }, - { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910 }, - { url = "https://files.pythonhosted.org/packages/42/7a/9d086fab7c66bd7c4d0f27c57a1b6b068ced810afc498cc8c49e0088661c/cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", size = 447200 }, - { url = "https://files.pythonhosted.org/packages/da/63/1785ced118ce92a993b0ec9e0d0ac8dc3e5dbfbcaa81135be56c69cabbb6/cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", size = 454565 }, - { url = "https://files.pythonhosted.org/packages/74/06/90b8a44abf3556599cdec107f7290277ae8901a58f75e6fe8f970cd72418/cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", size = 435635 }, - { url = "https://files.pythonhosted.org/packages/bd/62/a1f468e5708a70b1d86ead5bab5520861d9c7eacce4a885ded9faa7729c3/cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", size = 445218 }, - { url = "https://files.pythonhosted.org/packages/5b/95/b34462f3ccb09c2594aa782d90a90b045de4ff1f70148ee79c69d37a0a5a/cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", size = 460486 }, - { url = "https://files.pythonhosted.org/packages/fc/fc/a1e4bebd8d680febd29cf6c8a40067182b64f00c7d105f8f26b5bc54317b/cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", size = 437911 }, - { url = "https://files.pythonhosted.org/packages/e6/c3/21cab7a6154b6a5ea330ae80de386e7665254835b9e98ecc1340b3a7de9a/cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", size = 460632 }, - { url = "https://files.pythonhosted.org/packages/cb/b5/fd9f8b5a84010ca169ee49f4e4ad6f8c05f4e3545b72ee041dbbcb159882/cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", size = 171820 }, - { url = "https://files.pythonhosted.org/packages/8c/52/b08750ce0bce45c143e1b5d7357ee8c55341b52bdef4b0f081af1eb248c2/cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", size = 181290 }, +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191, upload-time = "2024-09-04T20:43:30.027Z" }, + { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592, upload-time = "2024-09-04T20:43:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804, upload-time = "2024-09-04T20:43:48.186Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299, upload-time = "2024-09-04T20:43:49.812Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ea/8bb50596b8ffbc49ddd7a1ad305035daa770202a6b782fc164647c2673ad/cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", size = 182220, upload-time = "2024-09-04T20:45:01.577Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/e77c8cd24f58285a82c23af484cf5b124a376b32644e445960d1a4654c3a/cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", size = 178605, upload-time = "2024-09-04T20:45:03.837Z" }, + { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910, upload-time = "2024-09-04T20:45:05.315Z" }, + { url = "https://files.pythonhosted.org/packages/42/7a/9d086fab7c66bd7c4d0f27c57a1b6b068ced810afc498cc8c49e0088661c/cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", size = 447200, upload-time = "2024-09-04T20:45:06.903Z" }, + { url = "https://files.pythonhosted.org/packages/da/63/1785ced118ce92a993b0ec9e0d0ac8dc3e5dbfbcaa81135be56c69cabbb6/cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", size = 454565, upload-time = "2024-09-04T20:45:08.975Z" }, + { url = "https://files.pythonhosted.org/packages/74/06/90b8a44abf3556599cdec107f7290277ae8901a58f75e6fe8f970cd72418/cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", size = 435635, upload-time = "2024-09-04T20:45:10.64Z" }, + { url = "https://files.pythonhosted.org/packages/bd/62/a1f468e5708a70b1d86ead5bab5520861d9c7eacce4a885ded9faa7729c3/cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", size = 445218, upload-time = "2024-09-04T20:45:12.366Z" }, + { url = "https://files.pythonhosted.org/packages/5b/95/b34462f3ccb09c2594aa782d90a90b045de4ff1f70148ee79c69d37a0a5a/cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", size = 460486, upload-time = "2024-09-04T20:45:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/fc/fc/a1e4bebd8d680febd29cf6c8a40067182b64f00c7d105f8f26b5bc54317b/cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", size = 437911, upload-time = "2024-09-04T20:45:15.696Z" }, + { url = "https://files.pythonhosted.org/packages/e6/c3/21cab7a6154b6a5ea330ae80de386e7665254835b9e98ecc1340b3a7de9a/cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", size = 460632, upload-time = "2024-09-04T20:45:17.284Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b5/fd9f8b5a84010ca169ee49f4e4ad6f8c05f4e3545b72ee041dbbcb159882/cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", size = 171820, upload-time = "2024-09-04T20:45:18.762Z" }, + { url = "https://files.pythonhosted.org/packages/8c/52/b08750ce0bce45c143e1b5d7357ee8c55341b52bdef4b0f081af1eb248c2/cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", size = 181290, upload-time = "2024-09-04T20:45:20.226Z" }, ] [[package]] name = "charset-normalizer" version = "3.4.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818 }, - { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649 }, - { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045 }, - { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356 }, - { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471 }, - { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317 }, - { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368 }, - { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491 }, - { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695 }, - { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849 }, - { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091 }, - { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445 }, - { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782 }, - { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794 }, - { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846 }, - { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350 }, - { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657 }, - { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260 }, - { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164 }, - { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571 }, - { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952 }, - { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959 }, - { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030 }, - { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015 }, - { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106 }, - { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402 }, - { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936 }, - { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790 }, - { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924 }, - { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626 }, - { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567 }, - { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957 }, - { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408 }, - { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399 }, - { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815 }, - { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537 }, - { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565 }, - { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357 }, - { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776 }, - { url = "https://files.pythonhosted.org/packages/28/f8/dfb01ff6cc9af38552c69c9027501ff5a5117c4cc18dcd27cb5259fa1888/charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", size = 201671 }, - { url = "https://files.pythonhosted.org/packages/32/fb/74e26ee556a9dbfe3bd264289b67be1e6d616329403036f6507bb9f3f29c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", size = 144744 }, - { url = "https://files.pythonhosted.org/packages/ad/06/8499ee5aa7addc6f6d72e068691826ff093329fe59891e83b092ae4c851c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", size = 154993 }, - { url = "https://files.pythonhosted.org/packages/f1/a2/5e4c187680728219254ef107a6949c60ee0e9a916a5dadb148c7ae82459c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", size = 147382 }, - { url = "https://files.pythonhosted.org/packages/4c/fe/56aca740dda674f0cc1ba1418c4d84534be51f639b5f98f538b332dc9a95/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", size = 149536 }, - { url = "https://files.pythonhosted.org/packages/53/13/db2e7779f892386b589173dd689c1b1e304621c5792046edd8a978cbf9e0/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", size = 151349 }, - { url = "https://files.pythonhosted.org/packages/69/35/e52ab9a276186f729bce7a0638585d2982f50402046e4b0faa5d2c3ef2da/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", size = 146365 }, - { url = "https://files.pythonhosted.org/packages/a6/d8/af7333f732fc2e7635867d56cb7c349c28c7094910c72267586947561b4b/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", size = 154499 }, - { url = "https://files.pythonhosted.org/packages/7a/3d/a5b2e48acef264d71e036ff30bcc49e51bde80219bb628ba3e00cf59baac/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", size = 157735 }, - { url = "https://files.pythonhosted.org/packages/85/d8/23e2c112532a29f3eef374375a8684a4f3b8e784f62b01da931186f43494/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", size = 154786 }, - { url = "https://files.pythonhosted.org/packages/c7/57/93e0169f08ecc20fe82d12254a200dfaceddc1c12a4077bf454ecc597e33/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", size = 150203 }, - { url = "https://files.pythonhosted.org/packages/2c/9d/9bf2b005138e7e060d7ebdec7503d0ef3240141587651f4b445bdf7286c2/charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", size = 98436 }, - { url = "https://files.pythonhosted.org/packages/6d/24/5849d46cf4311bbf21b424c443b09b459f5b436b1558c04e45dbb7cc478b/charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", size = 105772 }, - { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626 }, +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" }, + { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" }, + { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" }, + { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" }, + { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" }, + { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" }, + { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, + { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, + { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, + { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, + { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, + { url = "https://files.pythonhosted.org/packages/28/f8/dfb01ff6cc9af38552c69c9027501ff5a5117c4cc18dcd27cb5259fa1888/charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", size = 201671, upload-time = "2025-05-02T08:34:12.696Z" }, + { url = "https://files.pythonhosted.org/packages/32/fb/74e26ee556a9dbfe3bd264289b67be1e6d616329403036f6507bb9f3f29c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", size = 144744, upload-time = "2025-05-02T08:34:14.665Z" }, + { url = "https://files.pythonhosted.org/packages/ad/06/8499ee5aa7addc6f6d72e068691826ff093329fe59891e83b092ae4c851c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", size = 154993, upload-time = "2025-05-02T08:34:17.134Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a2/5e4c187680728219254ef107a6949c60ee0e9a916a5dadb148c7ae82459c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", size = 147382, upload-time = "2025-05-02T08:34:19.081Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/56aca740dda674f0cc1ba1418c4d84534be51f639b5f98f538b332dc9a95/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", size = 149536, upload-time = "2025-05-02T08:34:21.073Z" }, + { url = "https://files.pythonhosted.org/packages/53/13/db2e7779f892386b589173dd689c1b1e304621c5792046edd8a978cbf9e0/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", size = 151349, upload-time = "2025-05-02T08:34:23.193Z" }, + { url = "https://files.pythonhosted.org/packages/69/35/e52ab9a276186f729bce7a0638585d2982f50402046e4b0faa5d2c3ef2da/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", size = 146365, upload-time = "2025-05-02T08:34:25.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d8/af7333f732fc2e7635867d56cb7c349c28c7094910c72267586947561b4b/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", size = 154499, upload-time = "2025-05-02T08:34:27.359Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3d/a5b2e48acef264d71e036ff30bcc49e51bde80219bb628ba3e00cf59baac/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", size = 157735, upload-time = "2025-05-02T08:34:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/85/d8/23e2c112532a29f3eef374375a8684a4f3b8e784f62b01da931186f43494/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", size = 154786, upload-time = "2025-05-02T08:34:31.858Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/93e0169f08ecc20fe82d12254a200dfaceddc1c12a4077bf454ecc597e33/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", size = 150203, upload-time = "2025-05-02T08:34:33.88Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9d/9bf2b005138e7e060d7ebdec7503d0ef3240141587651f4b445bdf7286c2/charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", size = 98436, upload-time = "2025-05-02T08:34:35.907Z" }, + { url = "https://files.pythonhosted.org/packages/6d/24/5849d46cf4311bbf21b424c443b09b459f5b436b1558c04e45dbb7cc478b/charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", size = 105772, upload-time = "2025-05-02T08:34:37.935Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, ] [[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 } +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 }, + { 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]] @@ -248,62 +248,62 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fe/c8/a2a376a8711c1e11708b9c9972e0c3223f5fc682552c82d8db844393d6ce/cryptography-45.0.4.tar.gz", hash = "sha256:7405ade85c83c37682c8fe65554759800a4a8c54b2d96e0f8ad114d31b808d57", size = 744890 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/1c/92637793de053832523b410dbe016d3f5c11b41d0cf6eef8787aabb51d41/cryptography-45.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:425a9a6ac2823ee6e46a76a21a4e8342d8fa5c01e08b823c1f19a8b74f096069", size = 7055712 }, - { url = "https://files.pythonhosted.org/packages/ba/14/93b69f2af9ba832ad6618a03f8a034a5851dc9a3314336a3d71c252467e1/cryptography-45.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:680806cf63baa0039b920f4976f5f31b10e772de42f16310a6839d9f21a26b0d", size = 4205335 }, - { url = "https://files.pythonhosted.org/packages/67/30/fae1000228634bf0b647fca80403db5ca9e3933b91dd060570689f0bd0f7/cryptography-45.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4ca0f52170e821bc8da6fc0cc565b7bb8ff8d90d36b5e9fdd68e8a86bdf72036", size = 4431487 }, - { url = "https://files.pythonhosted.org/packages/6d/5a/7dffcf8cdf0cb3c2430de7404b327e3db64735747d641fc492539978caeb/cryptography-45.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f3fe7a5ae34d5a414957cc7f457e2b92076e72938423ac64d215722f6cf49a9e", size = 4208922 }, - { url = "https://files.pythonhosted.org/packages/c6/f3/528729726eb6c3060fa3637253430547fbaaea95ab0535ea41baa4a6fbd8/cryptography-45.0.4-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:25eb4d4d3e54595dc8adebc6bbd5623588991d86591a78c2548ffb64797341e2", size = 3900433 }, - { url = "https://files.pythonhosted.org/packages/d9/4a/67ba2e40f619e04d83c32f7e1d484c1538c0800a17c56a22ff07d092ccc1/cryptography-45.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ce1678a2ccbe696cf3af15a75bb72ee008d7ff183c9228592ede9db467e64f1b", size = 4464163 }, - { url = "https://files.pythonhosted.org/packages/7e/9a/b4d5aa83661483ac372464809c4b49b5022dbfe36b12fe9e323ca8512420/cryptography-45.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:49fe9155ab32721b9122975e168a6760d8ce4cffe423bcd7ca269ba41b5dfac1", size = 4208687 }, - { url = "https://files.pythonhosted.org/packages/db/b7/a84bdcd19d9c02ec5807f2ec2d1456fd8451592c5ee353816c09250e3561/cryptography-45.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2882338b2a6e0bd337052e8b9007ced85c637da19ef9ecaf437744495c8c2999", size = 4463623 }, - { url = "https://files.pythonhosted.org/packages/d8/84/69707d502d4d905021cac3fb59a316344e9f078b1da7fb43ecde5e10840a/cryptography-45.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:23b9c3ea30c3ed4db59e7b9619272e94891f8a3a5591d0b656a7582631ccf750", size = 4332447 }, - { url = "https://files.pythonhosted.org/packages/f3/ee/d4f2ab688e057e90ded24384e34838086a9b09963389a5ba6854b5876598/cryptography-45.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0a97c927497e3bc36b33987abb99bf17a9a175a19af38a892dc4bbb844d7ee2", size = 4572830 }, - { url = "https://files.pythonhosted.org/packages/70/d4/994773a261d7ff98034f72c0e8251fe2755eac45e2265db4c866c1c6829c/cryptography-45.0.4-cp311-abi3-win32.whl", hash = "sha256:e00a6c10a5c53979d6242f123c0a97cff9f3abed7f064fc412c36dc521b5f257", size = 2932769 }, - { url = "https://files.pythonhosted.org/packages/5a/42/c80bd0b67e9b769b364963b5252b17778a397cefdd36fa9aa4a5f34c599a/cryptography-45.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:817ee05c6c9f7a69a16200f0c90ab26d23a87701e2a284bd15156783e46dbcc8", size = 3410441 }, - { url = "https://files.pythonhosted.org/packages/ce/0b/2488c89f3a30bc821c9d96eeacfcab6ff3accc08a9601ba03339c0fd05e5/cryptography-45.0.4-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:964bcc28d867e0f5491a564b7debb3ffdd8717928d315d12e0d7defa9e43b723", size = 7031836 }, - { url = "https://files.pythonhosted.org/packages/fe/51/8c584ed426093aac257462ae62d26ad61ef1cbf5b58d8b67e6e13c39960e/cryptography-45.0.4-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6a5bf57554e80f75a7db3d4b1dacaa2764611ae166ab42ea9a72bcdb5d577637", size = 4195746 }, - { url = "https://files.pythonhosted.org/packages/5c/7d/4b0ca4d7af95a704eef2f8f80a8199ed236aaf185d55385ae1d1610c03c2/cryptography-45.0.4-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:46cf7088bf91bdc9b26f9c55636492c1cce3e7aaf8041bbf0243f5e5325cfb2d", size = 4424456 }, - { url = "https://files.pythonhosted.org/packages/1d/45/5fabacbc6e76ff056f84d9f60eeac18819badf0cefc1b6612ee03d4ab678/cryptography-45.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7bedbe4cc930fa4b100fc845ea1ea5788fcd7ae9562e669989c11618ae8d76ee", size = 4198495 }, - { url = "https://files.pythonhosted.org/packages/55/b7/ffc9945b290eb0a5d4dab9b7636706e3b5b92f14ee5d9d4449409d010d54/cryptography-45.0.4-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:eaa3e28ea2235b33220b949c5a0d6cf79baa80eab2eb5607ca8ab7525331b9ff", size = 3885540 }, - { url = "https://files.pythonhosted.org/packages/7f/e3/57b010282346980475e77d414080acdcb3dab9a0be63071efc2041a2c6bd/cryptography-45.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7ef2dde4fa9408475038fc9aadfc1fb2676b174e68356359632e980c661ec8f6", size = 4452052 }, - { url = "https://files.pythonhosted.org/packages/37/e6/ddc4ac2558bf2ef517a358df26f45bc774a99bf4653e7ee34b5e749c03e3/cryptography-45.0.4-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6a3511ae33f09094185d111160fd192c67aa0a2a8d19b54d36e4c78f651dc5ad", size = 4198024 }, - { url = "https://files.pythonhosted.org/packages/3a/c0/85fa358ddb063ec588aed4a6ea1df57dc3e3bc1712d87c8fa162d02a65fc/cryptography-45.0.4-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:06509dc70dd71fa56eaa138336244e2fbaf2ac164fc9b5e66828fccfd2b680d6", size = 4451442 }, - { url = "https://files.pythonhosted.org/packages/33/67/362d6ec1492596e73da24e669a7fbbaeb1c428d6bf49a29f7a12acffd5dc/cryptography-45.0.4-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5f31e6b0a5a253f6aa49be67279be4a7e5a4ef259a9f33c69f7d1b1191939872", size = 4325038 }, - { url = "https://files.pythonhosted.org/packages/53/75/82a14bf047a96a1b13ebb47fb9811c4f73096cfa2e2b17c86879687f9027/cryptography-45.0.4-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:944e9ccf67a9594137f942d5b52c8d238b1b4e46c7a0c2891b7ae6e01e7c80a4", size = 4560964 }, - { url = "https://files.pythonhosted.org/packages/cd/37/1a3cba4c5a468ebf9b95523a5ef5651244693dc712001e276682c278fc00/cryptography-45.0.4-cp37-abi3-win32.whl", hash = "sha256:c22fe01e53dc65edd1945a2e6f0015e887f84ced233acecb64b4daadb32f5c97", size = 2924557 }, - { url = "https://files.pythonhosted.org/packages/2a/4b/3256759723b7e66380397d958ca07c59cfc3fb5c794fb5516758afd05d41/cryptography-45.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:627ba1bc94f6adf0b0a2e35d87020285ead22d9f648c7e75bb64f367375f3b22", size = 3395508 }, - { url = "https://files.pythonhosted.org/packages/16/33/b38e9d372afde56906a23839302c19abdac1c505bfb4776c1e4b07c3e145/cryptography-45.0.4-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a77c6fb8d76e9c9f99f2f3437c1a4ac287b34eaf40997cfab1e9bd2be175ac39", size = 3580103 }, - { url = "https://files.pythonhosted.org/packages/c4/b9/357f18064ec09d4807800d05a48f92f3b369056a12f995ff79549fbb31f1/cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7aad98a25ed8ac917fdd8a9c1e706e5a0956e06c498be1f713b61734333a4507", size = 4143732 }, - { url = "https://files.pythonhosted.org/packages/c4/9c/7f7263b03d5db329093617648b9bd55c953de0b245e64e866e560f9aac07/cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3530382a43a0e524bc931f187fc69ef4c42828cf7d7f592f7f249f602b5a4ab0", size = 4385424 }, - { url = "https://files.pythonhosted.org/packages/a6/5a/6aa9d8d5073d5acc0e04e95b2860ef2684b2bd2899d8795fc443013e263b/cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:6b613164cb8425e2f8db5849ffb84892e523bf6d26deb8f9bb76ae86181fa12b", size = 4142438 }, - { url = "https://files.pythonhosted.org/packages/42/1c/71c638420f2cdd96d9c2b287fec515faf48679b33a2b583d0f1eda3a3375/cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:96d4819e25bf3b685199b304a0029ce4a3caf98947ce8a066c9137cc78ad2c58", size = 4384622 }, - { url = "https://files.pythonhosted.org/packages/ef/ab/e3a055c34e97deadbf0d846e189237d3385dca99e1a7e27384c3b2292041/cryptography-45.0.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b97737a3ffbea79eebb062eb0d67d72307195035332501722a9ca86bab9e3ab2", size = 3328911 }, - { url = "https://files.pythonhosted.org/packages/ea/ba/cf442ae99ef363855ed84b39e0fb3c106ac66b7a7703f3c9c9cfe05412cb/cryptography-45.0.4-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4828190fb6c4bcb6ebc6331f01fe66ae838bb3bd58e753b59d4b22eb444b996c", size = 3590512 }, - { url = "https://files.pythonhosted.org/packages/28/9a/a7d5bb87d149eb99a5abdc69a41e4e47b8001d767e5f403f78bfaafc7aa7/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:03dbff8411206713185b8cebe31bc5c0eb544799a50c09035733716b386e61a4", size = 4146899 }, - { url = "https://files.pythonhosted.org/packages/17/11/9361c2c71c42cc5c465cf294c8030e72fb0c87752bacbd7a3675245e3db3/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51dfbd4d26172d31150d84c19bbe06c68ea4b7f11bbc7b3a5e146b367c311349", size = 4388900 }, - { url = "https://files.pythonhosted.org/packages/c0/76/f95b83359012ee0e670da3e41c164a0c256aeedd81886f878911581d852f/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:0339a692de47084969500ee455e42c58e449461e0ec845a34a6a9b9bf7df7fb8", size = 4146422 }, - { url = "https://files.pythonhosted.org/packages/09/ad/5429fcc4def93e577a5407988f89cf15305e64920203d4ac14601a9dc876/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:0cf13c77d710131d33e63626bd55ae7c0efb701ebdc2b3a7952b9b23a0412862", size = 4388475 }, - { url = "https://files.pythonhosted.org/packages/99/49/0ab9774f64555a1b50102757811508f5ace451cf5dc0a2d074a4b9deca6a/cryptography-45.0.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bbc505d1dc469ac12a0a064214879eac6294038d6b24ae9f71faae1448a9608d", size = 3337594 }, +sdist = { url = "https://files.pythonhosted.org/packages/fe/c8/a2a376a8711c1e11708b9c9972e0c3223f5fc682552c82d8db844393d6ce/cryptography-45.0.4.tar.gz", hash = "sha256:7405ade85c83c37682c8fe65554759800a4a8c54b2d96e0f8ad114d31b808d57", size = 744890, upload-time = "2025-06-10T00:03:51.297Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/1c/92637793de053832523b410dbe016d3f5c11b41d0cf6eef8787aabb51d41/cryptography-45.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:425a9a6ac2823ee6e46a76a21a4e8342d8fa5c01e08b823c1f19a8b74f096069", size = 7055712, upload-time = "2025-06-10T00:02:38.826Z" }, + { url = "https://files.pythonhosted.org/packages/ba/14/93b69f2af9ba832ad6618a03f8a034a5851dc9a3314336a3d71c252467e1/cryptography-45.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:680806cf63baa0039b920f4976f5f31b10e772de42f16310a6839d9f21a26b0d", size = 4205335, upload-time = "2025-06-10T00:02:41.64Z" }, + { url = "https://files.pythonhosted.org/packages/67/30/fae1000228634bf0b647fca80403db5ca9e3933b91dd060570689f0bd0f7/cryptography-45.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4ca0f52170e821bc8da6fc0cc565b7bb8ff8d90d36b5e9fdd68e8a86bdf72036", size = 4431487, upload-time = "2025-06-10T00:02:43.696Z" }, + { url = "https://files.pythonhosted.org/packages/6d/5a/7dffcf8cdf0cb3c2430de7404b327e3db64735747d641fc492539978caeb/cryptography-45.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f3fe7a5ae34d5a414957cc7f457e2b92076e72938423ac64d215722f6cf49a9e", size = 4208922, upload-time = "2025-06-10T00:02:45.334Z" }, + { url = "https://files.pythonhosted.org/packages/c6/f3/528729726eb6c3060fa3637253430547fbaaea95ab0535ea41baa4a6fbd8/cryptography-45.0.4-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:25eb4d4d3e54595dc8adebc6bbd5623588991d86591a78c2548ffb64797341e2", size = 3900433, upload-time = "2025-06-10T00:02:47.359Z" }, + { url = "https://files.pythonhosted.org/packages/d9/4a/67ba2e40f619e04d83c32f7e1d484c1538c0800a17c56a22ff07d092ccc1/cryptography-45.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ce1678a2ccbe696cf3af15a75bb72ee008d7ff183c9228592ede9db467e64f1b", size = 4464163, upload-time = "2025-06-10T00:02:49.412Z" }, + { url = "https://files.pythonhosted.org/packages/7e/9a/b4d5aa83661483ac372464809c4b49b5022dbfe36b12fe9e323ca8512420/cryptography-45.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:49fe9155ab32721b9122975e168a6760d8ce4cffe423bcd7ca269ba41b5dfac1", size = 4208687, upload-time = "2025-06-10T00:02:50.976Z" }, + { url = "https://files.pythonhosted.org/packages/db/b7/a84bdcd19d9c02ec5807f2ec2d1456fd8451592c5ee353816c09250e3561/cryptography-45.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2882338b2a6e0bd337052e8b9007ced85c637da19ef9ecaf437744495c8c2999", size = 4463623, upload-time = "2025-06-10T00:02:52.542Z" }, + { url = "https://files.pythonhosted.org/packages/d8/84/69707d502d4d905021cac3fb59a316344e9f078b1da7fb43ecde5e10840a/cryptography-45.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:23b9c3ea30c3ed4db59e7b9619272e94891f8a3a5591d0b656a7582631ccf750", size = 4332447, upload-time = "2025-06-10T00:02:54.63Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ee/d4f2ab688e057e90ded24384e34838086a9b09963389a5ba6854b5876598/cryptography-45.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0a97c927497e3bc36b33987abb99bf17a9a175a19af38a892dc4bbb844d7ee2", size = 4572830, upload-time = "2025-06-10T00:02:56.689Z" }, + { url = "https://files.pythonhosted.org/packages/70/d4/994773a261d7ff98034f72c0e8251fe2755eac45e2265db4c866c1c6829c/cryptography-45.0.4-cp311-abi3-win32.whl", hash = "sha256:e00a6c10a5c53979d6242f123c0a97cff9f3abed7f064fc412c36dc521b5f257", size = 2932769, upload-time = "2025-06-10T00:02:58.467Z" }, + { url = "https://files.pythonhosted.org/packages/5a/42/c80bd0b67e9b769b364963b5252b17778a397cefdd36fa9aa4a5f34c599a/cryptography-45.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:817ee05c6c9f7a69a16200f0c90ab26d23a87701e2a284bd15156783e46dbcc8", size = 3410441, upload-time = "2025-06-10T00:03:00.14Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0b/2488c89f3a30bc821c9d96eeacfcab6ff3accc08a9601ba03339c0fd05e5/cryptography-45.0.4-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:964bcc28d867e0f5491a564b7debb3ffdd8717928d315d12e0d7defa9e43b723", size = 7031836, upload-time = "2025-06-10T00:03:01.726Z" }, + { url = "https://files.pythonhosted.org/packages/fe/51/8c584ed426093aac257462ae62d26ad61ef1cbf5b58d8b67e6e13c39960e/cryptography-45.0.4-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6a5bf57554e80f75a7db3d4b1dacaa2764611ae166ab42ea9a72bcdb5d577637", size = 4195746, upload-time = "2025-06-10T00:03:03.94Z" }, + { url = "https://files.pythonhosted.org/packages/5c/7d/4b0ca4d7af95a704eef2f8f80a8199ed236aaf185d55385ae1d1610c03c2/cryptography-45.0.4-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:46cf7088bf91bdc9b26f9c55636492c1cce3e7aaf8041bbf0243f5e5325cfb2d", size = 4424456, upload-time = "2025-06-10T00:03:05.589Z" }, + { url = "https://files.pythonhosted.org/packages/1d/45/5fabacbc6e76ff056f84d9f60eeac18819badf0cefc1b6612ee03d4ab678/cryptography-45.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7bedbe4cc930fa4b100fc845ea1ea5788fcd7ae9562e669989c11618ae8d76ee", size = 4198495, upload-time = "2025-06-10T00:03:09.172Z" }, + { url = "https://files.pythonhosted.org/packages/55/b7/ffc9945b290eb0a5d4dab9b7636706e3b5b92f14ee5d9d4449409d010d54/cryptography-45.0.4-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:eaa3e28ea2235b33220b949c5a0d6cf79baa80eab2eb5607ca8ab7525331b9ff", size = 3885540, upload-time = "2025-06-10T00:03:10.835Z" }, + { url = "https://files.pythonhosted.org/packages/7f/e3/57b010282346980475e77d414080acdcb3dab9a0be63071efc2041a2c6bd/cryptography-45.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7ef2dde4fa9408475038fc9aadfc1fb2676b174e68356359632e980c661ec8f6", size = 4452052, upload-time = "2025-06-10T00:03:12.448Z" }, + { url = "https://files.pythonhosted.org/packages/37/e6/ddc4ac2558bf2ef517a358df26f45bc774a99bf4653e7ee34b5e749c03e3/cryptography-45.0.4-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6a3511ae33f09094185d111160fd192c67aa0a2a8d19b54d36e4c78f651dc5ad", size = 4198024, upload-time = "2025-06-10T00:03:13.976Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c0/85fa358ddb063ec588aed4a6ea1df57dc3e3bc1712d87c8fa162d02a65fc/cryptography-45.0.4-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:06509dc70dd71fa56eaa138336244e2fbaf2ac164fc9b5e66828fccfd2b680d6", size = 4451442, upload-time = "2025-06-10T00:03:16.248Z" }, + { url = "https://files.pythonhosted.org/packages/33/67/362d6ec1492596e73da24e669a7fbbaeb1c428d6bf49a29f7a12acffd5dc/cryptography-45.0.4-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5f31e6b0a5a253f6aa49be67279be4a7e5a4ef259a9f33c69f7d1b1191939872", size = 4325038, upload-time = "2025-06-10T00:03:18.4Z" }, + { url = "https://files.pythonhosted.org/packages/53/75/82a14bf047a96a1b13ebb47fb9811c4f73096cfa2e2b17c86879687f9027/cryptography-45.0.4-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:944e9ccf67a9594137f942d5b52c8d238b1b4e46c7a0c2891b7ae6e01e7c80a4", size = 4560964, upload-time = "2025-06-10T00:03:20.06Z" }, + { url = "https://files.pythonhosted.org/packages/cd/37/1a3cba4c5a468ebf9b95523a5ef5651244693dc712001e276682c278fc00/cryptography-45.0.4-cp37-abi3-win32.whl", hash = "sha256:c22fe01e53dc65edd1945a2e6f0015e887f84ced233acecb64b4daadb32f5c97", size = 2924557, upload-time = "2025-06-10T00:03:22.563Z" }, + { url = "https://files.pythonhosted.org/packages/2a/4b/3256759723b7e66380397d958ca07c59cfc3fb5c794fb5516758afd05d41/cryptography-45.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:627ba1bc94f6adf0b0a2e35d87020285ead22d9f648c7e75bb64f367375f3b22", size = 3395508, upload-time = "2025-06-10T00:03:24.586Z" }, + { url = "https://files.pythonhosted.org/packages/16/33/b38e9d372afde56906a23839302c19abdac1c505bfb4776c1e4b07c3e145/cryptography-45.0.4-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a77c6fb8d76e9c9f99f2f3437c1a4ac287b34eaf40997cfab1e9bd2be175ac39", size = 3580103, upload-time = "2025-06-10T00:03:26.207Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b9/357f18064ec09d4807800d05a48f92f3b369056a12f995ff79549fbb31f1/cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7aad98a25ed8ac917fdd8a9c1e706e5a0956e06c498be1f713b61734333a4507", size = 4143732, upload-time = "2025-06-10T00:03:27.896Z" }, + { url = "https://files.pythonhosted.org/packages/c4/9c/7f7263b03d5db329093617648b9bd55c953de0b245e64e866e560f9aac07/cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3530382a43a0e524bc931f187fc69ef4c42828cf7d7f592f7f249f602b5a4ab0", size = 4385424, upload-time = "2025-06-10T00:03:29.992Z" }, + { url = "https://files.pythonhosted.org/packages/a6/5a/6aa9d8d5073d5acc0e04e95b2860ef2684b2bd2899d8795fc443013e263b/cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:6b613164cb8425e2f8db5849ffb84892e523bf6d26deb8f9bb76ae86181fa12b", size = 4142438, upload-time = "2025-06-10T00:03:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/42/1c/71c638420f2cdd96d9c2b287fec515faf48679b33a2b583d0f1eda3a3375/cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:96d4819e25bf3b685199b304a0029ce4a3caf98947ce8a066c9137cc78ad2c58", size = 4384622, upload-time = "2025-06-10T00:03:33.491Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ab/e3a055c34e97deadbf0d846e189237d3385dca99e1a7e27384c3b2292041/cryptography-45.0.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b97737a3ffbea79eebb062eb0d67d72307195035332501722a9ca86bab9e3ab2", size = 3328911, upload-time = "2025-06-10T00:03:35.035Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ba/cf442ae99ef363855ed84b39e0fb3c106ac66b7a7703f3c9c9cfe05412cb/cryptography-45.0.4-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4828190fb6c4bcb6ebc6331f01fe66ae838bb3bd58e753b59d4b22eb444b996c", size = 3590512, upload-time = "2025-06-10T00:03:36.982Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a7d5bb87d149eb99a5abdc69a41e4e47b8001d767e5f403f78bfaafc7aa7/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:03dbff8411206713185b8cebe31bc5c0eb544799a50c09035733716b386e61a4", size = 4146899, upload-time = "2025-06-10T00:03:38.659Z" }, + { url = "https://files.pythonhosted.org/packages/17/11/9361c2c71c42cc5c465cf294c8030e72fb0c87752bacbd7a3675245e3db3/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51dfbd4d26172d31150d84c19bbe06c68ea4b7f11bbc7b3a5e146b367c311349", size = 4388900, upload-time = "2025-06-10T00:03:40.233Z" }, + { url = "https://files.pythonhosted.org/packages/c0/76/f95b83359012ee0e670da3e41c164a0c256aeedd81886f878911581d852f/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:0339a692de47084969500ee455e42c58e449461e0ec845a34a6a9b9bf7df7fb8", size = 4146422, upload-time = "2025-06-10T00:03:41.827Z" }, + { url = "https://files.pythonhosted.org/packages/09/ad/5429fcc4def93e577a5407988f89cf15305e64920203d4ac14601a9dc876/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:0cf13c77d710131d33e63626bd55ae7c0efb701ebdc2b3a7952b9b23a0412862", size = 4388475, upload-time = "2025-06-10T00:03:43.493Z" }, + { url = "https://files.pythonhosted.org/packages/99/49/0ab9774f64555a1b50102757811508f5ace451cf5dc0a2d074a4b9deca6a/cryptography-45.0.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bbc505d1dc469ac12a0a064214879eac6294038d6b24ae9f71faae1448a9608d", size = 3337594, upload-time = "2025-06-10T00:03:45.523Z" }, ] [[package]] name = "filelock" version = "3.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075 } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215 }, + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, ] [[package]] name = "fsspec" version = "2025.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/00/f7/27f15d41f0ed38e8fcc488584b57e902b331da7f7c6dcda53721b15838fc/fsspec-2025.5.1.tar.gz", hash = "sha256:2e55e47a540b91843b755e83ded97c6e897fa0942b11490113f09e9c443c2475", size = 303033 } +sdist = { url = "https://files.pythonhosted.org/packages/00/f7/27f15d41f0ed38e8fcc488584b57e902b331da7f7c6dcda53721b15838fc/fsspec-2025.5.1.tar.gz", hash = "sha256:2e55e47a540b91843b755e83ded97c6e897fa0942b11490113f09e9c443c2475", size = 303033, upload-time = "2025-05-24T12:03:23.792Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/61/78c7b3851add1481b048b5fdc29067397a1784e2910592bc81bb3f608635/fsspec-2025.5.1-py3-none-any.whl", hash = "sha256:24d3a2e663d5fc735ab256263c4075f374a174c3410c0b25e5bd1970bceaa462", size = 199052 }, + { url = "https://files.pythonhosted.org/packages/bb/61/78c7b3851add1481b048b5fdc29067397a1784e2910592bc81bb3f608635/fsspec-2025.5.1-py3-none-any.whl", hash = "sha256:24d3a2e663d5fc735ab256263c4075f374a174c3410c0b25e5bd1970bceaa462", size = 199052, upload-time = "2025-05-24T12:03:21.66Z" }, ] [[package]] @@ -317,9 +317,9 @@ dependencies = [ { name = "protobuf" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/98/a2/8176b416ca08106b2ae30cd4a006c8176945f682c3a5b42f141c9173f505/google_api_core-2.25.0.tar.gz", hash = "sha256:9b548e688702f82a34ed8409fb8a6961166f0b7795032f0be8f48308dff4333a", size = 164914 } +sdist = { url = "https://files.pythonhosted.org/packages/98/a2/8176b416ca08106b2ae30cd4a006c8176945f682c3a5b42f141c9173f505/google_api_core-2.25.0.tar.gz", hash = "sha256:9b548e688702f82a34ed8409fb8a6961166f0b7795032f0be8f48308dff4333a", size = 164914, upload-time = "2025-06-02T14:45:34.789Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/ca/149e41a277bb0855e8ded85fd7579d7747c1223e253d82c5c0f1be236875/google_api_core-2.25.0-py3-none-any.whl", hash = "sha256:1db79d1281dcf9f3d10023283299ba38f3dc9f639ec41085968fd23e5bcf512e", size = 160668 }, + { url = "https://files.pythonhosted.org/packages/ac/ca/149e41a277bb0855e8ded85fd7579d7747c1223e253d82c5c0f1be236875/google_api_core-2.25.0-py3-none-any.whl", hash = "sha256:1db79d1281dcf9f3d10023283299ba38f3dc9f639ec41085968fd23e5bcf512e", size = 160668, upload-time = "2025-06-02T14:45:33.272Z" }, ] [[package]] @@ -331,9 +331,9 @@ dependencies = [ { name = "pyasn1-modules" }, { name = "rsa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/9b/e92ef23b84fa10a64ce4831390b7a4c2e53c0132568d99d4ae61d04c8855/google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77", size = 281029 } +sdist = { url = "https://files.pythonhosted.org/packages/9e/9b/e92ef23b84fa10a64ce4831390b7a4c2e53c0132568d99d4ae61d04c8855/google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77", size = 281029, upload-time = "2025-06-04T18:04:57.577Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/63/b19553b658a1692443c62bd07e5868adaa0ad746a0751ba62c59568cd45b/google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca", size = 216137 }, + { url = "https://files.pythonhosted.org/packages/17/63/b19553b658a1692443c62bd07e5868adaa0ad746a0751ba62c59568cd45b/google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca", size = 216137, upload-time = "2025-06-04T18:04:55.573Z" }, ] [[package]] @@ -344,9 +344,9 @@ dependencies = [ { name = "google-api-core" }, { name = "google-auth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d6/b8/2b53838d2acd6ec6168fd284a990c76695e84c65deee79c9f3a4276f6b4f/google_cloud_core-2.4.3.tar.gz", hash = "sha256:1fab62d7102844b278fe6dead3af32408b1df3eb06f5c7e8634cbd40edc4da53", size = 35861 } +sdist = { url = "https://files.pythonhosted.org/packages/d6/b8/2b53838d2acd6ec6168fd284a990c76695e84c65deee79c9f3a4276f6b4f/google_cloud_core-2.4.3.tar.gz", hash = "sha256:1fab62d7102844b278fe6dead3af32408b1df3eb06f5c7e8634cbd40edc4da53", size = 35861, upload-time = "2025-03-10T21:05:38.948Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/86/bda7241a8da2d28a754aad2ba0f6776e35b67e37c36ae0c45d49370f1014/google_cloud_core-2.4.3-py2.py3-none-any.whl", hash = "sha256:5130f9f4c14b4fafdff75c79448f9495cfade0d8775facf1b09c3bf67e027f6e", size = 29348 }, + { url = "https://files.pythonhosted.org/packages/40/86/bda7241a8da2d28a754aad2ba0f6776e35b67e37c36ae0c45d49370f1014/google_cloud_core-2.4.3-py2.py3-none-any.whl", hash = "sha256:5130f9f4c14b4fafdff75c79448f9495cfade0d8775facf1b09c3bf67e027f6e", size = 29348, upload-time = "2025-03-10T21:05:37.785Z" }, ] [[package]] @@ -361,43 +361,43 @@ dependencies = [ { name = "google-resumable-media" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/36/76/4d965702e96bb67976e755bed9828fa50306dca003dbee08b67f41dd265e/google_cloud_storage-2.19.0.tar.gz", hash = "sha256:cd05e9e7191ba6cb68934d8eb76054d9be4562aa89dbc4236feee4d7d51342b2", size = 5535488 } +sdist = { url = "https://files.pythonhosted.org/packages/36/76/4d965702e96bb67976e755bed9828fa50306dca003dbee08b67f41dd265e/google_cloud_storage-2.19.0.tar.gz", hash = "sha256:cd05e9e7191ba6cb68934d8eb76054d9be4562aa89dbc4236feee4d7d51342b2", size = 5535488, upload-time = "2024-12-05T01:35:06.49Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/94/6db383d8ee1adf45dc6c73477152b82731fa4c4a46d9c1932cc8757e0fd4/google_cloud_storage-2.19.0-py2.py3-none-any.whl", hash = "sha256:aeb971b5c29cf8ab98445082cbfe7b161a1f48ed275822f59ed3f1524ea54fba", size = 131787 }, + { url = "https://files.pythonhosted.org/packages/d5/94/6db383d8ee1adf45dc6c73477152b82731fa4c4a46d9c1932cc8757e0fd4/google_cloud_storage-2.19.0-py2.py3-none-any.whl", hash = "sha256:aeb971b5c29cf8ab98445082cbfe7b161a1f48ed275822f59ed3f1524ea54fba", size = 131787, upload-time = "2024-12-05T01:35:04.736Z" }, ] [[package]] name = "google-crc32c" version = "1.7.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/ae/87802e6d9f9d69adfaedfcfd599266bf386a54d0be058b532d04c794f76d/google_crc32c-1.7.1.tar.gz", hash = "sha256:2bff2305f98846f3e825dbeec9ee406f89da7962accdb29356e4eadc251bd472", size = 14495 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/69/b1b05cf415df0d86691d6a8b4b7e60ab3a6fb6efb783ee5cd3ed1382bfd3/google_crc32c-1.7.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:b07d48faf8292b4db7c3d64ab86f950c2e94e93a11fd47271c28ba458e4a0d76", size = 30467 }, - { url = "https://files.pythonhosted.org/packages/44/3d/92f8928ecd671bd5b071756596971c79d252d09b835cdca5a44177fa87aa/google_crc32c-1.7.1-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:7cc81b3a2fbd932a4313eb53cc7d9dde424088ca3a0337160f35d91826880c1d", size = 30311 }, - { url = "https://files.pythonhosted.org/packages/33/42/c2d15a73df79d45ed6b430b9e801d0bd8e28ac139a9012d7d58af50a385d/google_crc32c-1.7.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1c67ca0a1f5b56162951a9dae987988679a7db682d6f97ce0f6381ebf0fbea4c", size = 37889 }, - { url = "https://files.pythonhosted.org/packages/57/ea/ac59c86a3c694afd117bb669bde32aaf17d0de4305d01d706495f09cbf19/google_crc32c-1.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc5319db92daa516b653600794d5b9f9439a9a121f3e162f94b0e1891c7933cb", size = 33028 }, - { url = "https://files.pythonhosted.org/packages/60/44/87e77e8476767a4a93f6cf271157c6d948eacec63688c093580af13b04be/google_crc32c-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcdf5a64adb747610140572ed18d011896e3b9ae5195f2514b7ff678c80f1603", size = 38026 }, - { url = "https://files.pythonhosted.org/packages/c8/bf/21ac7bb305cd7c1a6de9c52f71db0868e104a5b573a4977cd9d0ff830f82/google_crc32c-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:754561c6c66e89d55754106739e22fdaa93fafa8da7221b29c8b8e8270c6ec8a", size = 33476 }, - { url = "https://files.pythonhosted.org/packages/f7/94/220139ea87822b6fdfdab4fb9ba81b3fff7ea2c82e2af34adc726085bffc/google_crc32c-1.7.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:6fbab4b935989e2c3610371963ba1b86afb09537fd0c633049be82afe153ac06", size = 30468 }, - { url = "https://files.pythonhosted.org/packages/94/97/789b23bdeeb9d15dc2904660463ad539d0318286d7633fe2760c10ed0c1c/google_crc32c-1.7.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:ed66cbe1ed9cbaaad9392b5259b3eba4a9e565420d734e6238813c428c3336c9", size = 30313 }, - { url = "https://files.pythonhosted.org/packages/81/b8/976a2b843610c211e7ccb3e248996a61e87dbb2c09b1499847e295080aec/google_crc32c-1.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee6547b657621b6cbed3562ea7826c3e11cab01cd33b74e1f677690652883e77", size = 33048 }, - { url = "https://files.pythonhosted.org/packages/c9/16/a3842c2cf591093b111d4a5e2bfb478ac6692d02f1b386d2a33283a19dc9/google_crc32c-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d68e17bad8f7dd9a49181a1f5a8f4b251c6dbc8cc96fb79f1d321dfd57d66f53", size = 32669 }, - { url = "https://files.pythonhosted.org/packages/04/17/ed9aba495916fcf5fe4ecb2267ceb851fc5f273c4e4625ae453350cfd564/google_crc32c-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:6335de12921f06e1f774d0dd1fbea6bf610abe0887a1638f64d694013138be5d", size = 33476 }, - { url = "https://files.pythonhosted.org/packages/dd/b7/787e2453cf8639c94b3d06c9d61f512234a82e1d12d13d18584bd3049904/google_crc32c-1.7.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2d73a68a653c57281401871dd4aeebbb6af3191dcac751a76ce430df4d403194", size = 30470 }, - { url = "https://files.pythonhosted.org/packages/ed/b4/6042c2b0cbac3ec3a69bb4c49b28d2f517b7a0f4a0232603c42c58e22b44/google_crc32c-1.7.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:22beacf83baaf59f9d3ab2bbb4db0fb018da8e5aebdce07ef9f09fce8220285e", size = 30315 }, - { url = "https://files.pythonhosted.org/packages/29/ad/01e7a61a5d059bc57b702d9ff6a18b2585ad97f720bd0a0dbe215df1ab0e/google_crc32c-1.7.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19eafa0e4af11b0a4eb3974483d55d2d77ad1911e6cf6f832e1574f6781fd337", size = 33180 }, - { url = "https://files.pythonhosted.org/packages/3b/a5/7279055cf004561894ed3a7bfdf5bf90a53f28fadd01af7cd166e88ddf16/google_crc32c-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6d86616faaea68101195c6bdc40c494e4d76f41e07a37ffdef270879c15fb65", size = 32794 }, - { url = "https://files.pythonhosted.org/packages/0f/d6/77060dbd140c624e42ae3ece3df53b9d811000729a5c821b9fd671ceaac6/google_crc32c-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:b7491bdc0c7564fcf48c0179d2048ab2f7c7ba36b84ccd3a3e1c3f7a72d3bba6", size = 33477 }, - { url = "https://files.pythonhosted.org/packages/e3/89/940d170a9f24e6e711666a7c5596561358243023b4060869d9adae97a762/google_crc32c-1.7.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:9fc196f0b8d8bd2789352c6a522db03f89e83a0ed6b64315923c396d7a932315", size = 30462 }, - { url = "https://files.pythonhosted.org/packages/42/0c/22bebe2517368e914a63e5378aab74e2b6357eb739d94b6bc0e830979a37/google_crc32c-1.7.1-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:bb5e35dcd8552f76eed9461a23de1030920a3c953c1982f324be8f97946e7127", size = 30304 }, - { url = "https://files.pythonhosted.org/packages/36/32/2daf4c46f875aaa3a057ecc8569406979cb29fb1e2389e4f2570d8ed6a5c/google_crc32c-1.7.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f2226b6a8da04f1d9e61d3e357f2460b9551c5e6950071437e122c958a18ae14", size = 37734 }, - { url = "https://files.pythonhosted.org/packages/76/b5/b3e220b68d5d265c4aacd2878301fdb2df72715c45ba49acc19f310d4555/google_crc32c-1.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f2b3522222746fff0e04a9bd0a23ea003ba3cccc8cf21385c564deb1f223242", size = 32869 }, - { url = "https://files.pythonhosted.org/packages/0a/90/2931c3c8d2de1e7cde89945d3ceb2c4258a1f23f0c22c3c1c921c3c026a6/google_crc32c-1.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3bda0fcb632d390e3ea8b6b07bf6b4f4a66c9d02dcd6fbf7ba00a197c143f582", size = 37875 }, - { url = "https://files.pythonhosted.org/packages/30/9e/0aaed8a209ea6fa4b50f66fed2d977f05c6c799e10bb509f5523a5a5c90c/google_crc32c-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:713121af19f1a617054c41f952294764e0c5443d5a5d9034b2cd60f5dd7e0349", size = 33471 }, - { url = "https://files.pythonhosted.org/packages/0b/43/31e57ce04530794917dfe25243860ec141de9fadf4aa9783dffe7dac7c39/google_crc32c-1.7.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8e9afc74168b0b2232fb32dd202c93e46b7d5e4bf03e66ba5dc273bb3559589", size = 28242 }, - { url = "https://files.pythonhosted.org/packages/eb/f3/8b84cd4e0ad111e63e30eb89453f8dd308e3ad36f42305cf8c202461cdf0/google_crc32c-1.7.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa8136cc14dd27f34a3221c0f16fd42d8a40e4778273e61a3c19aedaa44daf6b", size = 28049 }, - { url = "https://files.pythonhosted.org/packages/16/1b/1693372bf423ada422f80fd88260dbfd140754adb15cbc4d7e9a68b1cb8e/google_crc32c-1.7.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85fef7fae11494e747c9fd1359a527e5970fc9603c90764843caabd3a16a0a48", size = 28241 }, - { url = "https://files.pythonhosted.org/packages/fd/3c/2a19a60a473de48717b4efb19398c3f914795b64a96cf3fbe82588044f78/google_crc32c-1.7.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6efb97eb4369d52593ad6f75e7e10d053cf00c48983f7a973105bc70b0ac4d82", size = 28048 }, +sdist = { url = "https://files.pythonhosted.org/packages/19/ae/87802e6d9f9d69adfaedfcfd599266bf386a54d0be058b532d04c794f76d/google_crc32c-1.7.1.tar.gz", hash = "sha256:2bff2305f98846f3e825dbeec9ee406f89da7962accdb29356e4eadc251bd472", size = 14495, upload-time = "2025-03-26T14:29:13.32Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/69/b1b05cf415df0d86691d6a8b4b7e60ab3a6fb6efb783ee5cd3ed1382bfd3/google_crc32c-1.7.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:b07d48faf8292b4db7c3d64ab86f950c2e94e93a11fd47271c28ba458e4a0d76", size = 30467, upload-time = "2025-03-26T14:31:11.92Z" }, + { url = "https://files.pythonhosted.org/packages/44/3d/92f8928ecd671bd5b071756596971c79d252d09b835cdca5a44177fa87aa/google_crc32c-1.7.1-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:7cc81b3a2fbd932a4313eb53cc7d9dde424088ca3a0337160f35d91826880c1d", size = 30311, upload-time = "2025-03-26T14:53:14.161Z" }, + { url = "https://files.pythonhosted.org/packages/33/42/c2d15a73df79d45ed6b430b9e801d0bd8e28ac139a9012d7d58af50a385d/google_crc32c-1.7.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1c67ca0a1f5b56162951a9dae987988679a7db682d6f97ce0f6381ebf0fbea4c", size = 37889, upload-time = "2025-03-26T14:41:27.83Z" }, + { url = "https://files.pythonhosted.org/packages/57/ea/ac59c86a3c694afd117bb669bde32aaf17d0de4305d01d706495f09cbf19/google_crc32c-1.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc5319db92daa516b653600794d5b9f9439a9a121f3e162f94b0e1891c7933cb", size = 33028, upload-time = "2025-03-26T14:41:29.141Z" }, + { url = "https://files.pythonhosted.org/packages/60/44/87e77e8476767a4a93f6cf271157c6d948eacec63688c093580af13b04be/google_crc32c-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcdf5a64adb747610140572ed18d011896e3b9ae5195f2514b7ff678c80f1603", size = 38026, upload-time = "2025-03-26T14:41:29.921Z" }, + { url = "https://files.pythonhosted.org/packages/c8/bf/21ac7bb305cd7c1a6de9c52f71db0868e104a5b573a4977cd9d0ff830f82/google_crc32c-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:754561c6c66e89d55754106739e22fdaa93fafa8da7221b29c8b8e8270c6ec8a", size = 33476, upload-time = "2025-03-26T14:29:09.086Z" }, + { url = "https://files.pythonhosted.org/packages/f7/94/220139ea87822b6fdfdab4fb9ba81b3fff7ea2c82e2af34adc726085bffc/google_crc32c-1.7.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:6fbab4b935989e2c3610371963ba1b86afb09537fd0c633049be82afe153ac06", size = 30468, upload-time = "2025-03-26T14:32:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/94/97/789b23bdeeb9d15dc2904660463ad539d0318286d7633fe2760c10ed0c1c/google_crc32c-1.7.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:ed66cbe1ed9cbaaad9392b5259b3eba4a9e565420d734e6238813c428c3336c9", size = 30313, upload-time = "2025-03-26T14:57:38.758Z" }, + { url = "https://files.pythonhosted.org/packages/81/b8/976a2b843610c211e7ccb3e248996a61e87dbb2c09b1499847e295080aec/google_crc32c-1.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee6547b657621b6cbed3562ea7826c3e11cab01cd33b74e1f677690652883e77", size = 33048, upload-time = "2025-03-26T14:41:30.679Z" }, + { url = "https://files.pythonhosted.org/packages/c9/16/a3842c2cf591093b111d4a5e2bfb478ac6692d02f1b386d2a33283a19dc9/google_crc32c-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d68e17bad8f7dd9a49181a1f5a8f4b251c6dbc8cc96fb79f1d321dfd57d66f53", size = 32669, upload-time = "2025-03-26T14:41:31.432Z" }, + { url = "https://files.pythonhosted.org/packages/04/17/ed9aba495916fcf5fe4ecb2267ceb851fc5f273c4e4625ae453350cfd564/google_crc32c-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:6335de12921f06e1f774d0dd1fbea6bf610abe0887a1638f64d694013138be5d", size = 33476, upload-time = "2025-03-26T14:29:10.211Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b7/787e2453cf8639c94b3d06c9d61f512234a82e1d12d13d18584bd3049904/google_crc32c-1.7.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2d73a68a653c57281401871dd4aeebbb6af3191dcac751a76ce430df4d403194", size = 30470, upload-time = "2025-03-26T14:34:31.655Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b4/6042c2b0cbac3ec3a69bb4c49b28d2f517b7a0f4a0232603c42c58e22b44/google_crc32c-1.7.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:22beacf83baaf59f9d3ab2bbb4db0fb018da8e5aebdce07ef9f09fce8220285e", size = 30315, upload-time = "2025-03-26T15:01:54.634Z" }, + { url = "https://files.pythonhosted.org/packages/29/ad/01e7a61a5d059bc57b702d9ff6a18b2585ad97f720bd0a0dbe215df1ab0e/google_crc32c-1.7.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19eafa0e4af11b0a4eb3974483d55d2d77ad1911e6cf6f832e1574f6781fd337", size = 33180, upload-time = "2025-03-26T14:41:32.168Z" }, + { url = "https://files.pythonhosted.org/packages/3b/a5/7279055cf004561894ed3a7bfdf5bf90a53f28fadd01af7cd166e88ddf16/google_crc32c-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6d86616faaea68101195c6bdc40c494e4d76f41e07a37ffdef270879c15fb65", size = 32794, upload-time = "2025-03-26T14:41:33.264Z" }, + { url = "https://files.pythonhosted.org/packages/0f/d6/77060dbd140c624e42ae3ece3df53b9d811000729a5c821b9fd671ceaac6/google_crc32c-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:b7491bdc0c7564fcf48c0179d2048ab2f7c7ba36b84ccd3a3e1c3f7a72d3bba6", size = 33477, upload-time = "2025-03-26T14:29:10.94Z" }, + { url = "https://files.pythonhosted.org/packages/e3/89/940d170a9f24e6e711666a7c5596561358243023b4060869d9adae97a762/google_crc32c-1.7.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:9fc196f0b8d8bd2789352c6a522db03f89e83a0ed6b64315923c396d7a932315", size = 30462, upload-time = "2025-03-26T14:29:25.969Z" }, + { url = "https://files.pythonhosted.org/packages/42/0c/22bebe2517368e914a63e5378aab74e2b6357eb739d94b6bc0e830979a37/google_crc32c-1.7.1-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:bb5e35dcd8552f76eed9461a23de1030920a3c953c1982f324be8f97946e7127", size = 30304, upload-time = "2025-03-26T14:49:16.642Z" }, + { url = "https://files.pythonhosted.org/packages/36/32/2daf4c46f875aaa3a057ecc8569406979cb29fb1e2389e4f2570d8ed6a5c/google_crc32c-1.7.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f2226b6a8da04f1d9e61d3e357f2460b9551c5e6950071437e122c958a18ae14", size = 37734, upload-time = "2025-03-26T14:41:37.88Z" }, + { url = "https://files.pythonhosted.org/packages/76/b5/b3e220b68d5d265c4aacd2878301fdb2df72715c45ba49acc19f310d4555/google_crc32c-1.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f2b3522222746fff0e04a9bd0a23ea003ba3cccc8cf21385c564deb1f223242", size = 32869, upload-time = "2025-03-26T14:41:38.965Z" }, + { url = "https://files.pythonhosted.org/packages/0a/90/2931c3c8d2de1e7cde89945d3ceb2c4258a1f23f0c22c3c1c921c3c026a6/google_crc32c-1.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3bda0fcb632d390e3ea8b6b07bf6b4f4a66c9d02dcd6fbf7ba00a197c143f582", size = 37875, upload-time = "2025-03-26T14:41:41.732Z" }, + { url = "https://files.pythonhosted.org/packages/30/9e/0aaed8a209ea6fa4b50f66fed2d977f05c6c799e10bb509f5523a5a5c90c/google_crc32c-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:713121af19f1a617054c41f952294764e0c5443d5a5d9034b2cd60f5dd7e0349", size = 33471, upload-time = "2025-03-26T14:29:12.578Z" }, + { url = "https://files.pythonhosted.org/packages/0b/43/31e57ce04530794917dfe25243860ec141de9fadf4aa9783dffe7dac7c39/google_crc32c-1.7.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8e9afc74168b0b2232fb32dd202c93e46b7d5e4bf03e66ba5dc273bb3559589", size = 28242, upload-time = "2025-03-26T14:41:42.858Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f3/8b84cd4e0ad111e63e30eb89453f8dd308e3ad36f42305cf8c202461cdf0/google_crc32c-1.7.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa8136cc14dd27f34a3221c0f16fd42d8a40e4778273e61a3c19aedaa44daf6b", size = 28049, upload-time = "2025-03-26T14:41:44.651Z" }, + { url = "https://files.pythonhosted.org/packages/16/1b/1693372bf423ada422f80fd88260dbfd140754adb15cbc4d7e9a68b1cb8e/google_crc32c-1.7.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85fef7fae11494e747c9fd1359a527e5970fc9603c90764843caabd3a16a0a48", size = 28241, upload-time = "2025-03-26T14:41:45.898Z" }, + { url = "https://files.pythonhosted.org/packages/fd/3c/2a19a60a473de48717b4efb19398c3f914795b64a96cf3fbe82588044f78/google_crc32c-1.7.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6efb97eb4369d52593ad6f75e7e10d053cf00c48983f7a973105bc70b0ac4d82", size = 28048, upload-time = "2025-03-26T14:41:46.696Z" }, ] [[package]] @@ -407,9 +407,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-crc32c" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/58/5a/0efdc02665dca14e0837b62c8a1a93132c264bd02054a15abb2218afe0ae/google_resumable_media-2.7.2.tar.gz", hash = "sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0", size = 2163099 } +sdist = { url = "https://files.pythonhosted.org/packages/58/5a/0efdc02665dca14e0837b62c8a1a93132c264bd02054a15abb2218afe0ae/google_resumable_media-2.7.2.tar.gz", hash = "sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0", size = 2163099, upload-time = "2024-08-07T22:20:38.555Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/35/b8d3baf8c46695858cb9d8835a53baa1eeb9906ddaf2f728a5f5b640fd1e/google_resumable_media-2.7.2-py2.py3-none-any.whl", hash = "sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa", size = 81251 }, + { url = "https://files.pythonhosted.org/packages/82/35/b8d3baf8c46695858cb9d8835a53baa1eeb9906ddaf2f728a5f5b640fd1e/google_resumable_media-2.7.2-py2.py3-none-any.whl", hash = "sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa", size = 81251, upload-time = "2024-08-07T22:20:36.409Z" }, ] [[package]] @@ -419,45 +419,45 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/39/24/33db22342cf4a2ea27c9955e6713140fedd51e8b141b5ce5260897020f1a/googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257", size = 145903 } +sdist = { url = "https://files.pythonhosted.org/packages/39/24/33db22342cf4a2ea27c9955e6713140fedd51e8b141b5ce5260897020f1a/googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257", size = 145903, upload-time = "2025-04-14T10:17:02.924Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/86/f1/62a193f0227cf15a920390abe675f386dec35f7ae3ffe6da582d3ade42c7/googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8", size = 294530 }, + { url = "https://files.pythonhosted.org/packages/86/f1/62a193f0227cf15a920390abe675f386dec35f7ae3ffe6da582d3ade42c7/googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8", size = 294530, upload-time = "2025-04-14T10:17:01.271Z" }, ] [[package]] name = "hf-transfer" version = "0.1.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1a/eb/8fc64f40388c29ce8ce3b2b180a089d4d6b25b1d0d232d016704cb852104/hf_transfer-0.1.9.tar.gz", hash = "sha256:035572865dab29d17e783fbf1e84cf1cb24f3fcf8f1b17db1cfc7fdf139f02bf", size = 25201 } +sdist = { url = "https://files.pythonhosted.org/packages/1a/eb/8fc64f40388c29ce8ce3b2b180a089d4d6b25b1d0d232d016704cb852104/hf_transfer-0.1.9.tar.gz", hash = "sha256:035572865dab29d17e783fbf1e84cf1cb24f3fcf8f1b17db1cfc7fdf139f02bf", size = 25201, upload-time = "2025-01-07T10:05:12.947Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/f5/461d2e5f307e5048289b1168d5c642ae3bb2504e88dff1a38b92ed990a21/hf_transfer-0.1.9-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e66acf91df4a8b72f60223059df3003062a5ae111757187ed1a06750a30e911b", size = 1393046 }, - { url = "https://files.pythonhosted.org/packages/41/ba/8d9fd9f1083525edfcb389c93738c802f3559cb749324090d7109c8bf4c2/hf_transfer-0.1.9-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:8669dbcc7a3e2e8d61d42cd24da9c50d57770bd74b445c65123291ca842a7e7a", size = 1348126 }, - { url = "https://files.pythonhosted.org/packages/8e/a2/cd7885bc9959421065a6fae0fe67b6c55becdeda4e69b873e52976f9a9f0/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fd0167c4407a3bc4cdd0307e65ada2294ec04f1813d8a69a5243e379b22e9d8", size = 3728604 }, - { url = "https://files.pythonhosted.org/packages/f6/2e/a072cf196edfeda3310c9a5ade0a0fdd785e6154b3ce24fc738c818da2a7/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ee8b10afedcb75f71091bcc197c526a6ebf5c58bbbadb34fdeee6160f55f619f", size = 3064995 }, - { url = "https://files.pythonhosted.org/packages/c2/84/aec9ef4c0fab93c1ea2b1badff38c78b4b2f86f0555b26d2051dbc920cde/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5828057e313de59300dd1abb489444bc452efe3f479d3c55b31a8f680936ba42", size = 3580908 }, - { url = "https://files.pythonhosted.org/packages/29/63/b560d39651a56603d64f1a0212d0472a44cbd965db2fa62b99d99cb981bf/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc6bd19e1cc177c66bdef15ef8636ad3bde79d5a4f608c158021153b4573509d", size = 3400839 }, - { url = "https://files.pythonhosted.org/packages/d6/d8/f87ea6f42456254b48915970ed98e993110521e9263472840174d32c880d/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdca9bfb89e6f8f281890cc61a8aff2d3cecaff7e1a4d275574d96ca70098557", size = 3552664 }, - { url = "https://files.pythonhosted.org/packages/d6/56/1267c39b65fc8f4e2113b36297320f102718bf5799b544a6cbe22013aa1d/hf_transfer-0.1.9-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:89a23f58b7b7effbc047b8ca286f131b17728c99a9f972723323003ffd1bb916", size = 4073732 }, - { url = "https://files.pythonhosted.org/packages/82/1a/9c748befbe3decf7cb415e34f8a0c3789a0a9c55910dea73d581e48c0ce5/hf_transfer-0.1.9-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:dc7fff1345980d6c0ebb92c811d24afa4b98b3e07ed070c8e38cc91fd80478c5", size = 3390096 }, - { url = "https://files.pythonhosted.org/packages/72/85/4c03da147b6b4b7cb12e074d3d44eee28604a387ed0eaf7eaaead5069c57/hf_transfer-0.1.9-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:1a6bd16c667ebe89a069ca163060127a794fa3a3525292c900b8c8cc47985b0d", size = 3664743 }, - { url = "https://files.pythonhosted.org/packages/e7/6e/e597b04f753f1b09e6893075d53a82a30c13855cbaa791402695b01e369f/hf_transfer-0.1.9-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d2fde99d502093ade3ab1b53f80da18480e9902aa960dab7f74fb1b9e5bc5746", size = 3695243 }, - { url = "https://files.pythonhosted.org/packages/09/89/d4e234727a26b2546c8fb70a276cd924260d60135f2165bf8b9ed67bb9a4/hf_transfer-0.1.9-cp38-abi3-win32.whl", hash = "sha256:435cc3cdc8524ce57b074032b8fd76eed70a4224d2091232fa6a8cef8fd6803e", size = 1086605 }, - { url = "https://files.pythonhosted.org/packages/a1/14/f1e15b851d1c2af5b0b1a82bf8eb10bda2da62d98180220ba6fd8879bb5b/hf_transfer-0.1.9-cp38-abi3-win_amd64.whl", hash = "sha256:16f208fc678911c37e11aa7b586bc66a37d02e636208f18b6bc53d29b5df40ad", size = 1160240 }, + { url = "https://files.pythonhosted.org/packages/81/f5/461d2e5f307e5048289b1168d5c642ae3bb2504e88dff1a38b92ed990a21/hf_transfer-0.1.9-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e66acf91df4a8b72f60223059df3003062a5ae111757187ed1a06750a30e911b", size = 1393046, upload-time = "2025-01-07T10:04:51.003Z" }, + { url = "https://files.pythonhosted.org/packages/41/ba/8d9fd9f1083525edfcb389c93738c802f3559cb749324090d7109c8bf4c2/hf_transfer-0.1.9-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:8669dbcc7a3e2e8d61d42cd24da9c50d57770bd74b445c65123291ca842a7e7a", size = 1348126, upload-time = "2025-01-07T10:04:45.712Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a2/cd7885bc9959421065a6fae0fe67b6c55becdeda4e69b873e52976f9a9f0/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fd0167c4407a3bc4cdd0307e65ada2294ec04f1813d8a69a5243e379b22e9d8", size = 3728604, upload-time = "2025-01-07T10:04:14.173Z" }, + { url = "https://files.pythonhosted.org/packages/f6/2e/a072cf196edfeda3310c9a5ade0a0fdd785e6154b3ce24fc738c818da2a7/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ee8b10afedcb75f71091bcc197c526a6ebf5c58bbbadb34fdeee6160f55f619f", size = 3064995, upload-time = "2025-01-07T10:04:18.663Z" }, + { url = "https://files.pythonhosted.org/packages/c2/84/aec9ef4c0fab93c1ea2b1badff38c78b4b2f86f0555b26d2051dbc920cde/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5828057e313de59300dd1abb489444bc452efe3f479d3c55b31a8f680936ba42", size = 3580908, upload-time = "2025-01-07T10:04:32.834Z" }, + { url = "https://files.pythonhosted.org/packages/29/63/b560d39651a56603d64f1a0212d0472a44cbd965db2fa62b99d99cb981bf/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc6bd19e1cc177c66bdef15ef8636ad3bde79d5a4f608c158021153b4573509d", size = 3400839, upload-time = "2025-01-07T10:04:26.122Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d8/f87ea6f42456254b48915970ed98e993110521e9263472840174d32c880d/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdca9bfb89e6f8f281890cc61a8aff2d3cecaff7e1a4d275574d96ca70098557", size = 3552664, upload-time = "2025-01-07T10:04:40.123Z" }, + { url = "https://files.pythonhosted.org/packages/d6/56/1267c39b65fc8f4e2113b36297320f102718bf5799b544a6cbe22013aa1d/hf_transfer-0.1.9-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:89a23f58b7b7effbc047b8ca286f131b17728c99a9f972723323003ffd1bb916", size = 4073732, upload-time = "2025-01-07T10:04:55.624Z" }, + { url = "https://files.pythonhosted.org/packages/82/1a/9c748befbe3decf7cb415e34f8a0c3789a0a9c55910dea73d581e48c0ce5/hf_transfer-0.1.9-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:dc7fff1345980d6c0ebb92c811d24afa4b98b3e07ed070c8e38cc91fd80478c5", size = 3390096, upload-time = "2025-01-07T10:04:59.98Z" }, + { url = "https://files.pythonhosted.org/packages/72/85/4c03da147b6b4b7cb12e074d3d44eee28604a387ed0eaf7eaaead5069c57/hf_transfer-0.1.9-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:1a6bd16c667ebe89a069ca163060127a794fa3a3525292c900b8c8cc47985b0d", size = 3664743, upload-time = "2025-01-07T10:05:05.416Z" }, + { url = "https://files.pythonhosted.org/packages/e7/6e/e597b04f753f1b09e6893075d53a82a30c13855cbaa791402695b01e369f/hf_transfer-0.1.9-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d2fde99d502093ade3ab1b53f80da18480e9902aa960dab7f74fb1b9e5bc5746", size = 3695243, upload-time = "2025-01-07T10:05:11.411Z" }, + { url = "https://files.pythonhosted.org/packages/09/89/d4e234727a26b2546c8fb70a276cd924260d60135f2165bf8b9ed67bb9a4/hf_transfer-0.1.9-cp38-abi3-win32.whl", hash = "sha256:435cc3cdc8524ce57b074032b8fd76eed70a4224d2091232fa6a8cef8fd6803e", size = 1086605, upload-time = "2025-01-07T10:05:18.873Z" }, + { url = "https://files.pythonhosted.org/packages/a1/14/f1e15b851d1c2af5b0b1a82bf8eb10bda2da62d98180220ba6fd8879bb5b/hf_transfer-0.1.9-cp38-abi3-win_amd64.whl", hash = "sha256:16f208fc678911c37e11aa7b586bc66a37d02e636208f18b6bc53d29b5df40ad", size = 1160240, upload-time = "2025-01-07T10:05:14.324Z" }, ] [[package]] name = "hf-xet" version = "1.1.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/75/dc/dc091aeeb671e71cbec30e84963f9c0202c17337b24b0a800e7d205543e8/hf_xet-1.1.3.tar.gz", hash = "sha256:a5f09b1dd24e6ff6bcedb4b0ddab2d81824098bb002cf8b4ffa780545fa348c3", size = 488127 } +sdist = { url = "https://files.pythonhosted.org/packages/75/dc/dc091aeeb671e71cbec30e84963f9c0202c17337b24b0a800e7d205543e8/hf_xet-1.1.3.tar.gz", hash = "sha256:a5f09b1dd24e6ff6bcedb4b0ddab2d81824098bb002cf8b4ffa780545fa348c3", size = 488127, upload-time = "2025-06-04T00:47:27.456Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/1f/bc01a4c0894973adebbcd4aa338a06815c76333ebb3921d94dcbd40dae6a/hf_xet-1.1.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c3b508b5f583a75641aebf732853deb058953370ce8184f5dabc49f803b0819b", size = 2256929 }, - { url = "https://files.pythonhosted.org/packages/78/07/6ef50851b5c6b45b77a6e018fa299c69a2db3b8bbd0d5af594c0238b1ceb/hf_xet-1.1.3-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:b788a61977fbe6b5186e66239e2a329a3f0b7e7ff50dad38984c0c74f44aeca1", size = 2153719 }, - { url = "https://files.pythonhosted.org/packages/52/48/e929e6e3db6e4758c2adf0f2ca2c59287f1b76229d8bdc1a4c9cfc05212e/hf_xet-1.1.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd2da210856444a34aad8ada2fc12f70dabed7cc20f37e90754d1d9b43bc0534", size = 4820519 }, - { url = "https://files.pythonhosted.org/packages/28/2e/03f89c5014a5aafaa9b150655f811798a317036646623bdaace25f485ae8/hf_xet-1.1.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8203f52827e3df65981984936654a5b390566336956f65765a8aa58c362bb841", size = 4964121 }, - { url = "https://files.pythonhosted.org/packages/47/8b/5cd399a92b47d98086f55fc72d69bc9ea5e5c6f27a9ed3e0cdd6be4e58a3/hf_xet-1.1.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:30c575a5306f8e6fda37edb866762140a435037365eba7a17ce7bd0bc0216a8b", size = 5283017 }, - { url = "https://files.pythonhosted.org/packages/53/e3/2fcec58d2fcfd25ff07feb876f466cfa11f8dcf9d3b742c07fe9dd51ee0a/hf_xet-1.1.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7c1a6aa6abed1f696f8099aa9796ca04c9ee778a58728a115607de9cc4638ff1", size = 4970349 }, - { url = "https://files.pythonhosted.org/packages/53/bf/10ca917e335861101017ff46044c90e517b574fbb37219347b83be1952f6/hf_xet-1.1.3-cp37-abi3-win_amd64.whl", hash = "sha256:b578ae5ac9c056296bb0df9d018e597c8dc6390c5266f35b5c44696003cde9f3", size = 2310934 }, + { url = "https://files.pythonhosted.org/packages/9b/1f/bc01a4c0894973adebbcd4aa338a06815c76333ebb3921d94dcbd40dae6a/hf_xet-1.1.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c3b508b5f583a75641aebf732853deb058953370ce8184f5dabc49f803b0819b", size = 2256929, upload-time = "2025-06-04T00:47:21.206Z" }, + { url = "https://files.pythonhosted.org/packages/78/07/6ef50851b5c6b45b77a6e018fa299c69a2db3b8bbd0d5af594c0238b1ceb/hf_xet-1.1.3-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:b788a61977fbe6b5186e66239e2a329a3f0b7e7ff50dad38984c0c74f44aeca1", size = 2153719, upload-time = "2025-06-04T00:47:19.302Z" }, + { url = "https://files.pythonhosted.org/packages/52/48/e929e6e3db6e4758c2adf0f2ca2c59287f1b76229d8bdc1a4c9cfc05212e/hf_xet-1.1.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd2da210856444a34aad8ada2fc12f70dabed7cc20f37e90754d1d9b43bc0534", size = 4820519, upload-time = "2025-06-04T00:47:17.244Z" }, + { url = "https://files.pythonhosted.org/packages/28/2e/03f89c5014a5aafaa9b150655f811798a317036646623bdaace25f485ae8/hf_xet-1.1.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8203f52827e3df65981984936654a5b390566336956f65765a8aa58c362bb841", size = 4964121, upload-time = "2025-06-04T00:47:15.17Z" }, + { url = "https://files.pythonhosted.org/packages/47/8b/5cd399a92b47d98086f55fc72d69bc9ea5e5c6f27a9ed3e0cdd6be4e58a3/hf_xet-1.1.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:30c575a5306f8e6fda37edb866762140a435037365eba7a17ce7bd0bc0216a8b", size = 5283017, upload-time = "2025-06-04T00:47:23.239Z" }, + { url = "https://files.pythonhosted.org/packages/53/e3/2fcec58d2fcfd25ff07feb876f466cfa11f8dcf9d3b742c07fe9dd51ee0a/hf_xet-1.1.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7c1a6aa6abed1f696f8099aa9796ca04c9ee778a58728a115607de9cc4638ff1", size = 4970349, upload-time = "2025-06-04T00:47:25.383Z" }, + { url = "https://files.pythonhosted.org/packages/53/bf/10ca917e335861101017ff46044c90e517b574fbb37219347b83be1952f6/hf_xet-1.1.3-cp37-abi3-win_amd64.whl", hash = "sha256:b578ae5ac9c056296bb0df9d018e597c8dc6390c5266f35b5c44696003cde9f3", size = 2310934, upload-time = "2025-06-04T00:47:29.632Z" }, ] [[package]] @@ -474,9 +474,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/c8/4f7d270285c46324fd66f62159eb16739aa5696f422dba57678a8c6b78e9/huggingface_hub-0.32.4.tar.gz", hash = "sha256:f61d45cd338736f59fb0e97550b74c24ee771bcc92c05ae0766b9116abe720be", size = 424494 } +sdist = { url = "https://files.pythonhosted.org/packages/60/c8/4f7d270285c46324fd66f62159eb16739aa5696f422dba57678a8c6b78e9/huggingface_hub-0.32.4.tar.gz", hash = "sha256:f61d45cd338736f59fb0e97550b74c24ee771bcc92c05ae0766b9116abe720be", size = 424494, upload-time = "2025-06-03T09:59:46.105Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/67/8b/222140f3cfb6f17b0dd8c4b9a0b36bd4ebefe9fb0098ba35d6960abcda0f/huggingface_hub-0.32.4-py3-none-any.whl", hash = "sha256:37abf8826b38d971f60d3625229221c36e53fe58060286db9baf619cfbf39767", size = 512101 }, + { url = "https://files.pythonhosted.org/packages/67/8b/222140f3cfb6f17b0dd8c4b9a0b36bd4ebefe9fb0098ba35d6960abcda0f/huggingface_hub-0.32.4-py3-none-any.whl", hash = "sha256:37abf8826b38d971f60d3625229221c36e53fe58060286db9baf619cfbf39767", size = 512101, upload-time = "2025-06-03T09:59:44.099Z" }, ] [package.optional-dependencies] @@ -488,32 +488,32 @@ hf-transfer = [ name = "idna" version = "3.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] [[package]] name = "isodate" version = "0.7.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705 } +sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320 }, + { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, ] [[package]] name = "jmespath" version = "1.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843 } +sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843, upload-time = "2022-06-17T18:00:12.224Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256 }, + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" }, ] [[package]] name = "kserve-storage" -version = "1" +version = "0.15.2" source = { virtual = "." } dependencies = [ { name = "azure-identity" }, @@ -532,7 +532,7 @@ requires-dist = [ { name = "azure-storage-file-share", specifier = ">=12.16.0,<13.0.0" }, { name = "boto3", specifier = ">=1.29.0,<2.0.0" }, { name = "google-cloud-storage", specifier = ">=2.14.0,<3.0.0" }, - { name = "huggingface-hub", extras = ["hf-transfer"], specifier = ">=0.30.0,<1.0.0" }, + { name = "huggingface-hub", extras = ["hf-transfer"], specifier = ">=0.32.0" }, { name = "requests", specifier = ">=2.32.2,<3.0.0" }, ] @@ -545,9 +545,9 @@ dependencies = [ { name = "pyjwt", extra = ["crypto"] }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3f/90/81dcc50f0be11a8c4dcbae1a9f761a26e5f905231330a7cacc9f04ec4c61/msal-1.32.3.tar.gz", hash = "sha256:5eea038689c78a5a70ca8ecbe1245458b55a857bd096efb6989c69ba15985d35", size = 151449 } +sdist = { url = "https://files.pythonhosted.org/packages/3f/90/81dcc50f0be11a8c4dcbae1a9f761a26e5f905231330a7cacc9f04ec4c61/msal-1.32.3.tar.gz", hash = "sha256:5eea038689c78a5a70ca8ecbe1245458b55a857bd096efb6989c69ba15985d35", size = 151449, upload-time = "2025-04-25T13:12:34.204Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/bf/81516b9aac7fd867709984d08eb4db1d2e3fe1df795c8e442cde9b568962/msal-1.32.3-py3-none-any.whl", hash = "sha256:b2798db57760b1961b142f027ffb7c8169536bf77316e99a0df5c4aaebb11569", size = 115358 }, + { url = "https://files.pythonhosted.org/packages/04/bf/81516b9aac7fd867709984d08eb4db1d2e3fe1df795c8e442cde9b568962/msal-1.32.3-py3-none-any.whl", hash = "sha256:b2798db57760b1961b142f027ffb7c8169536bf77316e99a0df5c4aaebb11569", size = 115358, upload-time = "2025-04-25T13:12:33.034Z" }, ] [[package]] @@ -557,18 +557,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "msal" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/01/99/5d239b6156eddf761a636bded1118414d161bd6b7b37a9335549ed159396/msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4", size = 23315 } +sdist = { url = "https://files.pythonhosted.org/packages/01/99/5d239b6156eddf761a636bded1118414d161bd6b7b37a9335549ed159396/msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4", size = 23315, upload-time = "2025-03-14T23:51:03.902Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583 }, + { url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583, upload-time = "2025-03-14T23:51:03.016Z" }, ] [[package]] name = "packaging" version = "25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] [[package]] @@ -578,34 +578,34 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/ac/87285f15f7cce6d4a008f33f1757fb5a13611ea8914eb58c3d0d26243468/proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012", size = 56142 } +sdist = { url = "https://files.pythonhosted.org/packages/f4/ac/87285f15f7cce6d4a008f33f1757fb5a13611ea8914eb58c3d0d26243468/proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012", size = 56142, upload-time = "2025-03-10T15:54:38.843Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/6d/280c4c2ce28b1593a19ad5239c8b826871fc6ec275c21afc8e1820108039/proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66", size = 50163 }, + { url = "https://files.pythonhosted.org/packages/4e/6d/280c4c2ce28b1593a19ad5239c8b826871fc6ec275c21afc8e1820108039/proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66", size = 50163, upload-time = "2025-03-10T15:54:37.335Z" }, ] [[package]] name = "protobuf" version = "6.31.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/f3/b9655a711b32c19720253f6f06326faf90580834e2e83f840472d752bc8b/protobuf-6.31.1.tar.gz", hash = "sha256:d8cac4c982f0b957a4dc73a80e2ea24fab08e679c0de9deb835f4a12d69aca9a", size = 441797 } +sdist = { url = "https://files.pythonhosted.org/packages/52/f3/b9655a711b32c19720253f6f06326faf90580834e2e83f840472d752bc8b/protobuf-6.31.1.tar.gz", hash = "sha256:d8cac4c982f0b957a4dc73a80e2ea24fab08e679c0de9deb835f4a12d69aca9a", size = 441797, upload-time = "2025-05-28T19:25:54.947Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/6f/6ab8e4bf962fd5570d3deaa2d5c38f0a363f57b4501047b5ebeb83ab1125/protobuf-6.31.1-cp310-abi3-win32.whl", hash = "sha256:7fa17d5a29c2e04b7d90e5e32388b8bfd0e7107cd8e616feef7ed3fa6bdab5c9", size = 423603 }, - { url = "https://files.pythonhosted.org/packages/44/3a/b15c4347dd4bf3a1b0ee882f384623e2063bb5cf9fa9d57990a4f7df2fb6/protobuf-6.31.1-cp310-abi3-win_amd64.whl", hash = "sha256:426f59d2964864a1a366254fa703b8632dcec0790d8862d30034d8245e1cd447", size = 435283 }, - { url = "https://files.pythonhosted.org/packages/6a/c9/b9689a2a250264a84e66c46d8862ba788ee7a641cdca39bccf64f59284b7/protobuf-6.31.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:6f1227473dc43d44ed644425268eb7c2e488ae245d51c6866d19fe158e207402", size = 425604 }, - { url = "https://files.pythonhosted.org/packages/76/a1/7a5a94032c83375e4fe7e7f56e3976ea6ac90c5e85fac8576409e25c39c3/protobuf-6.31.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:a40fc12b84c154884d7d4c4ebd675d5b3b5283e155f324049ae396b95ddebc39", size = 322115 }, - { url = "https://files.pythonhosted.org/packages/fa/b1/b59d405d64d31999244643d88c45c8241c58f17cc887e73bcb90602327f8/protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4ee898bf66f7a8b0bd21bce523814e6fbd8c6add948045ce958b73af7e8878c6", size = 321070 }, - { url = "https://files.pythonhosted.org/packages/b1/f0/4160dbd205eee8fdf8647d154e7ceaa9d25b3a877b6311274eb6dc896b75/protobuf-6.31.1-cp39-cp39-win32.whl", hash = "sha256:0414e3aa5a5f3ff423828e1e6a6e907d6c65c1d5b7e6e975793d5590bdeecc16", size = 423626 }, - { url = "https://files.pythonhosted.org/packages/09/34/13989eb9f482409ed821bfa3e34e6a3878b42607c38e7f7572b4cc825091/protobuf-6.31.1-cp39-cp39-win_amd64.whl", hash = "sha256:8764cf4587791e7564051b35524b72844f845ad0bb011704c3736cce762d8fe9", size = 435347 }, - { url = "https://files.pythonhosted.org/packages/f7/af/ab3c51ab7507a7325e98ffe691d9495ee3d3aa5f589afad65ec920d39821/protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e", size = 168724 }, + { url = "https://files.pythonhosted.org/packages/f3/6f/6ab8e4bf962fd5570d3deaa2d5c38f0a363f57b4501047b5ebeb83ab1125/protobuf-6.31.1-cp310-abi3-win32.whl", hash = "sha256:7fa17d5a29c2e04b7d90e5e32388b8bfd0e7107cd8e616feef7ed3fa6bdab5c9", size = 423603, upload-time = "2025-05-28T19:25:41.198Z" }, + { url = "https://files.pythonhosted.org/packages/44/3a/b15c4347dd4bf3a1b0ee882f384623e2063bb5cf9fa9d57990a4f7df2fb6/protobuf-6.31.1-cp310-abi3-win_amd64.whl", hash = "sha256:426f59d2964864a1a366254fa703b8632dcec0790d8862d30034d8245e1cd447", size = 435283, upload-time = "2025-05-28T19:25:44.275Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c9/b9689a2a250264a84e66c46d8862ba788ee7a641cdca39bccf64f59284b7/protobuf-6.31.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:6f1227473dc43d44ed644425268eb7c2e488ae245d51c6866d19fe158e207402", size = 425604, upload-time = "2025-05-28T19:25:45.702Z" }, + { url = "https://files.pythonhosted.org/packages/76/a1/7a5a94032c83375e4fe7e7f56e3976ea6ac90c5e85fac8576409e25c39c3/protobuf-6.31.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:a40fc12b84c154884d7d4c4ebd675d5b3b5283e155f324049ae396b95ddebc39", size = 322115, upload-time = "2025-05-28T19:25:47.128Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/b59d405d64d31999244643d88c45c8241c58f17cc887e73bcb90602327f8/protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4ee898bf66f7a8b0bd21bce523814e6fbd8c6add948045ce958b73af7e8878c6", size = 321070, upload-time = "2025-05-28T19:25:50.036Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f0/4160dbd205eee8fdf8647d154e7ceaa9d25b3a877b6311274eb6dc896b75/protobuf-6.31.1-cp39-cp39-win32.whl", hash = "sha256:0414e3aa5a5f3ff423828e1e6a6e907d6c65c1d5b7e6e975793d5590bdeecc16", size = 423626, upload-time = "2025-05-28T19:25:51.355Z" }, + { url = "https://files.pythonhosted.org/packages/09/34/13989eb9f482409ed821bfa3e34e6a3878b42607c38e7f7572b4cc825091/protobuf-6.31.1-cp39-cp39-win_amd64.whl", hash = "sha256:8764cf4587791e7564051b35524b72844f845ad0bb011704c3736cce762d8fe9", size = 435347, upload-time = "2025-05-28T19:25:52.932Z" }, + { url = "https://files.pythonhosted.org/packages/f7/af/ab3c51ab7507a7325e98ffe691d9495ee3d3aa5f589afad65ec920d39821/protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e", size = 168724, upload-time = "2025-05-28T19:25:53.926Z" }, ] [[package]] name = "pyasn1" version = "0.6.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 }, + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, ] [[package]] @@ -615,27 +615,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyasn1" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892 } +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259 }, + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, ] [[package]] name = "pycparser" version = "2.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, ] [[package]] name = "pyjwt" version = "2.10.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785 } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997 }, + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, ] [package.optional-dependencies] @@ -650,53 +650,53 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] name = "pyyaml" version = "6.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, - { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, - { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, - { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, - { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, - { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, - { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, - { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, - { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, - { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, - { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, - { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, - { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, - { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, - { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, - { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, - { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, - { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, - { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777 }, - { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318 }, - { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891 }, - { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614 }, - { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360 }, - { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006 }, - { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577 }, - { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593 }, - { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312 }, +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777, upload-time = "2024-08-06T20:33:25.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318, upload-time = "2024-08-06T20:33:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891, upload-time = "2024-08-06T20:33:28.974Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614, upload-time = "2024-08-06T20:33:34.157Z" }, + { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360, upload-time = "2024-08-06T20:33:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006, upload-time = "2024-08-06T20:33:37.501Z" }, + { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577, upload-time = "2024-08-06T20:33:39.389Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593, upload-time = "2024-08-06T20:33:46.63Z" }, + { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312, upload-time = "2024-08-06T20:33:49.073Z" }, ] [[package]] @@ -710,9 +710,9 @@ dependencies = [ { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "urllib3", version = "2.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258 } +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847 }, + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, ] [[package]] @@ -722,9 +722,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyasn1" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034 } +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696 }, + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, ] [[package]] @@ -734,18 +734,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ed/5d/9dcc100abc6711e8247af5aa561fc07c4a046f72f659c3adea9a449e191a/s3transfer-0.13.0.tar.gz", hash = "sha256:f5e6db74eb7776a37208001113ea7aa97695368242b364d73e91c981ac522177", size = 150232 } +sdist = { url = "https://files.pythonhosted.org/packages/ed/5d/9dcc100abc6711e8247af5aa561fc07c4a046f72f659c3adea9a449e191a/s3transfer-0.13.0.tar.gz", hash = "sha256:f5e6db74eb7776a37208001113ea7aa97695368242b364d73e91c981ac522177", size = 150232, upload-time = "2025-05-22T19:24:50.245Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/17/22bf8155aa0ea2305eefa3a6402e040df7ebe512d1310165eda1e233c3f8/s3transfer-0.13.0-py3-none-any.whl", hash = "sha256:0148ef34d6dd964d0d8cf4311b2b21c474693e57c2e069ec708ce043d2b527be", size = 85152 }, + { url = "https://files.pythonhosted.org/packages/18/17/22bf8155aa0ea2305eefa3a6402e040df7ebe512d1310165eda1e233c3f8/s3transfer-0.13.0-py3-none-any.whl", hash = "sha256:0148ef34d6dd964d0d8cf4311b2b21c474693e57c2e069ec708ce043d2b527be", size = 85152, upload-time = "2025-05-22T19:24:48.703Z" }, ] [[package]] name = "six" version = "1.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] [[package]] @@ -755,18 +755,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, ] [[package]] name = "typing-extensions" version = "4.14.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423 } +sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839 }, + { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, ] [[package]] @@ -776,9 +776,9 @@ source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] -sdist = { url = "https://files.pythonhosted.org/packages/e4/e8/6ff5e6bc22095cfc59b6ea711b687e2b7ed4bdb373f7eeec370a97d7392f/urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32", size = 307380 } +sdist = { url = "https://files.pythonhosted.org/packages/e4/e8/6ff5e6bc22095cfc59b6ea711b687e2b7ed4bdb373f7eeec370a97d7392f/urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32", size = 307380, upload-time = "2024-08-29T15:43:11.37Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/cf/8435d5a7159e2a9c83a95896ed596f68cf798005fe107cc655b5c5c14704/urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", size = 144225 }, + { url = "https://files.pythonhosted.org/packages/33/cf/8435d5a7159e2a9c83a95896ed596f68cf798005fe107cc655b5c5c14704/urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", size = 144225, upload-time = "2024-08-29T15:43:08.921Z" }, ] [[package]] @@ -788,7 +788,7 @@ source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.10'", ] -sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" }, ] diff --git a/python/test_resources/graph/error_404_isvc/uv.lock b/python/test_resources/graph/error_404_isvc/uv.lock index 79a090ae961..df4d2531e0d 100644 --- a/python/test_resources/graph/error_404_isvc/uv.lock +++ b/python/test_resources/graph/error_404_isvc/uv.lock @@ -486,18 +486,13 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "asgi-logger", marker = "extra == 'logging'", specifier = ">=0.1.0,<1.0.0" }, - { name = "azure-identity", marker = "extra == 'storage'", specifier = ">=1.15.0,<2.0.0" }, - { name = "azure-storage-blob", marker = "extra == 'storage'", specifier = ">=12.20.0,<13.0.0" }, - { name = "azure-storage-file-share", marker = "extra == 'storage'", specifier = ">=12.16.0,<13.0.0" }, - { name = "boto3", marker = "extra == 'storage'", specifier = ">=1.29.0,<2.0.0" }, { name = "cloudevents", specifier = ">=1.6.2,<2.0.0" }, { name = "fastapi", specifier = ">=0.115.3" }, - { name = "google-cloud-storage", marker = "extra == 'storage'", specifier = ">=2.14.0,<3.0.0" }, { name = "grpc-interceptor", specifier = ">=0.15.4,<1.0.0" }, { name = "grpcio", specifier = ">=1.64.1,<2.0.0" }, { name = "grpcio-tools", specifier = ">=1.64.1,<2.0.0" }, { name = "httpx", specifier = ">=0.27.2,<1.0.0" }, - { name = "huggingface-hub", extras = ["hf-transfer"], marker = "extra == 'storage'", specifier = ">=0.32.0" }, + { name = "kserve-storage", marker = "extra == 'storage'", specifier = "==2" }, { name = "kubernetes", specifier = ">=23.3.0" }, { name = "numpy", specifier = ">=1.26.0,<3.0.0" }, { name = "orjson", specifier = ">=3.9.15,<4.0.0" }, @@ -509,7 +504,6 @@ requires-dist = [ { name = "python-dateutil", specifier = ">=2.8.0,<3.0.0" }, { name = "pyyaml", specifier = ">=6.0.0,<7.0.0" }, { name = "ray", extras = ["serve"], marker = "extra == 'ray'", specifier = ">=2.43.0" }, - { name = "requests", marker = "extra == 'storage'", specifier = ">=2.32.2,<3.0.0" }, { name = "six", specifier = ">=1.16.0,<2.0.0" }, { name = "tabulate", specifier = ">=0.9.0,<1.0.0" }, { name = "timing-asgi", specifier = ">=0.3.0,<1.0.0" }, diff --git a/python/test_resources/graph/success_200_isvc/uv.lock b/python/test_resources/graph/success_200_isvc/uv.lock index 02ee1ac69a1..e74e8d59cd9 100644 --- a/python/test_resources/graph/success_200_isvc/uv.lock +++ b/python/test_resources/graph/success_200_isvc/uv.lock @@ -470,18 +470,13 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "asgi-logger", marker = "extra == 'logging'", specifier = ">=0.1.0,<1.0.0" }, - { name = "azure-identity", marker = "extra == 'storage'", specifier = ">=1.15.0,<2.0.0" }, - { name = "azure-storage-blob", marker = "extra == 'storage'", specifier = ">=12.20.0,<13.0.0" }, - { name = "azure-storage-file-share", marker = "extra == 'storage'", specifier = ">=12.16.0,<13.0.0" }, - { name = "boto3", marker = "extra == 'storage'", specifier = ">=1.29.0,<2.0.0" }, { name = "cloudevents", specifier = ">=1.6.2,<2.0.0" }, { name = "fastapi", specifier = ">=0.115.3" }, - { name = "google-cloud-storage", marker = "extra == 'storage'", specifier = ">=2.14.0,<3.0.0" }, { name = "grpc-interceptor", specifier = ">=0.15.4,<1.0.0" }, { name = "grpcio", specifier = ">=1.64.1,<2.0.0" }, { name = "grpcio-tools", specifier = ">=1.64.1,<2.0.0" }, { name = "httpx", specifier = ">=0.27.2,<1.0.0" }, - { name = "huggingface-hub", extras = ["hf-transfer"], marker = "extra == 'storage'", specifier = ">=0.32.0" }, + { name = "kserve-storage", marker = "extra == 'storage'", specifier = "==2" }, { name = "kubernetes", specifier = ">=23.3.0" }, { name = "numpy", specifier = ">=1.26.0,<3.0.0" }, { name = "orjson", specifier = ">=3.9.15,<4.0.0" }, @@ -493,7 +488,6 @@ requires-dist = [ { name = "python-dateutil", specifier = ">=2.8.0,<3.0.0" }, { name = "pyyaml", specifier = ">=6.0.0,<7.0.0" }, { name = "ray", extras = ["serve"], marker = "extra == 'ray'", specifier = ">=2.43.0" }, - { name = "requests", marker = "extra == 'storage'", specifier = ">=2.32.2,<3.0.0" }, { name = "six", specifier = ">=1.16.0,<2.0.0" }, { name = "tabulate", specifier = ">=0.9.0,<1.0.0" }, { name = "timing-asgi", specifier = ">=0.3.0,<1.0.0" }, diff --git a/python/xgb.Dockerfile b/python/xgb.Dockerfile index 20b720ec726..8a29a03e73c 100644 --- a/python/xgb.Dockerfile +++ b/python/xgb.Dockerfile @@ -24,6 +24,13 @@ RUN cd kserve && uv sync --active --no-cache COPY kserve kserve RUN cd kserve && uv sync --active --no-cache +# Copy and install dependencies for kserve-storage using uv +COPY storage/pyproject.toml storage/uv.lock storage/ +RUN cd storage && uv sync --active --no-cache + +COPY storage storage +RUN cd storage && uv pip install . --no-cache + # Copy and install dependencies for xgbserver using uv COPY xgbserver/pyproject.toml xgbserver/uv.lock xgbserver/ RUN cd xgbserver && uv sync --active --no-cache @@ -56,6 +63,7 @@ RUN useradd kserve -m -u 1000 -d /home/kserve COPY --from=builder --chown=kserve:kserve third_party third_party COPY --from=builder --chown=kserve:kserve $VIRTUAL_ENV $VIRTUAL_ENV COPY --from=builder kserve kserve +COPY --from=builder storage storage COPY --from=builder xgbserver xgbserver USER 1000 diff --git a/python/xgbserver/pyproject.toml b/python/xgbserver/pyproject.toml index 2b64866af5a..5f4722cc722 100644 --- a/python/xgbserver/pyproject.toml +++ b/python/xgbserver/pyproject.toml @@ -5,7 +5,8 @@ authors = [ license = {text = "Apache-2.0"} requires-python = "<3.13,>=3.9" dependencies = [ - "kserve[storage] @ file:///${PROJECT_ROOT}/../kserve", + "kserve @ file:///${PROJECT_ROOT}/../kserve", + "kserve-storage @ file:///${PROJECT_ROOT}/../storage", "xgboost~=2.1.1", ] name = "xgbserver" diff --git a/python/xgbserver/uv.lock b/python/xgbserver/uv.lock index f76a6bcc064..5eaf86172bd 100644 --- a/python/xgbserver/uv.lock +++ b/python/xgbserver/uv.lock @@ -929,32 +929,16 @@ dependencies = [ { name = "uvicorn", extra = ["standard"] }, ] -[package.optional-dependencies] -storage = [ - { name = "azure-identity" }, - { name = "azure-storage-blob" }, - { name = "azure-storage-file-share" }, - { name = "boto3" }, - { name = "google-cloud-storage" }, - { name = "huggingface-hub", extra = ["hf-transfer"] }, - { name = "requests" }, -] - [package.metadata] requires-dist = [ { name = "asgi-logger", marker = "extra == 'logging'", specifier = ">=0.1.0,<1.0.0" }, - { name = "azure-identity", marker = "extra == 'storage'", specifier = ">=1.15.0,<2.0.0" }, - { name = "azure-storage-blob", marker = "extra == 'storage'", specifier = ">=12.20.0,<13.0.0" }, - { name = "azure-storage-file-share", marker = "extra == 'storage'", specifier = ">=12.16.0,<13.0.0" }, - { name = "boto3", marker = "extra == 'storage'", specifier = ">=1.29.0,<2.0.0" }, { name = "cloudevents", specifier = ">=1.6.2,<2.0.0" }, { name = "fastapi", specifier = ">=0.115.3" }, - { name = "google-cloud-storage", marker = "extra == 'storage'", specifier = ">=2.14.0,<3.0.0" }, { name = "grpc-interceptor", specifier = ">=0.15.4,<1.0.0" }, { name = "grpcio", specifier = ">=1.64.1,<2.0.0" }, { name = "grpcio-tools", specifier = ">=1.64.1,<2.0.0" }, { name = "httpx", specifier = ">=0.27.2,<1.0.0" }, - { name = "huggingface-hub", extras = ["hf-transfer"], marker = "extra == 'storage'", specifier = ">=0.32.0" }, + { name = "kserve-storage", marker = "extra == 'storage'", specifier = "==2" }, { name = "kubernetes", specifier = ">=23.3.0" }, { name = "numpy", specifier = ">=1.26.0,<3.0.0" }, { name = "orjson", specifier = ">=3.9.15,<4.0.0" }, @@ -966,7 +950,6 @@ requires-dist = [ { name = "python-dateutil", specifier = ">=2.8.0,<3.0.0" }, { name = "pyyaml", specifier = ">=6.0.0,<7.0.0" }, { name = "ray", extras = ["serve"], marker = "extra == 'ray'", specifier = ">=2.43.0" }, - { name = "requests", marker = "extra == 'storage'", specifier = ">=2.32.2,<3.0.0" }, { name = "six", specifier = ">=1.16.0,<2.0.0" }, { name = "tabulate", specifier = ">=0.9.0,<1.0.0" }, { name = "timing-asgi", specifier = ">=0.3.0,<1.0.0" }, @@ -993,6 +976,31 @@ test = [ { name = "tomlkit", specifier = ">=0.12.0,<1.0.0" }, ] +[[package]] +name = "kserve-storage" +version = "0.15.2" +source = { directory = "../storage" } +dependencies = [ + { name = "azure-identity" }, + { name = "azure-storage-blob" }, + { name = "azure-storage-file-share" }, + { name = "boto3" }, + { name = "google-cloud-storage" }, + { name = "huggingface-hub", extra = ["hf-transfer"] }, + { name = "requests" }, +] + +[package.metadata] +requires-dist = [ + { name = "azure-identity", specifier = ">=1.15.0,<2.0.0" }, + { name = "azure-storage-blob", specifier = ">=12.20.0,<13.0.0" }, + { name = "azure-storage-file-share", specifier = ">=12.16.0,<13.0.0" }, + { name = "boto3", specifier = ">=1.29.0,<2.0.0" }, + { name = "google-cloud-storage", specifier = ">=2.14.0,<3.0.0" }, + { name = "huggingface-hub", extras = ["hf-transfer"], specifier = ">=0.32.0" }, + { name = "requests", specifier = ">=2.32.2,<3.0.0" }, +] + [[package]] name = "kubernetes" version = "32.0.1" @@ -2222,7 +2230,8 @@ name = "xgbserver" version = "0.15.2" source = { virtual = "." } dependencies = [ - { name = "kserve", extra = ["storage"] }, + { name = "kserve" }, + { name = "kserve-storage" }, { name = "xgboost" }, ] @@ -2240,7 +2249,8 @@ test = [ [package.metadata] requires-dist = [ - { name = "kserve", extras = ["storage"], directory = "../kserve" }, + { name = "kserve", directory = "../kserve" }, + { name = "kserve-storage", directory = "../storage" }, { name = "xgboost", specifier = "~=2.1.1" }, ] diff --git a/python/xgbserver/xgbserver/model.py b/python/xgbserver/xgbserver/model.py index 4b96bad9fad..4225a2051f6 100644 --- a/python/xgbserver/xgbserver/model.py +++ b/python/xgbserver/xgbserver/model.py @@ -23,7 +23,7 @@ from xgboost import XGBModel from kserve import Model -from kserve.storage import Storage +from kserve_storage import Storage BOOSTER_FILE_EXTENSIONS = (".bst", ".json", ".ubj") diff --git a/test/e2e/explainer/test_art_explainer.py b/test/e2e/explainer/test_art_explainer.py index a085572e1e9..7b2dcfa12e7 100644 --- a/test/e2e/explainer/test_art_explainer.py +++ b/test/e2e/explainer/test_art_explainer.py @@ -38,11 +38,11 @@ pytest.skip("ODH does not support art explainer at the moment", allow_module_level=True) -@pytest.mark.path_based_routing @pytest.mark.explainer @pytest.mark.asyncio(scope="session") async def test_tabular_explainer(rest_v1_client): - service_name = "art-explainer" + suffix = str(uuid.uuid4())[1:6] + service_name = "art-explainer" + suffix isvc = V1beta1InferenceService( api_version=constants.KSERVE_V1BETA1, kind=constants.KSERVE_KIND_INFERENCESERVICE, From 4d1ab6ac3adb1ad4e59dd3b3726b4250649443cb Mon Sep 17 00:00:00 2001 From: Filippe Spolti Date: Tue, 18 Nov 2025 15:28:51 -0300 Subject: [PATCH 6/7] [RHOAIENG-31086] - IG test for raw deployment (#973) chore: Update the e2e tests to not deploy Serverless when running graph tests. Signed-off-by: Spolti --- test/scripts/openshift-ci/deploy.ossm.sh | 4 +- .../scripts/openshift-ci/deploy.serverless.sh | 18 ++++--- test/scripts/openshift-ci/setup-e2e-tests.sh | 47 +++++++++++++------ 3 files changed, 46 insertions(+), 23 deletions(-) diff --git a/test/scripts/openshift-ci/deploy.ossm.sh b/test/scripts/openshift-ci/deploy.ossm.sh index 1b3d69682e2..dda47cf7587 100755 --- a/test/scripts/openshift-ci/deploy.ossm.sh +++ b/test/scripts/openshift-ci/deploy.ossm.sh @@ -36,8 +36,8 @@ EOF wait_for_pod_ready "openshift-operators" "name=istio-operator" -# Create new namespace -oc new-project istio-system +# Create istio-system namespace if it doesn't exist +oc new-project istio-system || true # Install OSSM cat < /dev/null && pwd ) source "${SCRIPT_DIR}/common.sh" -# Create namespaces(openshift-serverless) -oc create ns openshift-serverless +# Set default value for RUNNING_LOCAL if not set +: "${RUNNING_LOCAL:=false}" -# Create operatorGroup -cat </dev/null && echo "" + + # OpenShift service CA + oc get configmap openshift-service-ca.crt -n openshift-config-managed -o jsonpath='{.data.service-ca\.crt}' 2>/dev/null || \ + oc get secret service-ca -n openshift-service-ca -o jsonpath='{.data.service-ca\.crt}' 2>/dev/null | base64 -d || true + } > /tmp/ca.crt + + # Verify we got a valid CA bundle + if [ -s "/tmp/ca.crt" ] && grep -q "BEGIN CERTIFICATE" "/tmp/ca.crt"; then + echo "✅ CA certificate bundle extracted ($(grep -c "BEGIN CERTIFICATE" /tmp/ca.crt) certificates)" + else + echo "❌ Failed to extract CA certificates" + fi +fi + +# Install KServe stack - skip serverless for raw and graph deployments +if [[ ! "$1" =~ raw && ! "$1" =~ graph ]]; then echo "Installing OSSM" $MY_PATH/deploy.ossm.sh echo "Installing Serverless" @@ -94,11 +115,12 @@ kustomize build $PROJECT_ROOT/config/overlays/test | oc apply --server-side=true -f - # Install DSC/DSCI for test. (sometimes there is timing issue when it is under the same kustomization so it is separated) -oc create -f config/overlays/test/dsci.yaml -oc create -f config/overlays/test/dsc.yaml +oc apply -f config/overlays/test/dsci.yaml +oc apply -f config/overlays/test/dsc.yaml # Patch the inferenceservice-config ConfigMap, when running RawDeployment tests -if [ "$1" == "raw" ]; then +if [[ "$1" =~ raw || "$1" =~ graph ]]; then + echo "Patching RAW/Graph deployment, markers: $1" export OPENSHIFT_INGRESS_DOMAIN=$(oc get ingresses.config cluster -o jsonpath='{.spec.domain}') oc patch configmap inferenceservice-config -n kserve --patch-file <(cat config/overlays/test/configmap/inferenceservice-openshift-ci-raw.yaml | envsubst) oc delete pod -n kserve -l control-plane=kserve-controller-manager @@ -106,20 +128,17 @@ if [ "$1" == "raw" ]; then oc patch DataScienceCluster test-dsc --type='json' -p='[{"op": "replace", "path": "/spec/components/kserve/defaultDeploymentMode", "value": "RawDeployment"}]' else export OPENSHIFT_INGRESS_DOMAIN=$(oc get ingresses.config cluster -o jsonpath='{.spec.domain}') - if [ "$1" == "graph" ]; then - oc patch configmap inferenceservice-config -n kserve --patch-file <(cat config/overlays/test/configmap/inferenceservice-openshift-ci-serverless.yaml | envsubst) - else - oc patch configmap inferenceservice-config -n kserve --patch-file <(cat config/overlays/test/configmap/inferenceservice-openshift-ci-serverless-predictor.yaml | envsubst) - fi + oc patch configmap inferenceservice-config -n kserve --patch-file <(cat config/overlays/test/configmap/inferenceservice-openshift-ci-serverless-predictor.yaml | envsubst) fi # Wait until KServe starts +echo "waiting kserve-controller get ready..." oc wait --for=condition=ready pod -l control-plane=kserve-controller-manager -n kserve --timeout=300s -if [ "$1" != "raw" ]; then +if [[ ! "$1" =~ raw && ! "$1" =~ graph ]]; then echo "Installing authorino and kserve gateways" # authorino - curl -sL https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/utils/install.sh | sed "s|kubectl|oc|" | + curl -sL https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/utils/install.sh | sed "s|kubectl|oc|" | bash -s -- -v 0.16.0 fi @@ -190,7 +209,7 @@ metadata: name: kserve-ci-e2e-test EOF -if [ "$1" != "raw" ]; then +if [[ ! "$1" =~ raw && ! "$1" =~ graph ]]; then cat < Date: Wed, 26 Nov 2025 14:43:18 -0500 Subject: [PATCH 7/7] make test --- pkg/openapi/openapi_generated.go | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/pkg/openapi/openapi_generated.go b/pkg/openapi/openapi_generated.go index 3f7fae916aa..0365c00baf6 100644 --- a/pkg/openapi/openapi_generated.go +++ b/pkg/openapi/openapi_generated.go @@ -2070,7 +2070,6 @@ func schema_pkg_apis_serving_v1alpha1_SupportedModelFormat(ref common.ReferenceC }, }, }, - }, }, } @@ -2591,7 +2590,6 @@ func schema_pkg_apis_serving_v1beta1_AuthenticationRef(ref common.ReferenceCallb }, }, }, - }, }, } @@ -4741,7 +4739,6 @@ func schema_pkg_apis_serving_v1beta1_ExplainerExtensionSpec(ref common.Reference }, }, }, - }, }, Dependencies: []string{ @@ -5820,7 +5817,6 @@ func schema_pkg_apis_serving_v1beta1_HuggingFaceRuntimeSpec(ref common.Reference }, }, }, - }, }, Dependencies: []string{ @@ -6580,7 +6576,6 @@ func schema_pkg_apis_serving_v1beta1_LightGBMSpec(ref common.ReferenceCallback) }, }, }, - }, }, Dependencies: []string{ @@ -6892,7 +6887,6 @@ func schema_pkg_apis_serving_v1beta1_ModelFormat(ref common.ReferenceCallback) c }, }, }, - }, }, } @@ -7699,7 +7693,6 @@ func schema_pkg_apis_serving_v1beta1_ONNXRuntimeSpec(ref common.ReferenceCallbac }, }, }, - }, }, Dependencies: []string{ @@ -8117,7 +8110,6 @@ func schema_pkg_apis_serving_v1beta1_PMMLSpec(ref common.ReferenceCallback) comm }, }, }, - }, }, Dependencies: []string{ @@ -8442,7 +8434,6 @@ func schema_pkg_apis_serving_v1beta1_PaddleServerSpec(ref common.ReferenceCallba }, }, }, - }, }, Dependencies: []string{ @@ -9309,7 +9300,6 @@ func schema_pkg_apis_serving_v1beta1_PredictorExtensionSpec(ref common.Reference }, }, }, - }, }, Dependencies: []string{ @@ -10340,7 +10330,6 @@ func schema_pkg_apis_serving_v1beta1_SKLearnSpec(ref common.ReferenceCallback) c }, }, }, - }, }, Dependencies: []string{ @@ -10748,7 +10737,6 @@ func schema_pkg_apis_serving_v1beta1_TFServingSpec(ref common.ReferenceCallback) }, }, }, - }, }, Dependencies: []string{ @@ -11074,7 +11062,6 @@ func schema_pkg_apis_serving_v1beta1_TorchServeSpec(ref common.ReferenceCallback }, }, }, - }, }, Dependencies: []string{ @@ -11967,7 +11954,6 @@ func schema_pkg_apis_serving_v1beta1_TritonSpec(ref common.ReferenceCallback) co }, }, }, - }, }, Dependencies: []string{ @@ -12761,7 +12747,6 @@ func schema_pkg_apis_serving_v1beta1_XGBoostSpec(ref common.ReferenceCallback) c }, }, }, - }, }, Dependencies: []string{